SQL Server Conditional Select Into Issue [duplicate] - sql-server

I have a T-Sql script where part of this script checks to see if a certain column exists in the a table. If so, I want it to execute a routine... if not, I want it to bypass this routine. My code looks like this:
IF COL_LENGTH('Database_Name.dbo.Table_Name', 'Column_Name1') IS NOT NULL
BEGIN
UPDATE Table_Name
SET Column_Name2 = (SELECT Column_Name3 FROM Table_Name2
WHERE Column_Name4 = 'Some Value')
WHERE Column_Name5 IS NULL;
UPDATE Table_Name
SET Column_Name6 = Column_Name1
WHERE Column_Name6 IS NULL;
END
My problem is that even when COL_LENGTH of Column_Name1 is null (meaning it does not exist) I am still getting an error telling me "Invalid column name 'Column_Name1'" from the 2nd UPDATE statement in the IF statement. For some reason this IF condition is still being evaluated even when the condition is FALSE and I don't know why.

SQL Server parses the statement and validates it, ignoring any if conditionals. This is why the following also fails:
IF 1 = 1
BEGIN
CREATE TABLE #foo(id INT);
END
ELSE
BEGIN
CREATE TABLE #foo(id INT);
END
Whether you hit Execute or just Parse, this results in:
Msg 2714, Level 16, State 1
There is already an object named '#foo' in the database.
SQL Server doesn't know or care which branch of a conditional will be entered; it validates all of the statements in a batch anyway. You can do things like (due to deferred name resolution):
IF <something>
BEGIN
SELECT foo FROM dbo.Table_That_Does_Not_Exist;
END
But you can't do:
IF <something>
BEGIN
SELECT column_that_does_not_exist FROM dbo.Table_That_Does;
END
The workaround, typically, is to use dynamic SQL:
IF <something>
BEGIN
DECLARE #sql NVARCHAR(MAX);
SET #sql = N'SELECT column_that_does_not_exist FROM dbo.Table_That_Does;';
EXEC sp_executesql #sql;
END

