Add column to table and then update it inside transaction - sql

I am creating a script that will be run in a MS SQL server. This script will run multiple statements and needs to be transactional, if one of the statement fails the overall execution is stopped and any changes are rolled back.
I am having trouble creating this transactional model when issuing ALTER TABLE statements to add columns to a table and then updating the newly added column. In order to access the newly added column right away, I use a GO command to execute the ALTER TABLE statement, and then call my UPDATE statement. The problem I am facing is that I cannot issue a GO command inside an IF statement. The IF statement is important within my transactional model. This is a sample code of the script I am trying to run. Also notice that issuing a GO command, will discard the #errorCode variable, and will need to be declared down in the code before being used (This is not in the code below).
BEGIN TRANSACTION
DECLARE #errorCode INT
SET #errorCode = ##ERROR
-- **********************************
-- * Settings
-- **********************************
IF #errorCode = 0
BEGIN
BEGIN TRY
ALTER TABLE Color ADD [CodeID] [uniqueidentifier] NOT NULL DEFAULT ('{00000000-0000-0000-0000-000000000000}')
GO
END TRY
BEGIN CATCH
SET #errorCode = ##ERROR
END CATCH
END
IF #errorCode = 0
BEGIN
BEGIN TRY
UPDATE Color
SET CodeID= 'B6D266DC-B305-4153-A7AB-9109962255FC'
WHERE [Name] = 'Red'
END TRY
BEGIN CATCH
SET #errorCode = ##ERROR
END CATCH
END
-- **********************************
-- * Check #errorCode to issue a COMMIT or a ROLLBACK
-- **********************************
IF #errorCode = 0
BEGIN
COMMIT
PRINT 'Success'
END
ELSE
BEGIN
ROLLBACK
PRINT 'Failure'
END
So what I would like to know is how to go around this problem, issuing ALTER TABLE statements to add a column and then updating that column, all within a script executing as a transactional unit.

GO is not a T-SQL command. Is a batch delimiter. The client tool (SSM, sqlcmd, osql etc) uses it to effectively cut the file at each GO and send to the server the individual batches. So obviously you cannot use GO inside IF, nor can you expect variables to span scope across batches.
Also, you cannot catch exceptions without checking for the XACT_STATE() to ensure the transaction is not doomed.
Using GUIDs for IDs is always at least suspicious.
Using NOT NULL constraints and providing a default 'guid' like '{00000000-0000-0000-0000-000000000000}' also cannot be correct.
Updated:
Separate the ALTER and UPDATE into two batches.
Use sqlcmd extensions to break the script on error. This is supported by SSMS when sqlcmd mode is on, sqlcmd, and is trivial to support it in client libraries too: dbutilsqlcmd.
use XACT_ABORT to force error to interrupt the batch. This is frequently used in maintenance scripts (schema changes). Stored procedures and application logic scripts in general use TRY-CATCH blocks instead, but with proper care: Exception handling and nested transactions.
example script:
:on error exit
set xact_abort on;
go
begin transaction;
go
if columnproperty(object_id('Code'), 'ColorId', 'AllowsNull') is null
begin
alter table Code add ColorId uniqueidentifier null;
end
go
update Code
set ColorId = '...'
where ...
go
commit;
go
Only a successful script will reach the COMMIT. Any error will abort the script and rollback.
I used COLUMNPROPERTY to check for column existance, you could use any method you like instead (eg. lookup sys.columns).

Orthogonal to Remus's comments, what you can do is execute the update in an sp_executesql.
ALTER TABLE [Table] ADD [Xyz] NVARCHAR(256);
DECLARE #sql NVARCHAR(2048) = 'UPDATE [Table] SET [Xyz] = ''abcd'';';
EXEC sys.sp_executesql #query = #sql;
We've needed to do this when creating upgrade scripts. Usually we just use GO but it has been necessary to do things conditionally.

I almost agree with Remus but you can do this with SET XACT_ABORT ON and XACT_STATE
Basically
SET XACT_ABORT ON will abort each batch on error and ROLLBACK
Each batch is separated by GO
Execution jumps to the next batch on error
Use XACT_STATE() will test if the transaction is still valid
Tools like Red Gate SQL Compare use this technique
Something like:
SET XACT_ABORT ON
GO
BEGIN TRANSACTION
GO
IF COLUMNPROPERTY(OBJECT_ID('Color'), 'CodeID', ColumnId) IS NULL
ALTER TABLE Color ADD CodeID [uniqueidentifier] NULL
GO
IF XACT_STATE() = 1
UPDATE Color
SET CodeID= 'B6D266DC-B305-4153-A7AB-9109962255FC'
WHERE [Name] = 'Red'
GO
IF XACT_STATE() = 1
COMMIT TRAN
--else would be rolled back
I've also removed the default. No value = NULL for GUID values. It's meant to be unique: don't try and set every row to all zeros because it will end in tears...

