Multiple statements inside one BEGIN ... END block - sql-server

In my installer I have to make a minor change in the schema:
IF NOT EXISTS (SELECT * FROM sys.columns WHERE object_id = OBJECT_ID(N'[dbo].[UserProfiles]') AND name = 'AllCheckboxesChecked')
BEGIN
ALTER TABLE [dbo].[UserProfiles] ADD [AllCheckboxesChecked] [bit] CONSTRAINT [DF_UserProfiles_AllCheckboxesChecked] DEFAULT 0 NOT NULL
UPDATE [dbo].[UserProfiles] SET [AllCheckboxesChecked]=1 WHERE [CheckedBoxes] LIKE '%#ALL#%'
END
GO
In SSMS this works, but not in Advanced Installer, where it fails with the error message that the column AllCheckboxesChecked does not exist. So I tried:
IF NOT EXISTS (SELECT * FROM sys.columns WHERE object_id = OBJECT_ID(N'[dbo].[UserProfiles]') AND name = 'AllCheckboxesChecked')
BEGIN
ALTER TABLE [dbo].[UserProfiles] ADD [AllCheckboxesChecked] [bit] CONSTRAINT [DF_UserProfiles_AllCheckboxesChecked] DEFAULT 0 NOT NULL
GO
UPDATE [dbo].[UserProfiles] SET [AllCheckboxesChecked]=1 WHERE [CheckedBoxes] LIKE '%#ALL#%'
END
GO
but this throws syntax errors as well (not in SSMS, only in AdvInst), so I guess that GO is not allowed inside the BEGIN...END block. The connection is configured as follows:
Connection type: Microsoft SQL Server / MSDE
Connection mode: ODBC Driver
ODBC Driver: SQL Server
Use 64-bit ODBC resource: No
What steps can I take to get the column created and populated with correct values iff the installer runs on a DB where the column does not yet exist?

