Why is test for existing column failing in SQL? - sql

I have this SQL change script that runs as part of my nant orchestrated DB creation or update:
SET XACT_ABORT ON
BEGIN TRANSACTION
PRINT 'Change MyColumn column to MyNewColumn column in MyTable table'
IF EXISTS (SELECT *
FROM sys.columns
WHERE Name = 'MyColumn' AND Object_id = OBJECT_ID('[dbo].[MyTable]'))
BEGIN
PRINT '-> Exists, change it'
/* NOTE THE NEXT LINE */
SET #Value = (SELECT MyColumn FROM [dbo].[MyTable])
ALTER TABLE [dbo].[MyTable]
DROP CONSTRAINT DF_MyTable_MyColumn
ALTER TABLE [dbo].[MyTable]
DROP COLUMN MyColumn
ALTER TABLE [dbo].[MyTable]
ADD MyNewColumn nvarchar(20) NULL
ALTER TABLE [dbo].[MyTable]
ADD CONSTRAINT DF_MyTable_MyNewColumn DEFAULT ('') FOR MyNewColumn
PRINT '-> Add values back into table'
SET #Dynamic_Sql = 'UPDATE [dbo].[MyTable] SET MyNewColumn = ''' + #Value + ''''
EXEC(#Dynamic_Sql)
PRINT '-> Alter to NOT NULL'
ALTER TABLE [dbo].[MyTable]
ALTER COLUMN MyNewColumn nvarchar(20) NOT NULL
END
ELSE
BEGIN
PRINT '-> Does not exist, skip it'
END
I have already ran this update script before and made the changes to the DB (so MyColumn no longer exists). But now I have a new script that comes after this one, but my "build" fails on this line of this script with:
Msg 207, Level 16, State 1, Line 15
Invalid column name 'MyColumn'
where Line 15 is the FROM sys.columns line. But this is actually complaining about the line I have within the IF statement, where I have put in the NOTE comment. Why would this be the behaviour? Of course the column name will be invalid if it no longer exists.

Do you include the GO batch separator after you create all of your columns? If not, the columns won't be created by the time your first query runs, because the query parser parses it all at the same time -- at parse time, the column really doesn't exist.
By adding the GO batch separator, you force it to parse the portions of the query which use your newly created columns after the columns are actually created.

The problem (as Dave Markle alludes to, so feel free to accept his answer) is that SQL Server parses the entire section of the script. It sees that you're referring to MyColumn and that column doesn't exist, so it gives you the error. It doesn't matter that it's within an IF statement.
You can test it easily with this script:
CREATE TABLE dbo.Test (my_id int)
GO
IF (1=0)
SELECT blah FROM Test
If I can find a way to defer the parsing, I'll update this answer, but other than using dynamic SQL I don't think that you can.
EDIT:
Here's one possible solution. I didn't go through Martin's link yet, but that may be another.
CREATE FUNCTION dbo.Get_my_id ()
RETURNS INT
AS
BEGIN
DECLARE #my_id INT
SELECT #my_id = blah FROM dbo.Test
RETURN #my_id
END
GO
CREATE TABLE dbo.Test (my_id INT)
GO
DECLARE #my_id INT
IF (1=0)
SELECT #my_id = dbo.Get_my_id()
GO
BTW, if your table has more than one row in it, you realize that the value of your variable cannot be predicted, correct?

Related

Update a Column that has been added in the same script

I have a deployment script that needs to add a column, and then populate it with some data. I check if the column exists - if it doesn't I add it, and attempt to change the value.
IF NOT EXISTS (SELECT * FROM sys.columns WHERE object_id = OBJECT_ID(N'[dbo].[MyTable]') AND name = 'MyColumn')
BEGIN
ALTER TABLE [dbo].MyTable
ADD MyColumn INT NULL
...
UPDATE MyTable SET MyColumn = MyValue
END
However, the script fails (on pre-compile?) as it says that MyColumn doesn't exist.
The only way I can think of fixing this, is to change the UPDATE statement to dynamic SQL, and EXEC it that way.
Is there a better way to do this?
This is tricky because of the compilation. One solution is dynamic SQL:
exec sp_executesql 'UPDATE MyTable SET MyColumn = MyValue';
If you take this path, then you should pass in the value as a parameter.
you should put your update statement out side of the IF NOT EXISTS condition.
Reason : If you have column already present in your table, then it will exit the condition and execute the update statement, else it will add the column and then perform the update. have a look at below code:
IF NOT EXISTS (SELECT * FROM sys.columns WHERE object_id =
OBJECT_ID(N'[dbo].[MyTable]') AND name = 'MyColumn')
BEGIN
ALTER TABLE [dbo].MyTable
ADD MyColumn INT NULL
END
GO
UPDATE MyTable SET MyColumn = 1
GO

SQL server convert int field to bit

I have a big database and I should to normalize it. One of the table contains field with type "integer" but it contains only 1 and 0 values. So it is good reason to convert this field to bit type. But when I try to save changes in SQL Server Management Studio it tells me that I can't do it. Also I have many field with values like nvarchar that should be converted to int or float that should be converted to int too.
Moreover I should create migration scripts for all changes so I can update real database without loosing data. Maybe somebody knows useful utility for this?
EDIT: It tells me that I can't update unable without drop it. And I want to update table without losing any data.
SQL version 2014
---Create one Temp. Column
Alter Table [dbo].[Demo2]
Add tempId int
GO
--Copy Data in temp. Coulmn
Update [dbo].[Demo2] set tempId=Id
--Drop column which you want to modify
Alter Table [dbo].[Demo2]
Drop Column Id
Go
--Create again that column with bit type
Alter Table [dbo].[Demo2]
Add Id bit
GO
--copy date back
Update [dbo].[Demo2] set Id=tempId
--drop temp column
Alter Table [dbo].[Demo2]
Drop Column tempId
Go
Here's how to add a new column to a table, set that column to the old column and then remove the old column
CREATE TABLE #test
(inttest int
)
Insert [#test]
( [inttest] )
Values ( 0
)
Insert [#test]
( [inttest] )
Values ( 1
)
Alter Table [#test] Add bittest bit
Update [#test] Set bittest=inttest
Alter Table [#test] Drop Column [inttest]
SELECT * FROM [#test] [T]
To generate migration script you don't need a special utility, SSMS does it pretty well.
Right-click the table in SSMS object explorer. Choose Design item in the context menu. Change the type of the column. In the main menu Table Designer choose item Generate Change Script. Save the generated script to a file, review it and make sure you understand each line in it before you run it on a production system. Adjust the script if needed.
On the other hand, to change the column type from int to bit you can use the ALTER TABLE statement:
ALTER TABLE dbo.TableName
ALTER COLUMN ColumnName bit NOT NULL
Before running this you should check that actual int values are indeed only 0 and 1.
After reading of all comments and posts I found solution in building procedure which will convert passed table and column in required. So I wrote this function
IF EXISTS (select * from dbo.sysobjects where id = object_id(N'IntToBit') and OBJECTPROPERTY(id, N'IsProcedure') = 1)
DROP PROCEDURE IntToBit
GO
IF OBJECT_ID('convertion_table', 'U') IS NOT NULL
DROP TABLE dbo.convertion_table;
go
CREATE TABLE dbo.convertion_table
(
bitTypeColumn bit NOT NULL DEFAULT 0,
intTypeColumnt integer ,
)
go
CREATE procedure IntToBit
#table nvarchar(150),
#column nvarchar(150)
AS
begin
DECLARE #sql nvarchar(4000)
SELECT #sql ='
--copy data to temp table
INSERT INTO convertion_table (bitTypeColumn)
SELECT '+#column +'
FROM ' +#table+'
--Drop column which you want to modify
Alter Table ' +#table+'
Drop Column '+#column +'
--Create again that column with bit type
Alter Table ' +#table+'
Add '+#column +' bit NOT NULL DEFAULT(0)
--copy date back
INSERT INTO '+#table+'('+#column+')
SELECT bitTypeColumn
FROM convertion_table
--cleare temp table
--DELETE bitTypeColumn FROM convertion_table
'
exec sp_executesql #sql
end
GO
and then call it passing field and table name :
exec dbo.IntToBit #table = 'tbl_SystemUsers', #column='intUseLogin';
Special thanks to Chris K and Hitesh Thakor
Simply Use TSql script to modify the table rather than using the designer
ALTER TABLE YourTableNameHere
ALTER COLUMN YourColumnNameHere INT
If you are using sql Server then you might wanna generate script for the table before altering the table so that you dont loose any data ..and you can simply retrieve everything using the script

Procedure which modify existing trigger

I want to create procedure which modify existing trigger. Trigger is responsible for blocking rows from beeing updated with specific ID. I tried something like that:
CREATE PROCEDURE Change_trigger
#List_of_ids varchar(8000)
AS
ALTER TRIGGER blocks
ON ttt
INSTEAD OF update
AS
BEGIN
If (SELECT Id_ttt FROM inserted) IN (#List_of_ids)
BEGIN
raiserror('You cannot modify this record.', 12, 1)
RETURN
END
UPDATE ttt
SET
field1 = INSERTED.field1
FROM INSERTED
WHERE INSERTED.Id_ttt = ttt.Id_ttt
END
Parameter #List_of_ids would be like this: 2,3,4,5,9,52. But when I try to create this procedure I got error:
Msg 156, Level 15, State 1, Procedure Change_trigger, Line 4
Incorrect syntax near the keyword 'TRIGGER'.
The trigger is created.
This is the trigger I'd write, once.
ALTER TRIGGER blocks
ON ttt
INSTEAD OF update
AS
BEGIN
SET NOCOUNT ON
UPDATE t
SET
field1 = i.field1
FROM INSERTED i
inner join
ttt t
on i.Id_ttt = t.Id_ttt
left join
ttt_blocked on tb
on
i.Id_ttt = tb.Id_ttt
WHERE
tb.Id_ttt is null
END
Note that this trigger no longer throws an error for blocked updates but it does allow for a mixed update (some rows blocked, some rows not) to occur. There's no clean way to raise an error whilst still partially applying an update in a trigger.
Then I'd have a table (referenced above):
CREATE TABLE ttt_blocked (
Id_ttt int not null,
constraint PK_ttt_blocked PRIMARY KEY (Id_ttt)
)
And then, if necessary, I'd create a procedure to maintain this table rather than continually changing the database schema:
CREATE PROCEDURE Change_blocking
#BlockedIDs xml
AS
--Better option would be table-valued parameters
--but I've chosen to do XML today
--We expect the XML to be of the form
--<blocks>
-- <id>10</id>
-- <id>15</id>
--</blocks>
MERGE INTO ttt_blocked t
USING (select x.id.value('text()[1]','int')
from #BlockedIDs.nodes('/blocks/id') x(id)) s(Id_ttt)
ON
t.Id_ttt = s.Id_ttt
WHEN NOT MATCHED THEN INSERT (Id_ttt) VALUES (s.Id_ttt)
WHEN NOT MATCHED BY SOURCE THEN DELETE;
As I also allude to above, I'd generally recommend Table-Valued Parameters rather than XML (and either of them ahead of varchar since they're designed to hold multiple values) but it would have added even more code to this answer.
Try this..
CREATE PROCEDURE Change_trigger
#List_of_ids varchar(4000)
AS
begin
declare #sql varchar(8000)
set #sql ='
ALTER TRIGGER blocks
ON ttt
INSTEAD OF update
AS
BEGIN
if exists (SELECT Id_ttt FROM inserted where Id_ttt IN ('+#List_of_ids+'))
BEGIN
raiserror(''You cannot modify this record.'', 12, 1)
RETURN
END
UPDATE ttt
SET
field1 = INSERTED.field1
FROM INSERTED
WHERE INSERTED.Id_ttt = ttt.Id_ttt
END' ;
exec (#sql);
END

Invalid column name on sql server update after column create

Does anyone see what's wrong with this code for SQL Server?
IF NOT EXISTS(SELECT *
FROM sys.columns
WHERE Name = 'OPT_LOCK'
AND object_ID = Object_id('REP_DSGN_SEC_GRP_LNK'))
BEGIN
ALTER TABLE REP_DSGN_SEC_GRP_LNK
ADD OPT_LOCK NUMERIC(10, 0)
UPDATE REP_DSGN_SEC_GRP_LNK
SET OPT_LOCK = 0
ALTER TABLE REP_DSGN_SEC_GRP_LNK
ALTER COLUMN OPT_LOCK NUMERIC(10, 0) NOT NULL
END;
When I run this, I get:
Msg 207, Level 16, State 1, Line 3
Invalid column name 'OPT_LOCK'.
on the update command.
Thanks.
In this case you can avoid the problem by adding the column as NOT NULL and setting the values for existing rows in one statement as per my answer here.
More generally the problem is a parse/compile issue. SQL Server tries to compile all statements in the batch before executing any of the statements.
When a statement references a table that doesn't exist at all the statement is subject to deferred compilation. When the table already exists it throws an error if you reference a non existing column. The best way round this is to do the DDL in a different batch from the DML.
If a statement both references a non existing column in an existing table and a non existent table the error may or may not be thrown before compilation is deferred.
You can either submit it in separate batches (e.g. by using the batch separator GO in the client tools) or perform it in a child scope that is compiled separately by using EXEC or EXEC sp_executesql.
The first approach would require you to refactor your code as an IF ... cannot span batches.
IF NOT EXISTS(SELECT *
FROM sys.columns
WHERE Name = 'OPT_LOCK'
AND object_ID = Object_id('REP_DSGN_SEC_GRP_LNK'))
BEGIN
ALTER TABLE REP_DSGN_SEC_GRP_LNK
ADD OPT_LOCK NUMERIC(10, 0)
EXEC('UPDATE REP_DSGN_SEC_GRP_LNK SET OPT_LOCK = 0');
ALTER TABLE REP_DSGN_SEC_GRP_LNK
ALTER COLUMN OPT_LOCK NUMERIC(10, 0) NOT NULL
END;
The root cause of the error is the newly added column name is not reflected in the sys.syscolumns and sys.columns table until you restart SQL Server Management Studio.
For your information,you can replace the IF NOT EXISTS with the COL_LENGTH function. It takes two parameters,
Table Name and
Column you are searching for
If the Column is found then it returns the range of the datatype of the column Ex: Int (4 bytes), when not found then it returns a NULL.
So, you could use this as follows and also combine 3 Statements into one.
IF (SELECT COL_LENGTH('REP_DSGN_SEC_GRP_LNK','OPT_LOCK')) IS NULL
BEGIN
ALTER TABLE REP_DSGN_SEC_GRP_LNK
ADD OPT_LOCK NUMERIC(10, 0) NOT NULL DEFAULT 0
END;
Makes it simpler.

SQL Column Not Found: Added earlier in program

I'm using SQL Server 2008. I have a stored procedure with code that looks like this:
if not exists (select column_name from INFORMATION_SCHEMA.columns
where table_name = 'sample_table' and column_name = 'sample_column')
BEGIN
ALTER TABLE Sample_Table
ADD Sample_Column NVARCHAR(50)
END
Update dbo.Sample_Table
SET Sample_Column = '1'
When I execute, I get a "Column Not Found" error because the column doesn't originally exist in Sample_Table-it's added in the procedure. What's the correct way to get around this?
My workaround (below) is to wrap the update statement in an EXEC statement, so that it is forced to create the code and execute after the ALTER TABLE step. But is there a better method?
EXEC ('
Update dbo.Sample_Table
SET Sample_Column = ''1'' ')
If you do not really like your workaround, the only other option seems to be separating your DDL logic from the DML one, i.e. one SP will check/create the column (maybe other columns too, as necessary), another SP sets the value(s).
On the other hand, it looks like you are using your UPDATE statement merely as a means of providing a default value for the newly created column. If that is the case, you might consider an entirely different solution: creating a DEFAULT constraint (no need for the UPDATE statement). Here:
if not exists (select column_name from INFORMATION_SCHEMA.columns
where table_name = 'sample_table' and column_name = 'sample_column')
BEGIN
ALTER TABLE Sample_Table
ADD Sample_Column NVARCHAR(50) NOT NULL
CONSTRAINT DF_SampleTable_SampleColumn DEFAULT ('1');
ALTER TABLE Sample_Table
ALTER COLUMN Sample_Column NVARCHAR(50) NULL;
END
The second ALTER TABLE command is there only to drop the NOT NULL restriction, as it seems like you didn't mean your column to have it. But if you are fine with NOT NULL, then just scrap the second ALTER TABLE.