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.
Related
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.
What is the best approach for stored procedure to toggle between user defined column names or default column name
Here is what I have done so far. This is fine for small query, is there a better way of doing this for larger query.
-- Drop stored procedure if it already exists
IF EXISTS(SELECT * FROM sys.procedures
WHERE schema_id = schema_id('dbo')
AND name = N'sp_test')
DROP PROCEDURE dbo.sp_test
GO
CREATE PROCEDURE [dbo].[sp_test]
-- /* Declare parameters */
#columnName BIT =0
AS
BEGIN
-- SELECT statement to fetch record
IF(#columnName =1)
(
SELECT
TOP 100
IM.INC_REF,
IM.ID
FROM
dbo.TEST AS IM
)
ELSE
(
SELECT
TOP 100
IM.INC_REF AS REF,
IM.ID AS ID
FROM
dbo.TEST AS IM
)
END
GO
-- ============================================
-- Execute stored procedure
-- ============================================
DECLARE #columnName AS BIT
SET #columnName =0
EXEC [dbo].[sp_test] #columnName
Thanks in advance
When we need different results from a stored procedure based on a parameter, we'll call one of two "sub" stored procedures. In your case, that would be:
CREATE PROCEDURE [dbo].[sp_test]
-- /* Declare parameters */
#columnName BIT = 0
AS
BEGIN
IF ( #columnName = 1 )
EXEC dbo.[sp_test1]
ELSE
EXEC dbo.[sp_test2]
END
GO
CREATE PROCEDURE dbo.[sp_test1]
AS
BEGIN
SELECT TOP 100
IM.INC_REF ,
IM.ID
FROM dbo.TEST AS IM
END
GO
CREATE PROCEDURE dbo.[sp_test2]
AS
BEGIN
SELECT TOP 100
IM.INC_REF AS REF ,
IM.ID AS ID
FROM dbo.TEST AS IM
END
GO
I've found this gets around the issue of badly cached plans.
SQL Server allows me to insert the returned result set of a stored procedure as:
DECLARE #T TABLE (
ID int,
Name varchar(255),
Amount money)
INSERT INTO #T
exec dbo.pVendorBalance
This works as long as the stored procedure only returns 1 result set.
Is there a way to make this work if the stored procedure returns several result sets?
E.g.
DECLARE #T1 (...)
DECLARE #T2 (...)
INSERT INTO #T1 THEN INTO #T2
exec dbo.pVendorBalance
One workaround to this problem is using OUTPUT parameters (JSON/XML) instead of resultsets.
CREATE TABLE tab1(ID INT, Name NVARCHAR(10), Amount MONEY);
INSERT INTO tab1(ID, Name, Amount)
VALUES (1, 'Alexander', 10),(2, 'Jimmy', 100), (6, 'Billy', 20);
CREATE PROCEDURE dbo.pVendorBalance
AS
BEGIN
-- first resultset
SELECT * FROM tab1 WHERE ID <=2;
-- second resultset
SELECT * FROM tab1 WHERE ID > 5;
END;
Version with OUT params:
CREATE PROCEDURE dbo.pVendorBalance2
#resultSet1 NVARCHAR(MAX) OUT,
#resultSet2 NVARCHAR(MAX) OUT
AS
BEGIN
SELECT #resultSet1 = (SELECT * FROM tab1 WHERE ID <=2 FOR JSON AUTO),
#resultSet2 = (SELECT * FROM tab1 WHERE ID > 5 FOR JSON AUTO);
END;
And final call:
DECLARE #r1 NVARCHAR(MAX), #r2 NVARCHAR(MAX);
EXEC dbo.pVendorBalance2 #r1 OUT, #r2 OUT;
-- first resultset as table
SELECT *
INTO #t1
FROM OpenJson(#r1)
WITH (ID int '$.ID', [Name] NVARCHAR(50) '$.Name',Amount money '$.Amount');
-- second resultset as table
SELECT *
INTO #t2
FROM OpenJson(#r2)
WITH (ID int '$.ID', [Name] NVARCHAR(50) '$.Name',Amount money '$.Amount');
SELECT * FROM #t1;
SELECT * FROM #t2;
DBFiddle Demo
EDIT:
Second approach is to use tSQLt.ResultSetFilter CLR function (part of tSQLt testing framework):
The ResultSetFilter procedure provides the ability to retrieve a single result set from a statement which produces multiple result sets.
CREATE TABLE #DatabaseSize (
database_name nvarchar(128),
database_size varchar(18),
unallocated_space varchar(18)
);
CREATE TABLE #ReservedSpaceUsed (
reserved VARCHAR(18),
data VARCHAR(18),
index_size VARCHAR(18),
unused VARCHAR(18)
);
INSERT INTO #DatabaseSize
EXEC tSQLt.ResultSetFilter 1, 'EXEC sp_spaceused';
INSERT INTO #ReservedSpaceUsed
EXEC tSQLt.ResultSetFilter 2, 'EXEC sp_spaceused';
SELECT * FROM #DatabaseSize;
SELECT * FROM #ReservedSpaceUsed;
No. But there is more of a work around since you cannot do an insert into with a procedure that returns multiple results with a different number of columns.
If you are allowed to modify the stored procedure, then you can declare temp tables outside of the procedure and populate them within the stored procedure. Then you can do whatever you need with them outside of the stored procedure.
CREATE TABLE #result1(Each column followed by data type of first result.);
----Example: CREATE TABLE #result1(Column1 int, Column2 varchar(10))
CREATE TABLE #result2(Each column followed by data type of second result.);
EXEC pVendorBalance;
SELECT * FROM #result1;
SELECT * FROM #result2;
I had a similar requirement, and ended up using the a CLR function which you can read about here (it's the answer with the InsertResultSetsToTables method, by user Dan Guzman):
https://social.msdn.microsoft.com/Forums/sqlserver/en-US/da5328a7-5dab-44b3-b2b1-4a8d6d7798b2/insert-into-table-one-or-multiple-result-sets-from-stored-procedure?forum=transactsql
You need to create a SQL Server CLR project in Visual Studio to get going. I had a project already written by a co-worker that I could just expand, but if you're starting from scratch, try reading this guide:
http://www.emoreau.com/Entries/Articles/2015/04/SQL-CLR-Integration-in-2015-year-not-product-version.aspx
If you've succeeded in writing and publishing the CLR project to the database, here is an example of using it I wrote:
-- declare a string with the SQL you want to execute (typically an SP call that returns multiple result sets)
DECLARE #sql NVARCHAR(MAX)
SET #sql = 'exec usp_SomeProcedure #variable1 = ' + #variable1 + '...' -- piece together a long SQL string from various parameters
-- create temp tables (one per result set) to hold the output; could also be actual tables (non-temp) if you want
CREATE TABLE #results_1(
[CustomerId] INT, [Name] varchar(500), [Address] varchar(500)
);
CREATE TABLE #results_2(
[SomeId] UNIQUEIDENTIFIER, [SomeData] INT, [SomethingElse] DateTime
);
-- on the exemplary 'CustomerDatabase' database, there is an SP (created automatically by the SQL CLR project deployment process in Visual Studio) which performs the actual call to the .NET assembly, and executes the .NET code
-- the CLR stored procedure CLR_InsertResultSetsToTables executes the SQL defined in the parameter #sourceQuery, and outputs multiple result sets into the specified list of tables (#targetTableList)
EXEC CustomerDatabase.dbo.CLR_InsertResultSetsToTables #sourceQuery = #sql, #targetTableList = N'#results_1,#results_2';
-- The output of the SP called in #sql is now dumped in the two temp tables and can be used for whatever in regular SQL
SELECT * FROM #results_1;
SELECT * FROM #results_2;
We can do it in the following way
Consider the input SP (which returns 2 tables as output) as usp_SourceData
Alter the usp_SourceData to accept a parameter as 1 and 2
Adjust the SP in a way that when
usp_SourceData '1' is executed it will return first table
and when
usp_SourceData '2' is executed it will return second table.
Actually stored procedures can return multiple result sets, or no result sets, it's pretty arbitrary. Because of this, I don't know of any way to navigate those results from other SQL code calling a stored procedure.
However, you CAN use the returned result set from a table-valued user defined function. It's just like a regular UDF, but instead of returning a scalar value you return a query result. Then you can use that UDF like any other table.
INSERT INTO #T SELECT * FROM dbp.pVendorBalanceUDF()
http://technet.microsoft.com/en-us/library/ms191165(v=sql.105).aspx
DROP TABLE ##Temp
DECLARE #dtmFrom VARCHAR(60) = '2020-12-01 00:00:00', #dtmTo VARCHAR(60) = '2020-12-02 23:59:59.997',#numAdmDscTransID VARCHAR(60) =247054
declare #procname nvarchar(255) = 'spGetCashUnpaidBills',
#procWithParam nvarchar(255) = '[dbo].[spGetCashUnpaidBills] #dtmFromDate= ''' +#dtmFrom+ ''' ,#dtmToDate= ''' +#dtmTo+''',#numCompanyID=1,#numAdmDscTransID='+ #numAdmDscTransID +',#tnyShowIPCashSchemeBills=1',
#sql nvarchar(max),
#tableName Varchar(60) = 'Temp'
set #sql = 'create table ##' + #tableName + ' ('
begin
select #sql = #sql + '[' + r.name + '] ' + r.system_type_name + ','
from sys.procedures AS p
cross apply sys.dm_exec_describe_first_result_set_for_object(p.object_id, 0) AS r
where p.name = #procname
set #sql = substring(#sql,1,len(#sql)-1) + ')'
execute (#sql)
execute('insert ##' + #tableName + ' exec ' + #procWithParam)
end
SELECT *FROM ##Temp
If the both result sets have same number of columns then
insert into #T1 exec dbo.pVendorBalance
will insert the union of both data set into #T1.
If not
Then edit dbo.pVendorBalance and insert results into temporary tables and in outer stored proc, select from those temporary tables.
Another way(If you need it), you can try
SELECT * into #temp
from OPENROWSET('SQLNCLI', 'Server=(local)\\(instance);Trusted_Connection=yes;',
'EXEC dbo.pVendorBalance')
it will take first dataset.
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;
I am working on a large project with a lot of stored procedures. I came into the following situation where a developer modified the arguments of a stored procedure which was called by another stored procedure.
Unfortunately, nothing prevents the ALTER PROC to complete.
Is there a way to perform those checks afterwards ?
What would be the guidelines to avoid getting into that kind of problems ?
Here is a sample code to reproduce this behavior :
CREATE PROC Test1 #arg1 int
AS
BEGIN
PRINT CONVERT(varchar(32), #arg1)
END
GO
CREATE PROC Test2 #arg1 int
AS
BEGIN
DECLARE #arg int;
SET #arg = #arg1+1;
EXEC Test1 #arg;
END
GO
EXEC Test2 1;
GO
ALTER PROC Test1 #arg1 int, #arg2 int AS
BEGIN
PRINT CONVERT(varchar(32), #arg1)
PRINT CONVERT(varchar(32), #arg2)
END
GO
EXEC Test2 1;
GO
DROP PROC Test2
DROP PROC Test1
GO
Sql server 2005 has a system view sys.sql_dependencies that tracks dependencies. Unfortunately, it's not all that reliable (For more info, see this answer). Oracle, however, is much better in that regard. So you could switch. There's also a 3rd party vendor, Redgate, who has Sql Dependency Tracker. Never tested it myself but there is a trial version available.
I have the same problem so I implemented my poor man's solution by creating a stored procedure that can search for strings in all the stored procedures and views in the current database. By searching on the name of the changed stored procedure I can (hopefully) find EXEC calls.
I used this on sql server 2000 and 2008 so it probably also works on 2005. (Note : #word1, #word2, etc must all be present but that can easily be changed in the last SELECT if you have different needs.)
CREATE PROCEDURE [dbo].[findWordsInStoredProceduresViews]
#word1 nvarchar(4000) = null,
#word2 nvarchar(4000) = null,
#word3 nvarchar(4000) = null,
#word4 nvarchar(4000) = null,
#word5 nvarchar(4000) = null
AS
BEGIN
-- SET NOCOUNT ON added to prevent extra result sets from
-- interfering with SELECT statements.
SET NOCOUNT ON;
-- create temp table
create table #temp
(
id int identity(1,1),
Proc_id INT,
Proc_Name SYSNAME,
Definition NTEXT
)
-- get the names of the procedures that meet our criteria
INSERT #temp(Proc_id, Proc_Name)
SELECT id, OBJECT_NAME(id)
FROM syscomments
WHERE OBJECTPROPERTY(id, 'IsProcedure') = 1 or
OBJECTPROPERTY(id, 'IsView') = 1
GROUP BY id, OBJECT_NAME(id)
-- initialize the NTEXT column so there is a pointer
UPDATE #temp SET Definition = ''
-- declare local variables
DECLARE
#txtPval binary(16),
#txtPidx INT,
#curText NVARCHAR(4000),
#counterId int,
#maxCounterId int,
#counterIdInner int,
#maxCounterIdInner int
-- set up a double while loop to get the data from syscomments
select #maxCounterId = max(id)
from #temp t
create table #tempInner
(
id int identity(1,1),
curName SYSNAME,
curtext ntext
)
set #counterId = 0
WHILE (#counterId < #maxCounterId)
BEGIN
set #counterId = #counterId + 1
insert into #tempInner(curName, curtext)
SELECT OBJECT_NAME(s.id), text
FROM syscomments s
INNER JOIN #temp t
ON s.id = t.Proc_id
WHERE t.id = #counterid
ORDER BY s.id, colid
select #maxCounterIdInner = max(id)
from #tempInner t
set #counterIdInner = 0
while (#counterIdInner < #maxCounterIdInner)
begin
set #counterIdInner = #counterIdInner + 1
-- get the pointer for the current procedure name / colid
SELECT #txtPval = TEXTPTR(Definition)
FROM #temp
WHERE id = #counterId
-- find out where to append the #temp table's value
SELECT #txtPidx = DATALENGTH(Definition)/2
FROM #temp
WHERE id = #counterId
select #curText = curtext
from #tempInner
where id = #counterIdInner
-- apply the append of the current 8KB chunk
UPDATETEXT #temp.definition #txtPval #txtPidx 0 #curtext
end
truncate table #tempInner
END
-- check our filter
SELECT Proc_Name, Definition
FROM #temp t
WHERE (#word1 is null or definition LIKE '%' + #word1 + '%') AND
(#word2 is null or definition LIKE '%' + #word2 + '%') AND
(#word3 is null or definition LIKE '%' + #word3 + '%') AND
(#word4 is null or definition LIKE '%' + #word4 + '%') AND
(#word5 is null or definition LIKE '%' + #word5 + '%')
ORDER BY Proc_Name
-- clean up
DROP TABLE #temp
DROP TABLE #tempInner
END
You can use sp_refreshsqlmodule to attempt to re-validate SPs (this also updates dependencies), but it won't validate this particular scenario with parameters at the caller level (it will validate things like invalid columns in tables and views).
http://www.mssqltips.com/tip.asp?tip=1294 has a number of techniques, including sp_depends
Dependency information is stored in the SQL Server metadata, including parameter columns/types for each SP and function, but it isn't obvious how to validate all the calls, but it is possible to locate them and inspect them.