The column doesn't exist error is due to validation that occurs on existing objects. Since the table already exists, the parser / compiler will verify that the table contains all of the referenced columns.
In order to get around such timing issues with object verification, you can enclose the statement in an EXEC which will not be verified until run-time:
BEGIN
ALTER TABLE [dbo].[UserProfiles]
ADD [AllCheckboxesChecked] [bit]
CONSTRAINT [DF_UserProfiles_AllCheckboxesChecked] DEFAULT 0
NOT NULL;
EXEC(N'UPDATE [dbo].[UserProfiles]
SET [AllCheckboxesChecked]=1
WHERE [CheckedBoxes] LIKE ''%#ALL#%''');
END;

The GO statement is a batch terminator and items in a batch only get committed at the end of the batch, either at the next GO statement or when the end of the script is reached. In your case the batch containing the ALTER COLUMN statement has not yet been committed and therefore you get an error that the column does not exist. You will have to split up your scripts in two parts.

GO is a batch terminator - it's specific to the tool you are using rather than SQL Server - so if you put
Statement1
GO
Statemetn2
That will be sent to SQL Server as two separate executions (batches).
Basically you are breaking your query up into two batches and the first batch you aren't closing the BEGIN block and in the second batch you aren't startinng the END block!

Related

Procedure for Altering Table and updating keeps failing with invalid column

I'm trying to add new columns to a table then update the table and set the new column with a date format change of the old column.
I have my procedure set out as follows:
begin
alter table [dbo].[mytable]
add New_Field1 varchar(24)
end
......
update [dbo].[SMR06_TARGET]
set New_Field1 = convert(varchar(24),Old_Field1,103)
.....
I have multiple alter table statements at the top of the table and update statements at the bottom for each new column. I think this is a rule with SQL keeping DDL at top and DML at bottom.
Ok so everytime I execute this to create the procedure it fails with incorrect column name New_Field1. I really can't peg down what is causing this. I've tried different variations of BEGIN....END tried commenting out the apprent offending statement, then it runs, then it fails again with the next statement.
I'm reckoning it's something to do with the way the statement(s) are terminated. I'm not sure as haven't done this type of procedure statement before with mixed DDL/DML.
Any hints would be most welcome.
Thanks
Andrew
You need to batch the statement that adds the column separately from the statement that updates it.
BEGIN TRANSACTION
GO
ALTER TABLE [dbo].[mytable]
ADD New_Field1 varchar(24) NULL
GO
UPDATE [dbo].[mytable]
SET New_Field1 = convert(varchar(24),Old_Field1,103)
GO
ALTER TABLE dbo.Batch SET (LOCK_ESCALATION = TABLE)
GO
COMMIT
The entire batch is reviewed by the parser before it starts executing the first line. Adding Old_Field1 is in the same batch as the reference to use Old_Field1. At the time the parser considers the statement containing Old_Field1, the statement to add Old_Field1 has not been executed, so that field does not yet exist.
If you're running in SSMS, include GO between each statement to force multiple batches. If you're running this in another tool that can't use GO, you'll need to submit each statement individually to ensure that they are fully executed before the next step is parsed.

How do I make ALTER COLUMN idempotent?

I have a migration script with the following statement:
ALTER TABLE [Tasks] ALTER COLUMN [SortOrder] int NOT NULL
What will happen if I run that twice? Will it change anything the second time? MS SQL Management Studio just reports "Command(s) completed successfully", but with no details on whether they actually did anything.
If it's not already idempotent, how do I make it so?
I would say that second time, SQL Server checks metadata and do nothing because nothing has changed.
But if you don't like possibility of multiple execution you can add simple condition to your script:
CREATE TABLE Tasks(SortOrder VARCHAR(100));
IF NOT EXISTS (SELECT 1
FROM INFORMATION_SCHEMA.COLUMNS
WHERE [TABLE_NAME] = 'Tasks'
AND [COLUMN_NAME] = 'SortOrder'
AND IS_NULLABLE = 'NO'
AND DATA_TYPE = 'INT')
BEGIN
ALTER TABLE [Tasks] ALTER COLUMN [SortOrder] INT NOT NULL
END
SqlFiddleDemo
When you execute it the second time, the query gets executed but since the table is already altered, there is no effect. So it makes no effect on the table.
No change is there when the script executes twice.
Here is a good MSDN read about: Inside ALTER TABLE
Let's look at what SQL Server does internally when performing an ALTER
TABLE command. SQL Server can carry out an ALTER TABLE command in any
of three ways:
SQL Server might need to change only metadata.
SQL Server might need to examine all the existing data to make sure
it's compatible with the change but then change only metadata.
SQL Server might need to physically change every row.

How can I omit system databases and allow SQL Server 2008 agent job to move past ERROR_NUMBER 208?

I have created a SQL Server 2008 Agent job that runs against all databases on the server. I am using the undocumented MS procedure, sp_MSforEachDB. I am specifying only certain databases to be processed and when these databases are found, if ERROR_NUMBER = 208, I want a table to be created and processing to continue to the next database. I also want to omit the system databases so the tabel doesn't get created in a system database. I though by specifying the names of the databases I want to process, I could create a table where none existed. The code I show below works if the 'focus' is set on a particular database. Also, when I run it, I get a table created in the master database, which I do not want to do. What I am trying to do with the code I created is:
1. select all the databases on the SQL server with name like "xyz_%"
2. if table "SESSIONS" exists, then delete all records older than 5 minutes
3. if table "SESSIONS" doesn't exist, then create it and continue processing -
this is where I get the 208 error - invalid object because this table does not exist
When the job sees there is no Sessions table, the job stops and no other databases are processed. I need all the named databases in 1 to be processed. If the SESSIONS table is not present, I get a 208 error that says "Invalid object name 'master.SchemaName.sessions'. If I set the 'focus' to the database without the sessions table, my code runs and the tables is created but I still get an error 208 - Invalid object name 'master.SchemaName.sessions'. No other error are displayed.
If someone could look at my code to see where my problem is, that would be great.
I have verified that I am getting the 208 error by using the Try Catch below. I also get error 2760 on the 2nd Try Catch that says the specified schema doesn't exist or I don't have access. I am an admin on this server.
I have been using SQL for a few years and I don't consider myself an expert. I have researched this on Google to come up with the code I have below. Any help would be appreciated.
declare #ERROR INT
BEGIN TRY
exec sp_MSforeachdb 'IF "[?]" NOT IN ("master", "model","msdb", "tempdb")
BEGIN
DELETE FROM [?].schema.sessions WHERE name like ''xyz_%''
AND sessionStart <DATEADD(mi, -5,GETDATE())
END'
END TRY
BEGIN CATCH
SELECT ERROR_NUMBER() AS error_number, ERROR_MESSAGE() AS error_message
SELECT #ERROR = ERROR_NUMBER()
If #ERROR = 208
BEGIN TRY
/* create the Sessions table */
BEGIN
CREATE TABLE [SCHEMA].[SESSIONS](
[authuser] [varchar](30) NULL,
[sessionID] [char](36) NULL,
[sessionStart] [datetime] NULL)
grant select,delete on reviewadmin.sessions to public;
END
END TRY
BEGIN CATCH
SELECT ERROR_NUMBER() AS error_number, ERROR_MESSAGE() AS error_message
END CATCH
END CATCH
You could try using DB_ID(), I believe the DB_ID() is always 1-4 (unless you have a distribution database). In theory, just check to see if DB_ID() is greater than 4:
exec sp_MSforeachdb 'IF DB_ID(''?'')) > 4
BEGIN
DELETE FROM [?].schema.sessions WHERE name like ''xyz_%''
AND sessionStart <DATEADD(mi, -5,GETDATE())
END'
Also it doesn't look like you're using the correct database in the error handler - surely your try catch that creates the table should be in the call to sp_MSforeachdb as well?
I mean in your text you're running for each db, including this in a try catch:
CREATE TABLE [?].[SCHEMA].[SESSIONS]
(
[authuser] [VARCHAR](30) NULL,
[sessionID] [CHAR](36) NULL,
[sessionStart] [DATETIME] NULL
)
One more thing - at the moment you seem to have hard coded the value reviewadmin.sessions in your permission granting code - I assume you meant that to be your new [?].[Schema].[Sessions] table - again which should be in the call to sp_MSforeachdb so it knows which database to run on.
I hope I've given you enough to go on, I'm not at a machine where I can write & test the whole thing I'm afraid!
If you just want databases with the name xyz_% :
exec sp_MSforeachdb 'IF DB_NAME() LIKE ''xyz_%''
BEGIN
DELETE FROM [?].schema.sessions WHERE name like ''xyz_%''
AND sessionStart <DATEADD(mi, -5,GETDATE())
END'
Note we can now skip the check for DB_ID > 4, since none of the system databases match your naming convention anyway.
OK, I've rethought this. Rather than trying and then catching the error if it fails, how about this new plan:
Check if the database matches the naming convention you want.
Check if the table exists.
Create the table if it doesn't, grant permissions as before.
Do the insert.
This should hopefully do it:
EXEC Sp_msforeachdb 'IF ''?'' LIKE ''xyz_%''
BEGIN
IF OBJECT_ID(''?.REVIEWADMIN.Sessions'', ''U'') IS NULL
BEGIN
CREATE TABLE [?].ReviewAdmin.Sessions
(
[authuser] [VARCHAR](30) NULL,
[sessionID] [CHAR](36) NULL,
[sessionStart] [DATETIME] NULL
)
grant select,delete on reviewadmin.sessions to public;
END
DELETE FROM [?].ReviewAdmin.Sessions
WHERE sessionStart < DATEADD(mi, -5,GETDATE())
END'

Why does this update throw an error even though the Alter Table command should be finished?

This has been a nagging issue for me for some time and I would love to know the reason why these SQL Batch commands aren't working.
I have a table that I use to hold configuration settings for a system. When a new setting is added, we add a new field to the table. During an update, I need to change a slew of databases on the server with the same script. Generally, they are all in the same state and I can just do the following:
Alter Table Configuration Add ShowClassesInCheckin bit;
GO
Update Configuration Set ShowClassesInCheckin=ShowFacilitiesInCheckin;
GO
This works fine. However, sometimes one or two databases get updated so I want to write conditional logic to make these changes only if the field doesn't already exist:
if Not Exists(select * from sys.columns where Name = N'ShowClassesInCheckin' AND Object_ID = Object_ID(N'Configuration'))
BEGIN
Alter Table Configuration Add ShowClassesInCheckin bit;
Update Configuration Set ShowClassesInCheckin=ShowFacilitiesInCheckin;
END;
GO
In this case, I get an error: "Invalid column name 'ShowClassesInCheckin'" Now, this makes sense in that the Alter Table isn't comitted in the batch before the Update is called (it doesn't work without the "GO" between the Alter and Update). But that doesn't help...I still don't know how to I achieve what I am after...
The entire SQL script is parsed before it's executed. During the parsing phase, the column will not exist, so the parser generates an error. The error is raised before the first line of the script is executed.
The solution is dynamic SQL:
exec (N'Update Configuration Set ShowClassesInCheckin=ShowFacilitiesInCheckin;')
This won't get parsed before the exec is reached, and by then, the column will exist.
An alternative that should work is to re-introduce the go. This means that you need to use something else as the condition for the update, possibly based on database name.
if Not Exists(select * from sys.columns where Name = N'ShowClassesInCheckin' AND Object_ID = Object_ID(N'Configuration'))
BEGIN
Alter Table Configuration Add ShowClassesInCheckin bit;
END;
GO
if *new condition here*
BEGIN
Update Configuration Set ShowClassesInCheckin=ShowFacilitiesInCheckin;
END;
GO