Make your statement a string. And if column exists, execute it
IF COL_LENGTH('Database_Name.dbo.Table_Name', 'Column_Name1') IS NOT NULL
BEGIN
DECLARE #sql VARCHAR(MAX)
SET #sql = 'UPDATE Table_Name
SET Column_Name2 = (SELECT Column_Name3 FROM Table_Name2
WHERE Column_Name4 = ''Some Value'')
WHERE Column_Name5 IS NULL;
UPDATE Table_Name
SET Column_Name6 = Column_Name1
WHERE Column_Name6 IS NULL;'
EXEC(#sql)
END

Related

Formatting XML with `GO` into a set of sql commands to be executed

I have seen this question Replace value in XML using SQL and again I am thankful, however, I have a XML file with all the trigger creation scripts but I have not been able to execute it because of the GO.
Partial view of my XML file:
<Krishna>IF 'MY_SERVER' <> ##ServerName THROW 50001, 'Wrong Server!!!',1 </Krishna>
<Krishna>GO</Krishna>
<Krishna>use [DB_02]</Krishna>
<Krishna>GO</Krishna>
<Krishna>IF EXISTS (SELECT 'Radhe' FROM sys.triggers t wHERE t.[name] = 'tgr_repl_AuPair_Insert' AND CASE WHEN t.parent_id = 0 THEN t.parent_id ELSE object_id('[sub].[repl_Aupair]') END = t.parent_id )
</Krishna>
<Krishna>EXEC('BEGIN DROP TRIGGER [sub].[tgr_repl_AuPair_Insert] END') </Krishna>
<Krishna>GO</Krishna>
<Krishna></Krishna>
<Krishna>CREATE TRIGGER [sub].[tgr_repl_AuPair_Insert]</Krishna>
<Krishna>ON [sub].[repl_Aupair]</Krishna>
<Krishna>FOR INSERT, UPDATE</Krishna>
when I try to get rid of the GO, replacing it like it is suggested here, I get a different error.
DECLARE #XML3 XML
SELECT #XML3 = (SELECT a.trigger_definition AS Krishna FROM TableBackups.dbo._MMiorelli_20220615_triggerdropping_203144_2 a FOR XML PATH(''))
WHILE #xml3.exist(N'//*[text()="GO"]')=1
BEGIN
SET #xml3.modify(N'replace value of (//*[text()="GO"]/text())[1] with ""');
END
exec sp_execXML #dbname=N'APCore'
,#XML=#xml3
,#DEBUG=1
,#PrintOnly=0
this is the way I am executing the commands that are within my XML:
declare #i int = 1
select #sql1 = ''
SELECT #SQL2 = 'Radhe'
WHILE #sql2 is not null
begin
SELECT #sql2 = #XML.value('(/Krishna/text())[sql:variable("#i") cast as xs:int?][1]', 'varchar(max)')
if #DEBUG=1
PRINT COALESCE(#sql2,'#SQL2 WAS NULL' + ' -- #I IS ' + CAST(#I AS VARCHAR(5)))
if #sql2 is not null
begin
SET #sql1 = CAST (#sql1 + #sql2 + #vbCrLf AS NVARCHAR(MAX))
IF #PrintOnly = 1
BEGIN
EXEC sp_DisplayNVarchar #textToDisplay = #SQL2, #debug =0
END
ELSE
BEGIN
EXEC (#SQL2)
END
end
SELECT #i = #i + 1
if #i >= #Limit
SET #sql2 = null
end
BASICALLY:
each line of the XML is a command
SELECT #sql2 = #XML.value('(/Krishna/text())[sql:variable("#i") cast as xs:int?][1]', 'varchar(max)')
My question is:
How could I replace the every GO inside my long script into a new line in my XML?
Every time I meet a GO, that GO is removed but from that place on is a new line in my XML.
this is an example of code and XML that works:
here is the code:
---------------------------------------
----check the data
---------------------------------------
GO
SELECT [##TRANCOUNT]=##TRANCOUNT
TRUNCATE TABLE #the_inserts
TRUNCATE TABLE #VICASA477
INSERT INTO #the_inserts(RADHE1)
SELECT RADHE1='use apcore;' + CHAR(10)+CHAR(13) + 'exec sp_count ' + '''' + E.AP_NAME2 + ''''
FROM #E E
DECLARE #XML3 XML
SELECT #XML3 = (SELECT #the_inserts.radhe1 AS Krishna FROM #the_inserts FOR XML PATH(''))
INSERT INTO #VICASA477
EXEC sp_execXML #dbname=N'APCore'
,#XML=#xml3
,#DEBUG=0
,#PrintOnly=0
select #XML3
SELECT * FROM #vicasa477
GO
Here is the XML: (partial view but you get the idea)
<Krishna>use apcore;
exec sp_count '[sub].[matchAgreementEmailSent]'</Krishna>
<Krishna>use apcore;
exec sp_count '[sub].[receivedLog]'</Krishna>
<Krishna>use apcore;
exec sp_count '[sub].[repl_Airline]'</Krishna>
<Krishna>use apcore;
exec sp_count '[sub].[repl_Airport]'</Krishna>
<Krishna>use apcore;
exec sp_count '[sub].[repl_ArrivalCalendar]'</Krishna>
<Krishna>use apcore;
exec sp_count '[sub].[repl_Aupair]'</Krishna>
<Krishna>
and here the results: (partial view but you get the idea)
EDIT: As mentioned by #DavidBrowne, this answer doesn't work if changing the current database with USE is necessary.
You can run this script using a cursor, which executes each batch separately.
To split the batches we need to use XQuery. This is made significantly more complicated by the fact that the batches are separated by the same Krishna node again, rather than each being contained in a separate child node.
DECLARE #sql nvarchar(max), #crsr CURSOR;
SET #crsr = CURSOR FAST_FORWARD READ_ONLY FOR
SELECT
x.krsh.query('
let $go := .
let $prev := /Krishna[. << $go][text() = "GO"][1]
return /Krishna[text() != "GO"][. << $go and not(. << $prev)]/text()
').value('text()[1]','nvarchar(max)') line
FROM #xml.nodes('/Krishna[text() = "GO"]') x(krsh);
OPEN #crsr;
GOTO Ftch;
WHILE ##FETCH_STATUS = 0
BEGIN
EXEC sp_executesql #sql;
ftch:
FETCH NEXT FROM #crsr
INTO #sql;
END;
db<>fiddle
The logic runs like this:
Use .nodes to grab all Krishna nodes which contain GO.
For each of those, we run the following XQuery expression:
Assign the current node to $go
Find the node previous to this which contains GO.
Find all Krishna nodes which do not contain GO, and...
... are located before $go...
... and after $prev (if any).
Return the inner text()
Concatenate all the text together using .query and .value
Note: This assumes that the final node is always GO.
There's no good way to run that script from TSQL. You need to run each batch separately, which can be done by accumulating the lines for each batch, and executing it when you see GO. Stripping GO is a hack which won't work for DDL scripts as many DDL statements must begin a batch, or be the only statement in the batch.
The thing that won't work is the scripts like
use somedb
go
create table foo(id int)
In TSQL if you parse and exec this as:
exec ('use somedb')
exec ('create table foo(id int)')
The database context will be switched inside the first batch, but revert to the original database context at the end of the first dynamic SQL invocation. You simply can't change the database context permanently for the session in dynamic SQL. So your pre-processor must concatenate the database context switch with the subsequent batches. eg
exec ('use somedb
create table foo(id int)')
Which might work if your script is generated exactly the same and you reliably identify the use database statements. But it's complicated and basically not worth doing. Instead use an external program like sqlcmd or powershell's `invoke-sqlcmd', or even a client program like .NET that can issue top-level batches that permanently change the database context.
And you may find other session-level settings that will have the same problem.

TSQLT Database Unit test

I am trying to hands on with TSQLT unittest framework for first time and have come across a problem. I have a stored proc like this which returns some numbers
Use Mydatabase
Declare #parameter1, #parameter2,#paramter3
BEGIN
SELECT #parameter1 = dbo.[function](#paramter2,parameter3)
..
..
-- Now some Dynamic SQL joins on two tables
BEGIN
SET #SQL = 'SELECT COLUMN1, COLUMN2 FROM FROM TABLE1 INNER JOIN TABLE2 ON TABLE1.COLUMN1 =
TABLE2.COLUMN2 WHERE TABLE2.COLUMN3 = + CAST(#parameter1 as varchar(10))
EXECUTE sp_executesql #SQL, Output parameter
return
The problem is the table1 and table2 are updated daily and I can't assert to values that are changing so I came across fake tables and spy procedures,
Is there a way to utilize the spyprocedures to use the faketables rather than original tables.?
Since my actual db and Unit test db are different but with in same connection, how can I reference the actual to the test one as I am getting error as
Cannot use SpyProcedure on [WD0000\server].[database].[dbo].[usp.mystoredproc] because the procedure does not exist
TSQLT code
EXEC tSQLt.NewTestClass 'SegmentSizeTest'
GO
CREATE PROCEDURE SegmentSizeTest.[test that checks the segment size]
AS
BEGIN
DECLARE #return_value int,#TotalCount int,#OneCount int,#TwoCount int
declare #Criteria Varchar(MAX)
declare #rampUpFactor int
declare #expected int
declare #actual int
SET #Criteria = 'My Criteria'
SET #GeoflexCriteria = NULL
--#TotalCount = #TotalCount OUTPUT
--#OneCount = #OneCount OUTPUT
-- #TwoCount = #TwoCount OUTPUT
SET #rampUpFactor = 1
set #expected = 160486
------Fake Table
EXEC tSQLt.FakeTable 'UnitTest.Household';
EXEC tSQLT.FakeTable 'UnitTest.Customer';
--
..Insert to UnitTest.Household and UnitTest.Customer tables
------Execution
EXEC #return_value = [VAReportingDB].[dbo].[GetSegmentSize]
#Criteria = #Criteria,
#TotalCount = #TotalCount OUTPUT,
#OneCount = #OneCount OUTPUT,
#TwoCount = #TwoCount OUTPUT
SELECT #TotalCount as N'#TotalCount',
#OneCount as N'#OneCount',
#TwoCount as N'#TwoCount'
------Assertion
EXEC tSQLt.AssertEquals #expected, #TotalCount;
END;
GO
FakeTable replaces the original table wit a test double. Therefore any code that is written to access the original table, dynamically or otherwise, will “see” the faked table during a test.
Independently, you should pass in #parameter1 to sp_executesql instead of concatenating it into your Sql statement. At least if your original code is similar to what you posted here.

Strange "There is already an object in the database." error

I have the following SQL code:
IF OBJECT_ID( 'tempdb..#PropList') IS NOT NULL
DROP TABLE #PropList
DECLARE #Split CHAR(1), #propList NVARCHAR(MAX), #PropListXml XML
SET #Split = ','
SET #propList = 'NAME,DESCRIPTION'
-- SET #propList = ''
IF (#propList IS NOT NULL AND #propList != '')
BEGIN
SET #PropListXml = CONVERT(XML,'<root><s>' + REPLACE(#propList, #Split, '</s><s>') + '</s></root>')
SELECT SystemName = T.c.VALUE('.','nvarchar(36)')
INTO #PropList
FROM #PropListXml.nodes('/root/s') T(c)
END
ELSE
BEGIN
SELECT SystemName
INTO #PropList -- Stops here
FROM tblProperty
END
SELECT * FROM #PropList
Regardless of the value of #propList, this code always stops at the indicated line with this error:
There is already an object named '#PropList' in the database.
My expectation was that only one of the two IF blocks is executed, therefore there should be only one attempt to create the table with the SELECT... INTO statement. Why is this failing?
As per comment, you'll need to explicitly define your #temp table before the IF statement, then change your
SELECT ... INTO #temp
to be
INSERT INTO #temp SELECT ...
This is because when SQL Server validates the query it ignores any control flow statements. For more detail on this see the following SO question:
T-Sql appears to be evaluating "If" statement even when the condition is not true

T-Sql appears to be evaluating "If" statement even when the condition is not true

I have a T-Sql script where part of this script checks to see if a certain column exists in the a table. If so, I want it to execute a routine... if not, I want it to bypass this routine. My code looks like this:
IF COL_LENGTH('Database_Name.dbo.Table_Name', 'Column_Name1') IS NOT NULL
BEGIN
UPDATE Table_Name
SET Column_Name2 = (SELECT Column_Name3 FROM Table_Name2
WHERE Column_Name4 = 'Some Value')
WHERE Column_Name5 IS NULL;
UPDATE Table_Name
SET Column_Name6 = Column_Name1
WHERE Column_Name6 IS NULL;
END
My problem is that even when COL_LENGTH of Column_Name1 is null (meaning it does not exist) I am still getting an error telling me "Invalid column name 'Column_Name1'" from the 2nd UPDATE statement in the IF statement. For some reason this IF condition is still being evaluated even when the condition is FALSE and I don't know why.
SQL Server parses the statement and validates it, ignoring any if conditionals. This is why the following also fails:
IF 1 = 1
BEGIN
CREATE TABLE #foo(id INT);
END
ELSE
BEGIN
CREATE TABLE #foo(id INT);
END
Whether you hit Execute or just Parse, this results in:
Msg 2714, Level 16, State 1
There is already an object named '#foo' in the database.
SQL Server doesn't know or care which branch of a conditional will be entered; it validates all of the statements in a batch anyway. You can do things like (due to deferred name resolution):
IF <something>
BEGIN
SELECT foo FROM dbo.Table_That_Does_Not_Exist;
END
But you can't do:
IF <something>
BEGIN
SELECT column_that_does_not_exist FROM dbo.Table_That_Does;
END
The workaround, typically, is to use dynamic SQL:
IF <something>
BEGIN
DECLARE #sql NVARCHAR(MAX);
SET #sql = N'SELECT column_that_does_not_exist FROM dbo.Table_That_Does;';
EXEC sp_executesql #sql;
END
Make your statement a string. And if column exists, execute it
IF COL_LENGTH('Database_Name.dbo.Table_Name', 'Column_Name1') IS NOT NULL
BEGIN
DECLARE #sql VARCHAR(MAX)
SET #sql = 'UPDATE Table_Name
SET Column_Name2 = (SELECT Column_Name3 FROM Table_Name2
WHERE Column_Name4 = ''Some Value'')
WHERE Column_Name5 IS NULL;
UPDATE Table_Name
SET Column_Name6 = Column_Name1
WHERE Column_Name6 IS NULL;'
EXEC(#sql)
END

Insert/Update/Delete with function in SQL Server

Can we perform Insert/Update/Delete statement with SQL Server Functions. I have tried with but SQL Server error is occured.
Error:
Invalid use of side-effecting or time-dependent operator in 'DELETE' within a function.
AnyBody have any Idea why we can not use Insert/Update/Delete statements with SQL Server functions.
Waiting for your good idea's
No, you cannot.
From SQL Server Books Online:
User-defined functions cannot be used
to perform actions that modify the
database state.
Ref.
Yes, you can!))
Disclaimer: This is not a solution, it is more of a hack to test out something. User-defined functions cannot be used to perform actions that modify the database state.
I found one way to make INSERT, UPDATE or DELETE in function using xp_cmdshell.
So you need just to replace the code inside #sql variable.
CREATE FUNCTION [dbo].[_tmp_func](#orderID NVARCHAR(50))
RETURNS INT
AS
BEGIN
DECLARE #sql varchar(4000), #cmd varchar(4000)
SELECT #sql = 'INSERT INTO _ord (ord_Code) VALUES (''' + #orderID + ''') '
SELECT #cmd = 'sqlcmd -S ' + ##servername +
' -d ' + db_name() + ' -Q "' + #sql + '"'
EXEC master..xp_cmdshell #cmd, 'no_output'
RETURN 1
END
Functions in SQL Server, as in mathematics, can not be used to modify the database. They are intended to be read only and can help developer to implement command-query separation. In other words, asking a question should not change the answer. When your program needs to modify the database use a stored procedure instead.
You can't update tables from a function like you would a stored procedure, but you CAN update table variables.
So for example, you can't do this in your function:
create table MyTable
(
ID int,
column1 varchar(100)
)
update [MyTable]
set column1='My value'
but you can do:
declare #myTable table
(
ID int,
column1 varchar(100)
)
Update #myTable
set column1='My value'
Yes, you can.
However, it requires SQL CLR with EXTERNAL_ACCESS or UNSAFE permission and specifying a connection string. This is obviously not recommended.
For example, using Eval SQL.NET (a SQL CLR which allow to add C# syntax in SQL)
CREATE FUNCTION [dbo].[fn_modify_table_state]
(
#conn VARCHAR(8000) ,
#sql VARCHAR(8000)
)
RETURNS INT
AS
BEGIN
RETURN SQLNET::New('
using(var connection = new SqlConnection(conn))
{
connection.Open();
using(var command = new SqlCommand(sql, connection))
{
return command.ExecuteNonQuery();
}
}
').ValueString('conn', #conn).ValueString('sql', #sql).EvalReadAccessInt()
END
GO
DECLARE #conn VARCHAR(8000) = 'Data Source=XPS8700;Initial Catalog=SqlServerEval_Debug;Integrated Security=True'
DECLARE #sql VARCHAR(8000) = 'UPDATE [Table_1] SET Value = -1 WHERE Name = ''zzz'''
DECLARE #rowAffecteds INT = dbo.fn_modify_table_state(#conn, #sql)
Documentation: Modify table state within a SQL Function
Disclaimer: I'm the owner of the project Eval SQL.NET
You can have a table variable as a return type and then update or insert on a table based on that output.
In other words, you can set the variable output as the original table, make the modifications and then do an insert to the original table from function output.
It is a little hack but if you insert the #output_table from the original table and then say for example:
Insert into my_table
select * from my_function
then you can achieve the result.
We can't say that it is possible of not their is some other way exist to perform update operation in user-defined Function. Directly DML is not possible in UDF it is for sure.
Below Query is working perfectly:
create table testTbl
(
id int identity(1,1) Not null,
name nvarchar(100)
)
GO
insert into testTbl values('ajay'),('amit'),('akhil')
Go
create function tblValued()
returns Table
as
return (select * from testTbl where id = 1)
Go
update tblValued() set name ='ajay sharma' where id = 1
Go
select * from testTbl
Go
"Functions have only READ-ONLY Database Access"
If DML operations would be allowed in functions then function would be prety similar to stored Procedure.
No, you can not do Insert/Update/Delete.
Functions only work with select statements. And it has only READ-ONLY Database Access.
In addition:
Functions compile every time.
Functions must return a value or result.
Functions only work with input parameters.
Try and catch statements are not used in functions.
CREATE FUNCTION dbo.UdfGetProductsScrapStatus
(
#ScrapComLevel INT
)
RETURNS #ResultTable TABLE
(
ProductName VARCHAR(50), ScrapQty FLOAT, ScrapReasonDef VARCHAR(100), ScrapStatus VARCHAR(50)
) AS BEGIN
INSERT INTO #ResultTable
SELECT PR.Name, SUM([ScrappedQty]), SC.Name, NULL
FROM [Production].[WorkOrder] AS WO
INNER JOIN
Production.Product AS PR
ON Pr.ProductID = WO.ProductID
INNER JOIN Production.ScrapReason AS SC
ON SC.ScrapReasonID = WO.ScrapReasonID
WHERE WO.ScrapReasonID IS NOT NULL
GROUP BY PR.Name, SC.Name
UPDATE #ResultTable
SET ScrapStatus =
CASE WHEN ScrapQty > #ScrapComLevel THEN 'Critical'
ELSE 'Normal'
END
RETURN
END
Functions are not meant to be used that way, if you wish to perform data change you can just create a Stored Proc for that.
if you need to run the delete/insert/update you could also run dynamic statements. i.e.:
declare
#v_dynDelete NVARCHAR(500);
SET #v_dynDelete = 'DELETE some_table;';
EXEC #v_dynDelete
Just another alternative using sp_executesql (tested only in SQL 2016).
As previous posts noticed, atomicity must be handled elsewhere.
CREATE FUNCTION [dbo].[fn_get_service_version_checksum2]
(
#ServiceId INT
)
RETURNS INT
AS
BEGIN
DECLARE #Checksum INT;
SELECT #Checksum = dbo.fn_get_service_version(#ServiceId);
DECLARE #LatestVersion INT = (SELECT MAX(ServiceVersion) FROM [ServiceVersion] WHERE ServiceId = #ServiceId);
-- Check whether the current version already exists and that it's the latest version.
IF EXISTS(SELECT TOP 1 1 FROM [ServiceVersion] WHERE ServiceId = #ServiceId AND [Checksum] = #Checksum AND ServiceVersion = #LatestVersion)
RETURN #LatestVersion;
-- Insert the new version to the table.
EXEC sp_executesql N'
INSERT INTO [ServiceVersion] (ServiceId, ServiceVersion, [Checksum], [Timestamp])
VALUES (#ServiceId, #LatestVersion + 1, #Checksum, GETUTCDATE());',
N'#ServiceId INT = NULL, #LatestVersion INT = NULL, #Checksum INT = NULL',
#ServiceId = #ServiceId,
#LatestVersion = #LatestVersion,
#Checksum = #Checksum
;
RETURN #LatestVersion + 1;
END;

Resources