Have you tried it without the GO?
Normally you should not mix table changes and data changes in the same script.

Another alternative, if you don't want to split the code into separate batches, is to use EXEC to create a nested scope/batch
as here

Related

Properly understanding the error Cannot use the ROLLBACK statement within an INSERT-EXEC statement - Msg 50000, Level 16, State 1

I understand there is a regularly quoted answer that is meant to address this question, but I believe there is not enough explanation on that thread to really answer the question.
Why earlier answers are inadequate
The first (and accepted) answer simply says this is a common problem and talks about having only one active insert-exec at a time (which is only the first half of the question asked there and doesn't address the ROLLBACK error). The given workaround is to use a table-valued function - which does not help my scenario where my stored procedure needs to update data before returning a result set.
The second answer talks about using openrowset but notes you cannot dynamically specify argument values for the stored procedure - which does not help my scenario because different users need to call my procedure with different parameters.
The third answer provides something called "the old single hash table approach" but does not explain whether it is addressing part 1 or 2 of the question, nor how it works, nor why.
No answer explains why the database is giving this error in the first place.
My use case / requirements
To give specifics for my scenario (although simplified and generic), I have procedures something like below.
In a nutshell though - the first procedure will return a result set, but before it does so, it updates a status column. Effectively these records represent records that need to be synchronised somewhere, so when you call this procedure the procedure will flag the records as being "in progress" for sync.
The second stored procedure calls that first one. Of course the second stored procedure wants to take those records and perform inserts and updates on some tables - to keep those tables in sync with whatever data was returned from the first procedure. After performing all the updates, the second procedure then calls a third procedure - within a cursor (ie. row by row on all the rows in the result set that was received from the first procedure) - for the purpose of setting the status on the source data to "in sync". That is, one by one it goes back and says "update the sync status on record id 1, to 'in sync'" ... and then record 2, and then record 3, etc.
The issue I'm having is that calling the second procedure results in the error
Msg 50000, Level 16, State 1, Procedure getValuesOuterCall, Line 484 [Batch Start Line 24]
Cannot use the ROLLBACK statement within an INSERT-EXEC statement.
but calling the first procedure directly causes no error.
Procedure 1
-- Purpose here is to return a result set,
-- but for every record in the set we want to set a status flag
-- to another value as well.
alter procedure getValues #username, #password, #target
as
begin
set xact_abort on;
begin try
begin transaction;
declare #tableVariable table (
...
);
update someOtherTable
set something = somethingElse
output
someColumns
into #tableVariable
from someTable
join someOtherTable
join etc
where someCol = #username
and etc
;
select
someCols
from #tableVariable
;
commit;
end try
begin catch
if ##trancount > 0 rollback;
declare #msg nvarchar(2048) = error_message() + ' Error line: ' + CAST(ERROR_LINE() AS nvarchar(100));
raiserror (#msg, 16, 1);
return 55555
end catch
end
Procedure 2
-- Purpose here is to obtain the result set from earlier procedure
-- and then do a bunch of data updates based on the result set.
-- Lastly, for each row in the set, call another procedure which will
-- update that status flag to another value.
alter procedure getValuesOuterCall #username, #password, #target
as
begin
set xact_abort on;
begin try
begin transaction;
declare #anotherTableVariable
insert into #anotherTableVariable
exec getValues #username = 'blah', #password = #somePass, #target = ''
;
with CTE as (
select someCols
from #anotherTableVariable
join someOtherTables, etc;
)
merge anUnrelatedTable as target
using CTE as source
on target.someCol = source.someCol
when matched then update
target.yetAnotherCol = source.yetAnotherCol,
etc
when not matched then
insert (someCols, andMoreCols, etc)
values ((select someSubquery), source.aColumn, source.etc)
;
declare #myLocalVariable int;
declare #mySecondLocalVariable int;
declare lcur_myCursor cursor for
select keyColumn
from #anotherTableVariable
;
open lcur_muCursor;
fetch lcur_myCursor into #myLocalVariable;
while ##fetch_status = 0
begin
select #mySecondLocalVariable = someCol
from someTable
where someOtherCol = #myLocalVariable;
exec thirdStoredProcForSettingStatusValues #id = #mySecondLocalVariable, etc
end
deallocate lcur_myCursor;
commit;
end try
begin catch
if ##trancount > 0 rollback;
declare #msg nvarchar(2048) = error_message() + ' Error line: ' + CAST(ERROR_LINE() AS nvarchar(100));
raiserror (#msg, 16, 1);
return 55555
end catch
end
The parts I don't understand
Firstly, I have no explicit 'rollback' (well, except in the catch block) - so I have to presume that an implicit rollback is causing the issue - but it is difficult to understand where the root of this problem is; I am not even entirely sure which stored procedure is causing the issue.
Secondly, I believe the statements to set xact_abort and begin transaction are required - because in procedure 1 I am updating data before returning the result set. In procedure 2 I am updating data before I call a third procedure to update further data.
Thirdly, I don't think procedure 1 can be converted to a table-valued function because the procedure performs a data update (which would not be allowed in a function?)
Things I have tried
I removed the table variable from procedure 2 and actually created a permanent table to store the results coming back from procedure 1. Before calling procedure 1, procedure 2 would truncate the table. I still got the rollback error.
I replaced the table variable in procedure 1 with a temporary table (ie. single #). I read the articles about how such a table persists for the lifetime of the connection, so within procedure 1 I had drop table if exists... and then create table #.... I still got the rollback error.
Lastly
I still don't understand exactly what is the problem - what is Microsoft struggling to accomplish here? Or what is the scenario that SQL Server cannot accommodate for a requirement that appears to be fairly straightforward: One procedure returns a result set. The calling procedure wants to perform actions based on what's in that result set. If the result set is scoped to the first procedure, then why can't SQL Server just create a temporary copy of the result set within the scope of the second procedure so that it can be acted upon?
Or have I missed it completely and the issue has something to do with the final call to a third procedure, or maybe to do with using try ... catch - for example, perhaps the logic is totally fine but for some reason it is hitting the catch block and the rollback there is the problem (ie. so if I fix the underlying reason leading us to the catch block, all will resolve)?

Alter statement in a Transaction

I execute the code below:
use AdventureWorks2008R2
begin transaction
BEGIN
alter table HumanResources.Department add newcolumn int
update HumanResources.Department set newcolumn=1 where departmentid=1
END
commit
The error I get is:
Invalid column name 'newcolumn'.
Can ALTER statements be included in Transactions like this? If so, how can I prevent this error?
I have researched this online e.g. here. I have not found an answer to my specific question.
Yes, you can include an ALTER in a transaction. The problem is that the parser validates the syntax for your UPDATE statement, and can't "see" that you are also performing an ALTER. One workaround is to use dynamic SQL, so that the parser doesn't inspect your syntax (and validate column names) until runtime, where the ALTER will have already happened:
BEGIN TRANSACTION;
ALTER TABLE HumanResources.Department ADD newcolumn INT;
EXEC sp_executesql N'UPDATE HumanResources.Department
SET newcolumn = 1 WHERE DepartmentID = 1;';
COMMIT TRANSACTION;
Note that indentation makes code blocks much more easily identifiable (and your BEGIN/END was superfluous).
If you check the existence of column, then it should work.
BEGIN TRANSACTION;
IF COL_LENGTH('table_name', 'newcolumn') IS NULL
BEGIN
ALTER TABLE table_name ADD newcolumn INT;
END
EXEC sp_executesql N'UPDATE table_name
SET newcolumn = 1 WHERE DepartmentID = 1;';
COMMIT TRANSACTION;
Aaron has explained everything already. Another alternative that works for ad-hoc scripts in SSMS is to insert the batch separator GO so that the script is sent as two parts to the server. This only works if it is valid to split the script in the first place (you can't split an IF body for example).

T-SQL install / upgrade script as a transaction

I'm trying to write a single T-SQL script which will upgrade a system which is currently in deployment. The script will contain a mixture of:
New tables
New columns on existing tables
New functions
New stored procedures
Changes to stored procedures
New views
etc.
As it's a reasonably large upgrade I want the script to rollback if a single part of it fails. I have an outline of my attempted code below:
DECLARE #upgrade NVARCHAR(32);
SELECT #upgrade = 'my upgrade';
BEGIN TRANSACTION #upgrade
BEGIN
PRINT 'Starting';
BEGIN TRY
CREATE TABLE x ( --blah...
);
ALTER TABLE y --blah...
);
CREATE PROCEDURE z AS BEGIN ( --blah...
END
GO --> this is causing trouble!
CREATE FUNCTION a ( --blah...
END TRY
BEGIN CATCH
PRINT 'Error with transaction. Code: ' + ##ERROR + '; Message: ' + ERROR_MESSAGE();
ROLLBACK TRANSACTION #upgrade;
PRINT 'Rollback complete';
RETURN;
END TRY
END
PRINT 'Upgrade successful';
COMMIT TRANSACTION #upgrade;
GO
Note - I know some of the syntax is not perfect - I'm having to re-key the code
It seems as though I can't put Stored Procedures into a transaction block. Is there a reason for this? Is it because of the use of the word GO? If so, how can I put SPs into a transaction block? What are the limitations as to what can go into a transaction block? Or, what would be a better alternative to what I'm trying to achieve?
Thanks
As Thomas Haratyk said in his answer, your issue was the "go". However, you can have as many batches in a transaction as you want. It's the try/catch that doesn't like this. Here's a simple proof-of-concept:
begin tran
go
select 1
go
select 2
go
rollback
begin try
select 1
go
select 2
go
end try
begin catch
select 1
end catch
Remove the GO and create your procedure by using dynamic sql or it will fail.
EXEC ('create procedure z
as
begin
print "hello world"
end')
GO is not a SQL keyword, it is a batch separator. So it cannot be included into a transaction.
Please refer to those topics for further information :
sql error:'CREATE/ALTER PROCEDURE' must be the first statement in a query batch?
Using "GO" within a transaction
http://msdn.microsoft.com/en-us/library/ms188037.aspx

How to enable Transaction for DDL on SQL Server [duplicate]

This question already has answers here:
Closed 11 years ago.
Possible Duplicate:
Is it possible to run multiple DDL statements inside a transaction (within SQL Server)?
If I have following script:
BEGIN TRAN
GO
ALTER TABLE [dbo].[Table1] CHECK CONSTRAINT [FK_1]
GO
ALTER TABLE [dbo].[Users] CHECK CONSTRAINT [FK_2]
GO
COMMIT TRAN
Transcation is not working. It is still on transaction one statement. For example, if statement 1 failed, statement 2 still is done when running the script.
How to enable Transaction for DDL?
You're running the DDL in separate batches so if your first statement raises anything less than a connection-terminating error (hardware problem etc) the second batch will run.
Management studio treats GO as a batch separator and runs each batch separately.
You could use SET XACT_ABORT ON to automatically rollback your transaction in the event of an error. You can also remove the GO statements as ALTER TABLE statements do not need to be run in separate batches.
MagicMike is right, but I implemented an other solution I know to be efficient (even if his solution seems more elegant).
FYI, my solution with two transactions and a clean error management (the ##error feature exists on SQL Server, check the equivalent on your SQL, in Oracle it should be something like "exception when others" instead of "If (##error=0)" ):
begin tran
ALTER TABLE [dbo].[Table1] CHECK CONSTRAINT [FK_1]
IF (##Error=0)
begin
COMMIT TRAN
end
else
begin
rollback tran
END
begin tran
ALTER TABLE [dbo].[Users] CHECK CONSTRAINT [FK_2]
IF (##Error=0)
begin
COMMIT TRAN
end
else
begin
rollback tran
END
You don't need to disable or enable the DDL command
Just do the following
you can use
Begin Try
.......
End Try
Begin Catch
.......
End Try
in your terms of example
you can do this way
begin try
ALTER TABLE [dbo].temp CHECK CONSTRAINT [FK_1]
--GO
ALTER TABLE [dbo].temp CHECK CONSTRAINT [FK_2]
--GO
end try
begin catch
print 'Error in the Try Block'
end catch

TSQL - How to use GO inside of a BEGIN .. END block?

I am generating a script for automatically migrating changes from multiple development databases to staging/production. Basically, it takes a bunch of change-scripts, and merges them into a single script, wrapping each script in a IF whatever BEGIN ... END statement.
However, some of the scripts require a GO statement so that, for instance, the SQL parser knows about a new column after it's created.
ALTER TABLE dbo.EMPLOYEE
ADD COLUMN EMP_IS_ADMIN BIT NOT NULL
GO -- Necessary, or next line will generate "Unknown column: EMP_IS_ADMIN"
UPDATE dbo.EMPLOYEE SET EMP_IS_ADMIN = whatever
However, once I wrap that in an IF block:
IF whatever
BEGIN
ALTER TABLE dbo.EMPLOYEE ADD COLUMN EMP_IS_ADMIN BIT NOT NULL
GO
UPDATE dbo.EMPLOYEE SET EMP_IS_ADMIN = whatever
END
It fails because I am sending a BEGIN with no matching END. However, if I remove the GO it complains again about an unknown column.
Is there any way to create and update the same column within a single IF block?
I had the same problem and finally managed to solve it using SET NOEXEC.
IF not whatever
BEGIN
SET NOEXEC ON;
END
ALTER TABLE dbo.EMPLOYEE ADD COLUMN EMP_IS_ADMIN BIT NOT NULL
GO
UPDATE dbo.EMPLOYEE SET EMP_IS_ADMIN = whatever
SET NOEXEC OFF;
GO is not SQL - it is simply a batch separator used in some MS SQL tools.
If you don't use that, you need to ensure the statements are executed separately - either in different batches or by using dynamic SQL for the population (thanks #gbn):
IF whatever
BEGIN
ALTER TABLE dbo.EMPLOYEE ADD COLUMN EMP_IS_ADMIN BIT NOT NULL;
EXEC ('UPDATE dbo.EMPLOYEE SET EMP_IS_ADMIN = whatever')
END
You could try sp_executesql, splitting the contents between each GO statement into a separate string to be executed, as demonstrated in the example below. Also, there is a #statementNo variable to track which statement is being executed for easy debugging where an exception occurred. The line numbers will be relative to the beginning of the relevant statement number that caused the error.
BEGIN TRAN
DECLARE #statementNo INT
BEGIN TRY
IF 1=1
BEGIN
SET #statementNo = 1
EXEC sp_executesql
N' ALTER TABLE dbo.EMPLOYEE
ADD COLUMN EMP_IS_ADMIN BIT NOT NULL'
SET #statementNo = 2
EXEC sp_executesql
N' UPDATE dbo.EMPLOYEE
SET EMP_IS_ADMIN = 1'
SET #statementNo = 3
EXEC sp_executesql
N' UPDATE dbo.EMPLOYEE
SET EMP_IS_ADMIN = 1x'
END
END TRY
BEGIN CATCH
PRINT 'Error occurred on line ' + cast(ERROR_LINE() as varchar(10))
+ ' of ' + 'statement # ' + cast(#statementNo as varchar(10))
+ ': ' + ERROR_MESSAGE()
-- error occurred, so rollback the transaction
ROLLBACK
END CATCH
-- if we were successful, we should still have a transaction, so commit it
IF ##TRANCOUNT > 0
COMMIT
You can also easily execute multi-line statements, as demonstrated in the example above, by simply wrapping them in single quotes ('). Don't forget to escape any single quotes contained inside the string with a double single-quote ('') when generating the scripts.
You can enclose the statements in BEGIN and END instead of the GO inbetween
IF COL_LENGTH('Employees','EMP_IS_ADMIN') IS NULL --Column does not exist
BEGIN
BEGIN
ALTER TABLE dbo.Employees ADD EMP_IS_ADMIN BIT
END
BEGIN
UPDATE EMPLOYEES SET EMP_IS_ADMIN = 0
END
END
(Tested on Northwind database)
Edit: (Probably tested on SQL2012)
I ultimately got it to work by replacing every instance of GO on its own line with
END
GO
---Automatic replacement of GO keyword, need to recheck IF conditional:
IF whatever
BEGIN
This is greatly preferable to wrapping every group of statements in a string, but is still far from ideal. If anyone finds a better solution, post it and I'll accept it instead.
You may try this solution:
if exists(
SELECT...
)
BEGIN
PRINT 'NOT RUN'
RETURN
END
--if upper code not true
ALTER...
GO
UPDATE...
GO
I have used RAISERROR in the past for this
IF NOT whatever BEGIN
RAISERROR('YOU''RE ALL SET, and sorry for the error!', 20, -1) WITH LOG
END
ALTER TABLE dbo.EMPLOYEE ADD COLUMN EMP_IS_ADMIN BIT NOT NULL
GO
UPDATE dbo.EMPLOYEE SET EMP_IS_ADMIN = whatever
You can incorporate a GOTO and LABEL statements to skip over code, thus leaving the GO keywords intact.