Errors: "INSERT EXEC statement cannot be nested." and "Cannot use the ROLLBACK statement within an INSERT-EXEC statement." How to solve this?

I have three stored procedures Sp1, Sp2 and Sp3.
The first one (Sp1) will execute the second one (Sp2) and save returned data into #tempTB1 and the second one will execute the third one (Sp3) and save data into #tempTB2.
If I execute the Sp2 it will work and it will return me all my data from the Sp3, but the problem is in the Sp1, when I execute it it will display this error:
INSERT EXEC statement cannot be nested
I tried to change the place of execute Sp2 and it display me another error:
Cannot use the ROLLBACK statement
within an INSERT-EXEC statement.
This is a common issue when attempting to 'bubble' up data from a chain of stored procedures. A restriction in SQL Server is you can only have one INSERT-EXEC active at a time. I recommend looking at How to Share Data Between Stored Procedures which is a very thorough article on patterns to work around this type of problem.
For example a work around could be to turn Sp3 into a Table-valued function.
This is the only "simple" way to do this in SQL Server without some giant convoluted created function or executed sql string call, both of which are terrible solutions:
create a temp table
openrowset your stored procedure data into it
EXAMPLE:
INSERT INTO #YOUR_TEMP_TABLE
SELECT * FROM OPENROWSET ('SQLOLEDB','Server=(local);TRUSTED_CONNECTION=YES;','set fmtonly off EXEC [ServerName].dbo.[StoredProcedureName] 1,2,3')
Note: You MUST use 'set fmtonly off', AND you CANNOT add dynamic sql to this either inside the openrowset call, either for the string containing your stored procedure parameters or for the table name. Thats why you have to use a temp table rather than table variables, which would have been better, as it out performs temp table in most cases.
OK, encouraged by jimhark here is an example of the old single hash table approach: -
CREATE PROCEDURE SP3 as
BEGIN
SELECT 1, 'Data1'
UNION ALL
SELECT 2, 'Data2'
END
go
CREATE PROCEDURE SP2 as
BEGIN
if exists (select * from tempdb.dbo.sysobjects o where o.xtype in ('U') and o.id = object_id(N'tempdb..#tmp1'))
INSERT INTO #tmp1
EXEC SP3
else
EXEC SP3
END
go
CREATE PROCEDURE SP1 as
BEGIN
EXEC SP2
END
GO
/*
--I want some data back from SP3
-- Just run the SP1
EXEC SP1
*/
/*
--I want some data back from SP3 into a table to do something useful
--Try run this - get an error - can't nest Execs
if exists (select * from tempdb.dbo.sysobjects o where o.xtype in ('U') and o.id = object_id(N'tempdb..#tmp1'))
DROP TABLE #tmp1
CREATE TABLE #tmp1 (ID INT, Data VARCHAR(20))
INSERT INTO #tmp1
EXEC SP1
*/
/*
--I want some data back from SP3 into a table to do something useful
--However, if we run this single hash temp table it is in scope anyway so
--no need for the exec insert
if exists (select * from tempdb.dbo.sysobjects o where o.xtype in ('U') and o.id = object_id(N'tempdb..#tmp1'))
DROP TABLE #tmp1
CREATE TABLE #tmp1 (ID INT, Data VARCHAR(20))
EXEC SP1
SELECT * FROM #tmp1
*/
My work around for this problem has always been to use the principle that single hash temp tables are in scope to any called procs. So, I have an option switch in the proc parameters (default set to off). If this is switched on, the called proc will insert the results into the temp table created in the calling proc. I think in the past I have taken it a step further and put some code in the called proc to check if the single hash table exists in scope, if it does then insert the code, otherwise return the result set. Seems to work well - best way of passing large data sets between procs.
This trick works for me.
You don't have this problem on remote server, because on remote server, the last insert command waits for the result of previous command to execute. It's not the case on same server.
Profit that situation for a workaround.
If you have the right permission to create a Linked Server, do it.
Create the same server as linked server.
in SSMS, log into your server
go to "Server Object
Right Click on "Linked Servers", then "New Linked Server"
on the dialog, give any name of your linked server : eg: THISSERVER
server type is "Other data source"
Provider : Microsoft OLE DB Provider for SQL server
Data source: your IP, it can be also just a dot (.), because it's localhost
Go to the tab "Security" and choose the 3rd one "Be made using the login's current security context"
You can edit the server options (3rd tab) if you want
Press OK, your linked server is created
now your Sql command in the SP1 is
insert into #myTempTable
exec THISSERVER.MY_DATABASE_NAME.MY_SCHEMA.SP2
Believe me, it works even you have dynamic insert in SP2
I found a work around is to convert one of the prods into a table valued function. I realize that is not always possible, and introduces its own limitations. However, I have been able to always find at least one of the procedures a good candidate for this. I like this solution, because it doesn't introduce any "hacks" to the solution.
I encountered this issue when trying to import the results of a Stored Proc into a temp table, and that Stored Proc inserted into a temp table as part of its own operation. The issue being that SQL Server does not allow the same process to write to two different temp tables at the same time.
The accepted OPENROWSET answer works fine, but I needed to avoid using any Dynamic SQL or an external OLE provider in my process, so I went a different route.
One easy workaround I found was to change the temporary table in my stored procedure to a table variable. It works exactly the same as it did with a temp table, but no longer conflicts with my other temp table insert.
Just to head off the comment I know that a few of you are about to write, warning me off Table Variables as performance killers... All I can say to you is that in 2020 it pays dividends not to be afraid of Table Variables. If this was 2008 and my Database was hosted on a server with 16GB RAM and running off 5400RPM HDDs, I might agree with you. But it's 2020 and I have an SSD array as my primary storage and hundreds of gigs of RAM. I could load my entire company's database to a table variable and still have plenty of RAM to spare.
Table Variables are back on the menu!
I recommend to read this entire article. Below is the most relevant section of that article that addresses your question:
Rollback and Error Handling is Difficult
In my articles on Error and Transaction Handling in SQL Server, I suggest that you should always have an error handler like
BEGIN CATCH
IF ##trancount > 0 ROLLBACK TRANSACTION
EXEC error_handler_sp
RETURN 55555
END CATCH
The idea is that even if you do not start a transaction in the procedure, you should always include a ROLLBACK, because if you were not able to fulfil your contract, the transaction is not valid.
Unfortunately, this does not work well with INSERT-EXEC. If the called procedure executes a ROLLBACK statement, this happens:
Msg 3915, Level 16, State 0, Procedure SalesByStore, Line 9 Cannot use the ROLLBACK statement within an INSERT-EXEC statement.
The execution of the stored procedure is aborted. If there is no CATCH handler anywhere, the entire batch is aborted, and the transaction is rolled back. If the INSERT-EXEC is inside TRY-CATCH, that CATCH handler will fire, but the transaction is doomed, that is, you must roll it back. The net effect is that the rollback is achieved as requested, but the original error message that triggered the rollback is lost. That may seem like a small thing, but it makes troubleshooting much more difficult, because when you see this error, all you know is that something went wrong, but you don't know what.
I had the same issue and concern over duplicate code in two or more sprocs. I ended up adding an additional attribute for "mode". This allowed common code to exist inside one sproc and the mode directed flow and result set of the sproc.
what about just store the output to the static table ? Like
-- SubProcedure: subProcedureName
---------------------------------
-- Save the value
DELETE lastValue_subProcedureName
INSERT INTO lastValue_subProcedureName (Value)
SELECT #Value
-- Return the value
SELECT #Value
-- Procedure
--------------------------------------------
-- get last value of subProcedureName
SELECT Value FROM lastValue_subProcedureName
its not ideal, but its so simple and you don't need to rewrite everything.
UPDATE:
the previous solution does not work well with parallel queries (async and multiuser accessing) therefore now Iam using temp tables
-- A local temporary table created in a stored procedure is dropped automatically when the stored procedure is finished.
-- The table can be referenced by any nested stored procedures executed by the stored procedure that created the table.
-- The table cannot be referenced by the process that called the stored procedure that created the table.
IF OBJECT_ID('tempdb..#lastValue_spGetData') IS NULL
CREATE TABLE #lastValue_spGetData (Value INT)
-- trigger stored procedure with special silent parameter
EXEC dbo.spGetData 1 --silent mode parameter
nested spGetData stored procedure content
-- Save the output if temporary table exists.
IF OBJECT_ID('tempdb..#lastValue_spGetData') IS NOT NULL
BEGIN
DELETE #lastValue_spGetData
INSERT INTO #lastValue_spGetData(Value)
SELECT Col1 FROM dbo.Table1
END
-- stored procedure return
IF #silentMode = 0
SELECT Col1 FROM dbo.Table1
Declare an output cursor variable to the inner sp :
#c CURSOR VARYING OUTPUT
Then declare a cursor c to the select you want to return.
Then open the cursor.
Then set the reference:
DECLARE c CURSOR LOCAL FAST_FORWARD READ_ONLY FOR
SELECT ...
OPEN c
SET #c = c
DO NOT close or reallocate.
Now call the inner sp from the outer one supplying a cursor parameter like:
exec sp_abc a,b,c,, #cOUT OUTPUT
Once the inner sp executes, your #cOUT is ready to fetch. Loop and then close and deallocate.
If you are able to use other associated technologies such as C#, I suggest using the built in SQL command with Transaction parameter.
var sqlCommand = new SqlCommand(commandText, null, transaction);
I've created a simple Console App that demonstrates this ability which can be found here:
https://github.com/hecked12/SQL-Transaction-Using-C-Sharp
In short, C# allows you to overcome this limitation where you can inspect the output of each stored procedure and use that output however you like, for example you can feed it to another stored procedure. If the output is ok, you can commit the transaction, otherwise, you can revert the changes using rollback.
On SQL Server 2008 R2, I had a mismatch in table columns that caused the Rollback error. It went away when I fixed my sqlcmd table variable populated by the insert-exec statement to match that returned by the stored proc. It was missing org_code. In a windows cmd file, it loads result of stored procedure and selects it.
set SQLTXT= declare #resets as table (org_id nvarchar(9), org_code char(4), ^
tin(char9), old_strt_dt char(10), strt_dt char(10)); ^
insert #resets exec rsp_reset; ^
select * from #resets;
sqlcmd -U user -P pass -d database -S server -Q "%SQLTXT%" -o "OrgReport.txt"

Resources