Is there a way to query the parameters passed into a stored procedure and return them as XML without creating a string of the parameters and then casting that as xml? I'm looking for something generic, that will work for most SPs without having to physicially code it each time?
I have a bunch of stored procedures that access and modify verify specific information. At the end of the SPs I want to insert into a logging table the name of the SP, and the parameters (in xml) that were used to invoke the SP. I know how to get the name of the SP, and I know how to get a list of the parameters for the SP. What I want is a way to mash it all into XML along the actual values of the parameters that were passed.
I'm looking for something that does this, without the manual coding of each parameter:
DECLARE #L_Data varchar(1500)
SET #L_Data = '<parms>' +
CASE WHEN #ParamRegStationID IS NULL THEN ''
ELSE ',#ParamRegStationID=''' + Convert(varchar, #ParamRegStationID) + '''' END +
CASE WHEN #ParamScheduleID IS NULL THEN ''
ELSE ',#ParamScheduleID=''' + Convert(varchar, #ParamScheduleID) + '''' END +
CASE WHEN #ParamPatientID IS NULL THEN ''
ELSE ',#ParamPatientID=''' + Convert(varchar, #ParamPatientID) + '''' END +
CASE WHEN #ParamHISPatientID IS NULL THEN ''
ELSE ',#ParamHISPatientID=''' + #ParamHISPatientID + '''' END +
CASE WHEN #ParamEvent IS NULL THEN ''
ELSE ',#ParamEvent=''' + #ParamEvent + '''' END +
'</parms>'
This doesn't work, and it isn't as elegant as what I'm hoping for. However, here is an example illustrating what I'm trying to ultimately get to. It creates the temp table, but doesn't add the parameters to it as columns, so I can later extract it as XML.
ALTER PROC uspTest
#ParamID as bigint=null,
#ParamXYZ as varchar(255)=null
as
-- PROC Does whatever it is going to do ....
DECLARE #ProcName varchar(128), #ParmName varchar(128), #ParmType varchar(128), #ParmLen int,
#ParmSQL varchar(1000)
select #ProcName=OBJECT_NAME(##PROCID)
--select * from INFORMATION_SCHEMA.ROUTINES where ROUTINE_TYPE='PROCEDURE' and ROUTINE_NAME=#ProcName
DECLARE csrParms CURSOR
FOR
select PARAMETER_NAME, DATA_TYPE, CHARACTER_MAXIMUM_LENGTH from INFORMATION_SCHEMA.PARAMETERS where SPECIFIC_NAME=#ProcName and PARAMETER_MODE='IN'
ORDER BY ORDINAL_POSITION
FOR READ ONLY
OPEN csrParms
FETCH NEXT FROM csrParms
INTO #ParmName, #ParmType, #ParmLen
CREATE TABLE #Parms(ID int identity(1,1), Created DateTime)
INSERT INTO #Parms select GETDATE()
WHILE ##FETCH_STATUS = 0
BEGIN
-- GET Parm value and format as xml attribute to save parm
SET #ParmSQL = 'ALTER TABLE #Parms add ' + #ParmName + ' varchar(' + CAST(ISNULL(#ParmLen, 128) as varchar(128)) + ') NULL '
print #ParmSQL
EXEC (#ParmSQL)
SET #ParmSQL = 'UPDATE #Parms SET ' + #ParmName + ' = ''????'''
print #ParmSQL
--EXEC (#ParmSQL)
FETCH NEXT FROM csrParms
INTO #ParmName, #ParmType, #ParmLen
END
SET #ParmSQL = CAST((select * from #Parms FOR XML RAW) as varchar(1000))
select #ParmSQL
CLOSE csrParms
DEALLOCATE csrParms
This is close to what I'm looking for, I need to know how to replace the ??? with the current value of the parameter dynamically though.
ALTER PROC uspTest
#ParamID as bigint=null,
#ParamXYZ as varchar(255)=null
as
-- PROC Does whatever it is going to do ....
DECLARE #ProcName varchar(128), #ParmName varchar(128), #ParmType varchar(128), #ParmLen int,
#ParmSQL varchar(1000)
select #ProcName=OBJECT_NAME(##PROCID)
--select * from INFORMATION_SCHEMA.ROUTINES where ROUTINE_TYPE='PROCEDURE' and ROUTINE_NAME=#ProcName
set #ParmSQL =
' CREATE TABLE #Parms(ID int identity(1,1), Created DateTime, ' +
STUFF((select (', ' + REPLACE(PARAMETER_NAME,'#','') + ' varchar(' + CAST(ISNULL(CHARACTER_MAXIMUM_LENGTH, 128) as varchar(128)) + ') NULL ')
from INFORMATION_SCHEMA.PARAMETERS where SPECIFIC_NAME='uspTest' and PARAMETER_MODE='IN'
order by ORDINAL_POSITION for XML path(''), type).value('.', 'varchar(max)'), 1, 2, '')
+ ');
' + 'INSERT INTO #Parms (Created) select GETDATE(); ' + STUFF((select (';
UPDATE #Parms SET ' + REPLACE(PARAMETER_NAME,'#','') + ' = ''???''')
from INFORMATION_SCHEMA.PARAMETERS where SPECIFIC_NAME='uspTest' and PARAMETER_MODE='IN'
order by ORDINAL_POSITION for XML path(''), type).value('.', 'varchar(max)'), 1, 2, '')
+ ';
select CAST((select * from #Parms FOR XML RAW) as varchar(1000));'
print #ParmSQL
EXEC (#ParmSQL)
When I execute the proc as:
EXEC uspTest 1, 'test'
Returns:
<row ID="1" Created="2012-04-20T09:44:43.700" ParamID="???" ParamXYZ="???"/>
Prints out:
CREATE TABLE #Parms(ID int identity(1,1), Created DateTime, ParamID varchar(128) NULL , ParamXYZ varchar(255) NULL );
INSERT INTO #Parms (Created) select GETDATE();
UPDATE #Parms SET ParamID = '???';
UPDATE #Parms SET ParamXYZ = '???';
select CAST((select * from #Parms FOR XML RAW) as varchar(1000));
Is this SQL Server 2000 or later? If so you could use the FOR XML clause:
DECLARE #p1 varchar(100) = 'blah'
, #p2 int = 1
, #p3 datetime2(7) = '2011-01-01 13:41'
;
SELECT #p1 StringParm
, #p2 IntParm
, #p3 DateParm
FOR XML RAW
returns:
<row StringParm="blah" IntParm="1" DateParm="2011-01-01T13:41:00"/>
Edit
Ah, the problem there is that you need to parse out the parameter list as well as the values (which are local) into dynamic SQL (where they'd be out of scope).
I suppose you could use INFORMATION_SCHEMA.PARAMETERS to dynamically list the parameters and dbcc_inputbuffers to get the actual values passed. Something like:
create procedure junk
( #int INT
, #string VARCHAR(20)
, #date DATE
)
AS
BEGIN
DECLARE #tmp TABLE
( EventType NVARCHAR(30)
, PARMS INT
, Info NVARCHAR(2000)
);
DECLARE #object NVARCHAR(200);
INSERT INTO #tmp
EXEC('DBCC INPUTBUFFER(##SPID) WITH NO_INFOMSGS');
SELECT INFO
, 'Call' lType
FROM #tmp
UNION
SELECT STUFF(
( SELECT ', ' + parameter_name
FROM INFORMATION_SCHEMA.PARAMETERS
WHERE SPECIFIC_NAME = OBJECT_NAME(##procid)
ORDER BY ORDINAL_POSITION
FOR XML PATH('')
)
, 1
, 2
, ''
)
, 'Parms';
END
That now makes it so that:
exec dbo.junk #int = 3, #string = 'hoo', #date = '2/2/2002';
Returns:
exec dbo.junk #int = 3, #string = 'hoo', #date = '2/2/2002'; Call
#int, #string, #date Parms
Which should get you a ways along. The tricky bit is that DBCC_INPUTBUFFERS returns the EXACT call string. So you'd need to write code to parse out the call to match the input line to the parameter list. If you go that route, you'll likely want a stored function that does the parsing. It would likely take the call string and parameter list something like the return values above, match them, and use the FOR XML clause to return the format you want.
You could also parse call string in a cursor tied to the parameter list. Then you'd pull the parameters in order and look for the commas and #'s. You could still have trouble with parameter values that included those characters if you didn't take that into account.
IMHO, getting that squared away seems like a lot of work compared to a simple select which can almost be copied/pasted from the function header. Of course, if you're talking about a large volume of procedures then it might be worth it. Either way, good luck and thanks for a thought-provoking question.
Related
I will pass a table-valued input parameter into a stored procedure, and also a variable that contains query string, so I made my sproc like this.
CREATE PROCEDURE [dbo].[SP_SelectData_View]
(
#Sort VARCHAR(MAX),
#CONDITION VARCHAR(MAX) = ''
#Values dbo.FlowStatus READONLY
)
AS
BEGIN
DECLARE #STRQUERY NVARCHAR(MAX)
IF #CONDITION IS NOT NULL AND #CONDITION != ''
BEGIN
SET #CONDITION = 'WHERE ' + #CONDITION
END
ELSE
BEGIN
SET #CONDITION = ''
END
IF #Sort IS NULL OR #Sort = ''
BEGIN
SET #Sort = 'Id Desc'
END
BEGIN
SET #STRQUERY = 'SELECT A.*
FROM ' + #Values + ' as FlowStatus'
JOIN Tbl_A as A
ON A.status = FlowStatus.StatusNowId AND B.flow = FlowStatus.FlowNowId
' + #CONDITION + '
Order By ' + #Sort
EXEC(#STRQUERY)
END
END
But in the code above, I got an error
must declare scalar variable #Values
I've searched for it and I think it is because the aliases is not detected because it's inside a string. But if I didn't put it in a string query, the #condition and #sort variable will be error. Is there a solution where I can do both calling the table-valued variable and query string variable together?
There are several things wrong with the approach you currently have, as I and others have commented, Brent Ozar has a good reference on dynamic SQL https://www.brentozar.com/sql/dynamic/
I would say don't pass in some SQL, construct it in the stored proc; passing in parameters such as name which is used in the where, hence I have put a full working example. This also shows how to pass the user defined table type into the stored proc and then also pass it into the dynamic SQL.
I hope this is a good enough example of the techniques, I had a bit of time so thought I would try and help as much as possible :)
/*
--------------------------------------------
Create a test table to run the stored proc against
*/
IF (NOT EXISTS (SELECT * FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_SCHEMA = 'dbo' AND TABLE_NAME = 'MyTestTable'))
BEGIN
PRINT 'Creating table MyTestTable'
CREATE TABLE [dbo].[MyTestTable](
Id BIGINT NOT NULL IDENTITY(1,1) PRIMARY KEY,
[Name] NVARCHAR(50) NOT NULL
)
INSERT INTO dbo.MyTestTable ([Name])
VALUES ('Andrew'),
('Bob'),
('john')
-- SELECT * FROM MyTestTable
END
GO
/*
--------------------------------------------
Create the table type that we pass into the store proc
*/
IF NOT EXISTS (SELECT * FROM sys.types WHERE is_table_type = 1 AND name = 'FlowStatus')
BEGIN
PRINT 'Creating type [dbo].[FlowStatus]'
CREATE TYPE [dbo].FlowStatus AS TABLE (
MyId BIGINT PRIMARY KEY,
SomeText NVARCHAR(200)
)
END
GO
/*
--------------------------------------------
Create the stored proc with the User Defined table type
*/
CREATE OR ALTER PROCEDURE [dbo].[MyStoredProc]
(
#SortBy VARCHAR(50),
#SearchName VARCHAR(50),
#Values dbo.FlowStatus READONLY
)
AS
BEGIN
-- As your SQL gets more complex it is an idea to create seperate parts of the SQL
DECLARE #SqlToExecute NVARCHAR(MAX)
-- The primary data you want to get
SET #SqlToExecute = N'
SELECT T.Id, T.[Name], V.SomeText
FROM MyTestTable AS T
LEFT JOIN #Values AS V ON V.MyId = T.Id
WHERE 1 = 1' -- We do this so that we can have many AND statements which could be expanded upon
IF #SearchName IS NOT NULL
BEGIN
SET #SqlToExecute = #SqlToExecute + N'
AND T.[Name] LIKE ''%' + #SearchName + ''''
END
IF #SortBy IS NOT NULL
BEGIN
SET #SqlToExecute = #SqlToExecute + N'
ORDER BY ' +
CASE WHEN #SortBy LIKE 'Name%' THEN N'T.[Name]'
ELSE N'T.[Id]'
END
END
-- Print out the script that will be run, useful for debugging you code
PRINT #SqlToExecute
EXEC sp_executesql #SqlToExecute,
N'#Values dbo.FlowStatus READONLY', #Values
END
GO
/*
--------------------------------------------
Now lets test it
-- Test Andrew
*/
DECLARE #flowStatusType AS dbo.FlowStatus
INSERT INTO #flowStatusType(MyId, SomeText)
VALUES(1, 'Test1'),
(2, 'Test2')
EXEC [dbo].[MyStoredProc] #SearchName = 'Andrew', #SortBy = 'Name', #Values = #flowStatusType
GO
-- Test Bob
DECLARE #flowStatusType AS dbo.FlowStatus
INSERT INTO #flowStatusType(MyId, SomeText)
VALUES(1, 'Test1'),
(2, 'Test2')
EXEC [dbo].[MyStoredProc] #SearchName = 'Bob', #SortBy = 'Name', #Values = #flowStatusType
GO
Its also worth noting that if you can just join on the #Values without needing dynamic SQL then that is sure to be less work.
I am trying to write a stored procedure in SQL Server which will:
Take a list of integers as input ( let's assume these integers are "profile_id")
pick up all the table names which has a column named as "profile_id" into a cursor
loop through the cursor and print the profile_id value when it matches one of them in the input list of params.
Now the problem is: I am executing the procedure like this:
EXEC dbo.de_dup '1234,2345';
and getting a syntax error when trying to execute the commented out line below (Please see the procedure):
set #id = (select profile_id from #tname where profile_id in #a_profile_id );
Questions:
What would be the right way of executing and setting the value inside a cursor?
What is way (in our case) to pass a list of integers to this procedure?
This is my procedure:
ALTER PROCEDURE dbo.de_dup
(#a_profile_id nvarchar(MAX))
AS
DECLARE #tname VARCHAR(max),
#id int;
DECLARE tables_cursor CURSOR FOR
SELECT
a.TABLE_CATALOG +'.'+a.TABLE_SCHEMA + '.'+ a.TABLE_NAME AS table_name
FROM
JobApp.INFORMATION_SCHEMA.COLUMNS a
LEFT OUTER JOIN
JobApp.INFORMATION_SCHEMA.VIEWS b ON a.TABLE_CATALOG = b.TABLE_CATALOG
AND a.TABLE_SCHEMA = b.TABLE_SCHEMA
AND a.TABLE_NAME = b.TABLE_NAME
WHERE
a.COLUMN_NAME = 'profile_id'
GROUP BY
a.TABLE_CATALOG, a.TABLE_SCHEMA, a.TABLE_NAME, a.COLUMN_NAME;
OPEN tables_cursor;
FETCH NEXT FROM tables_cursor INTO #tname;
WHILE ##FETCH_STATUS = 0
BEGIN
PRINT #a_profile_id ;
PRINT #tname ;
--set #id= (select profile_id from #tname where profile_id in #a_profile_id );
--PRINT 'id : ' + #id;
FETCH NEXT FROM tables_cursor INTO #tname;
END;
CLOSE tables_cursor;
DEALLOCATE tables_cursor;
GO;
Please let me know should I provide more clarification. Thanks in advance.
This solution is using the dynamic SQL, As per my knowledge we need to use the dynamic SQL if we have the table name in a variable.
DBFIDDLE working code
Query:
CREATE PROCEDURE dbo.de_dup (#a_profile_id NVARCHAR(MAX))
AS
BEGIN
DECLARE #tname VARCHAR(max)
,#id INT
,#dynamicSQL NVARCHAR(MAX);
DECLARE #matched_tables TABLE (Name NVARCHAR(255));
DECLARE #matched_profileIds TABLE (profile_id INT);
DECLARE #profile_ids NVARCHAR(MAX) = #a_profile_id
INSERT INTO #matched_tables
SELECT DISTINCT a.TABLE_SCHEMA + '.' + a.TABLE_NAME AS table_name
FROM INFORMATION_SCHEMA.COLUMNS a
WHERE a.COLUMN_NAME = 'profile_id'
WHILE EXISTS (
SELECT 1
FROM #matched_tables
)
BEGIN
SELECT TOP 1 #tname = [Name]
FROM #matched_tables
SET #dynamicSQL = CONCAT (
'select profile_id from '
,#tname
,' WHERE '
,''','
,#profile_ids
,','''
,' LIKE '
,'''%,'
,''''
,' + CAST(profile_id AS NVARCHAR(MAX)) + '
,''',%'
,''''
)
PRINT #dynamicSQL;
INSERT INTO #matched_profileIds
EXEC (#dynamicSQL)
DELETE
FROM #matched_tables
WHERE [Name] = #tname
END
SELECT *
FROM #matched_profileIds
END
Dynamic SQL that gets formed is
SELECT profile_id
FROM dbo.TestTable
WHERE ',123,456,789,1011,1213,' LIKE '%,' + CAST(profile_id AS NVARCHAR(MAX)) + ',%'
So I have solved a similar issue with a table-valued function called Split. It splits a delimited list into rows in a table, which you can then JOIN or use as a subquery in your code.
CREATE FUNCTION [dbo].[Split]
(
#char_array varchar(500), #delimiter char(1)
)
RETURNS
#parsed_array table
(
Parsed varchar(50)
)
AS
BEGIN
DECLARE #parsed varchar(50), #pos int
SET #char_array = LTRIM(RTRIM(#char_array))+ #delimiter
SET #pos = CHARINDEX(#delimiter, #char_array, 1)
IF REPLACE(#char_array, #delimiter, '') <> ''
BEGIN
WHILE #pos > 0
BEGIN
SET #parsed = LTRIM(RTRIM(LEFT(#char_array, #pos - 1)))
IF #parsed <> ''
BEGIN
INSERT INTO #parsed_array (Parsed)
VALUES (#parsed)
END
SET #char_array = RIGHT(#char_array, LEN(#char_array) - #pos)
SET #pos = CHARINDEX(#delimiter, #char_array, 1)
END
END
RETURN
END
GO
You would use it like so
SELECT f.Parsed INTO #s FROM dbo.Split(#a_profile_id, ',') f;
Then in your query (only the relevant part for brevity)
select profile_id from #tname where profile_id in(select Parsed from #s);
I left out the set #id= because that will produce unpredictable results for the value of #id if the select statement returns multiple results. But you indicated this is not the actual code anyway so...
Disclaimer: I got the meat of the Split function from someone else online. If I could remember who I would attribute it properly.
I need a script that generate insert statements but with check for if the data doesn't already exist, this because it should be periodically run on parallell systems where different dtata will be added to the systems but we want them tables to be in sync. I have the basic ides and borrowed parts of code but get a syntax error i have trouble solving.
I'm basing my code on the code Param Yadav showed at Converting Select results into Insert script - SQL Server but I need to check for data already in the table. (I need to add more "bells & whistles later, but take this step-by-step)
My own main addition is the #NOT_EXISTS part which should be in the WHERE clause of the NOT EXISTS check. If I replace that with a plain WHERE 0=1 I get no syntax error so it indicates the error is in my #NOT_EXISTS string.
Edit: Yesterday I thought I had an answer to my own question but when running on "real data" I saw that some lines are too long for QUOTENAME, I have to fix those quotation marks "manually" (concats in script) instead...
SET NOCOUNT ON
DECLARE #CSV_COLUMN VARCHAR(MAX),
#QUOTED_DATA VARCHAR(MAX),
#NOT_EXISTS VARCHAR(MAX),
#SQL_KOD VARCHAR(MAX),
#TABLE_NAME VARCHAR(MAX),
#FILTER_CONDITION VARCHAR(MAX)='',
#FIRST_COL INT,
#LAST_COL INT
/* INPUT DATA */
SELECT #TABLE_NAME = 'WorkflowError'
SELECT #FIRST_COL = 2
SELECT #LAST_COL = 4
/* */
SELECT #CSV_COLUMN=STUFF
(
(
SELECT ',['+ NAME +']' FROM sys.all_columns
WHERE OBJECT_ID=OBJECT_ID(#TABLE_NAME) AND
is_identity!=1 FOR XML PATH('')
),1,1,''
)
--SELECT #CSV_COLUMN
SELECT #QUOTED_DATA=STUFF
(
(
SELECT ' ISNULL(QUOTENAME('+NAME+','+QUOTENAME('''','''''')+'),'+'''NULL'''+')+'','''+'+' FROM sys.all_columns
WHERE OBJECT_ID=OBJECT_ID(#TABLE_NAME) AND
is_identity!=1 FOR XML PATH('')
),1,1,''
)
SELECT #QUOTED_DATA=SUBSTRING(#QUOTED_DATA,1,LEN(#QUOTED_DATA)-5)
SELECT #QUOTED_DATA
SELECT #NOT_EXISTS=STUFF
(
(
SELECT ' ['+ COLUMN_NAME +']=', 'ISNULL(QUOTENAME('+COLUMN_NAME+','+QUOTENAME('''','''''')+'),'+'''NULL'''+') AND '
FROM information_schema.columns
WHERE table_name = #TABLE_NAME AND
ordinal_position BETWEEN #FIRST_COL AND #LAST_COL
FOR XML PATH('')
),1,1,''
)
SELECT #NOT_EXISTS=SUBSTRING(#NOT_EXISTS,1,LEN(#NOT_EXISTS)-4)
SELECT #NOT_EXISTS
--SELECT #NOT_EXISTS=' 0=1 '
SELECT #SQL_KOD='SELECT ''
IF NOT EXISTS(SELECT 1
FROM ' + #TABLE_NAME + ' WHERE ' + #NOT_EXISTS + ')
BEGIN
INSERT INTO '+#TABLE_NAME+'('+#CSV_COLUMN+')
VALUES('''+'+'+#QUOTED_DATA+'+'+''')
END
GO '''+' Insert_Scripts
FROM '+#TABLE_NAME + #FILTER_CONDITION
SELECT #SQL_KOD
EXECUTE (#SQL_KOD)
GO
[stackoverflow won't let me post code unless it's formatted, but then the strings below won't be as they are created in the script...]
When I do SELECT #NOT_EXISTS=' 0=1 ' I get an INSERT line for each row in my table:
IF NOT EXISTS(SELECT 1 FROM WorkflowError WHERE 0=1 )
BEGIN
INSERT INTO WorkflowError([TargetSystem],[ErrorCode],[ErrorText],[RetryMaxCount],[RetryStrategyName],[ErrorDescription])
VALUES('EttLiv','800','Value cannot be null. Parameter name: source','0',NULL,'Value cannot be null. Parameter name: source')
END
GO
With my #NOT_EXISTS code the #SQL_KOD string becomes this:
SELECT 'IF NOT EXISTS(SELECT 1 FROM WorkflowError
WHERE [TargetSystem]=ISNULL(QUOTENAME(TargetSystem,''''),'NULL'))
BEGIN
INSERT INTO WorkflowError([TargetSystem],[ErrorCode],[ErrorText],[RetryMaxCount],[RetryStrategyName],[ErrorDescription])
VALUES('+ISNULL(QUOTENAME(TargetSystem,''''),'NULL')+','
+ ISNULL(QUOTENAME(ErrorCode,''''),'NULL')+','
+ ISNULL(QUOTENAME(ErrorText,''''),'NULL')+','
+ ISNULL(QUOTENAME(RetryMaxCount,''''),'NULL')+','
+ ISNULL(QUOTENAME(RetryStrategyName,''''),'NULL')+','
+ ISNULL(QUOTENAME(ErrorDescription,''''),'NULL')+')
END
GO ' Insert_Scripts FROM WorkflowError
However, trying to execute that #SQL_KOD line just gives:
Msg 156, Level 15, State 1, Line 3
Incorrect syntax near the keyword 'NULL'.
...and I can't find out where I have done wrong, if it's in my thinking or if it's just a misplaced quotation mark...
Where do you expect #SQL_KOD to get its values from? Because if you are retrieving your values for TargetSystem / ErrorCode / ... / ErrorDescription from somewhere outside of your insert statement, I would expect a "from" statement. If you want to input variables, you are missing both the definition of the variables and the #-sign in front of the variable name.
As far as keeping quotes happy: try writing your code with QUOTED_IDENTIFIER OFF - you can create the entire #SQL_KOD variable by writing between double quotes ("), and single quotes would behave like normal quotation marks.
A very basic re-write of your code could be something as follows:
SET QUOTED_IDENTIFIER OFF
DECLARE #SQL_KOD VARCHAR(MAX)
SET #SQL_KOD =
"DECLARE #WorkFlowError TABLE ([TargetSystem] NVARCHAR(200),[ErrorCode] NVARCHAR(200))
IF NOT EXISTS ( SELECT 1 FROM #WorkFlowError )
BEGIN
INSERT INTO #WorkFlowError ([TargetSystem],[ErrorCode])
SELECT ISNULL(QUOTENAME([TargetSystem],''''),'NULL')
, ISNULL(QUOTENAME([ErrorCode],''''),'NULL')
FROM (
SELECT [TargetSystem]='Foo'
, [ErrorCode]='Bar'
) src
END";
I originally used QUOTENAME as in the Param Yadav script I borrowed from but that function can't handle long strings. It doesn't complain, just returns NULL if the string is too long. Now the script is less readable (long lines of quotation marks) but now works.
SET NOCOUNT ON
DECLARE #CSV_COLUMN VARCHAR(MAX),
#QUOTED_DATA VARCHAR(MAX),
#NOT_EXISTS VARCHAR(MAX),
#SQL_KOD VARCHAR(MAX),
#TABLE_NAME VARCHAR(MAX),
#FILTER_CONDITION VARCHAR(MAX),
#FIRST_COL INT,
#LAST_COL INT
/* INPUT DATA */
SELECT #TABLE_NAME = 'WorkflowError'
SELECT #FIRST_COL = 2
SELECT #LAST_COL = 4
SELECT #FILTER_CONDITION = ''
/* */
SELECT #CSV_COLUMN=STUFF
(
(
SELECT ',['+ NAME +']' FROM sys.all_columns
WHERE OBJECT_ID=OBJECT_ID(#TABLE_NAME) AND
is_identity!=1 FOR XML PATH('')
),1,1,''
)
SELECT #QUOTED_DATA=STUFF
(
(
SELECT ' ISNULL('''''''' + REPLACE('+NAME+','''''''','''''''''''') + '''''''','''+'NULL'''+''+')+'',''+'
FROM sys.all_columns
WHERE OBJECT_ID=OBJECT_ID(#TABLE_NAME) AND
is_identity!=1 FOR XML PATH('')
),1,1,''
)
SELECT #QUOTED_DATA=SUBSTRING(#QUOTED_DATA,1,LEN(#QUOTED_DATA)-5)
SELECT #NOT_EXISTS=STUFF
(
(
SELECT ' ['+ COLUMN_NAME +']='' + ', 'ISNULL('''''''' + REPLACE('+COLUMN_NAME+','''''''','''''''''''') + '''''''','''+'NULL'''+''+')+'' AND '
FROM information_schema.columns
WHERE table_name = #TABLE_NAME AND
ordinal_position BETWEEN #FIRST_COL AND #LAST_COL
FOR XML PATH('')
),1,1,''
)
SELECT #NOT_EXISTS=SUBSTRING(#NOT_EXISTS,1,LEN(#NOT_EXISTS)-6)
SELECT #SQL_KOD='SELECT ''IF NOT EXISTS(SELECT 1 FROM ' + #TABLE_NAME + ' WHERE ' + #NOT_EXISTS + ' + ' + ''') BEGIN INSERT INTO '+#TABLE_NAME+'('+#CSV_COLUMN+')VALUES('''+'+'+#QUOTED_DATA+'+'+''') END '''+' Insert_Scripts FROM ' + #TABLE_NAME + ' ' + #FILTER_CONDITION
EXECUTE (#SQL_KOD)
SET NOCOUNT OFF
i try conver some string such as '1,2,3' to 'a,b,c' with the anwser:
select stuff(
(
select ',' + realname from sys_user
where ','+'1,2,3'+',' like '%,'+cast(u_id as varchar(10))+',%' for xml path('')
),1,1,'')
charindex is well done. but i want to create a more common function, so that i can convert in any relation such that.
i try a function :
create function [dbo].[fn_enum2str]
(
#enum as varchar(1000),
#table_name as varchar(100),
#origin_field as varchar(100),
#target_field as varchar(100)
)
as
begin
declare #result varchar(1000)
declare #sqlstr nvarchar(1000)
set #sqlstr = 'set #result = ('
set #sqlstr = #sqlstr + 'select stuff('
set #sqlstr = #sqlstr + '(select '','' + ' +#target_field+ ' from ' + #table_name
set #sqlstr = #sqlstr + ' where '','+#enum+','' like ''%,''+cast('+#origin_field+' as varchar)+'',%'' for xml path(''''))'
set #sqlstr = #sqlstr + ',1,1,''''))'
exec(#sqlstr)
return #result
end
it faild with error, as you know, it is not allow to exec a dynamic sql in function.
i want to
select dbo.fn_enum2str(a.uids,'sys_user','u_id', 'realname') from my_table a
--output 'a,b,c'
so, in my question, how can i create a function or a proc to deal it ?
Suppose you have SQL-SERVER2016 you can use string_split like this:
Test data
CREATE TABLE [dbo].[stringlist]([Numbers] [nvarchar](50) NULL)
Insert into dbo.Stringlist(numbers)
values('1,2,3,4,5,10')
SQL Function
alter function dbo.HinkyBase26( #Value as varchar(250) ) returns VarChar(250) as
begin
--declare #Value as varchar(50) = '13,14,1,2,5,14'
-- Notes: 'A' = 0. Negative numbers are not handled.
declare #Result as VarChar(250) = '';
declare #stringsplit table (numbers nvarchar(50),Letters varchar(1))
insert into #stringsplit(numbers,Letters)
select numbers = #Value ,CHAR(64 + value) as Letters from string_split(#Value,',')
select #Result = Letter from (
select numbers,Letter = STUFF((Select ', ' + Letters
from #stringsplit b
where b.numbers = a.numbers
FOR XML PATH('')),1,2,'')
from #stringsplit a
group by numbers
)z
return #Result
end
Execution of function
SELECT TOP (1000) [Numbers],dbo.HinkyBase26(Numbers)
FROM [LegOgSpass].[dbo].[stringlist]
SQL Stored Proc
Create PROC dbo.usp_convertnumberstostring
#stringvalue nvarchar(250)
AS
BEGIN
Create table #stringsplit (numbers nvarchar(50),Letters varchar(1))
insert into #stringsplit(numbers,Letters)
SELECT Numbers = #stringvalue,CHAR(64 + value) as Letters
from string_split(#stringvalue,',')
select numbers,Letter = STUFF((Select DISTINCT ', ' + Letters
from #stringsplit b
where b.numbers = a.numbers
FOR XML PATH('')),1,2,'')
from #stringsplit a
group by numbers
drop table #stringsplit
END
Execute SP
DECLARE #RC int
DECLARE #stringvalue nvarchar(250) = '1,5,6'
-- TODO: Set parameter values here.
EXECUTE #RC = [dbo].[usp_convertnumberstostring]
#stringvalue
GO
Result
SQL Script
Create table #stringsplit (numbers nvarchar(50),Letters varchar(1))
insert into #stringsplit(numbers,Letters)
SELECT Numbers,CHAR(64 + value) as Letters
FROM [LegOgSpass].[dbo].[stringlist] a
cross apply string_split(numbers,',')
select numbers,Letter = STUFF((Select DISTINCT ', ' + Letters
from #stringsplit b
where b.numbers = a.numbers
FOR XML PATH('')),1,2,'')
from #stringsplit a
group by numbers
Drop table #stringsplit
CREATE function [dbo].[fn_enum2str]
(
#enum as varchar(1000),
#table_name as varchar(100)
)
returns varchar(1000)
as
begin
declare #result varchar(1000)
if #enum is null
return ''
if #table_name = 'sys_user'
set #result = (
select stuff(
(
select ',' + realname from sys_user
where ','+#enum+',' like '%,'+cast(u_id as varchar(10))+',%' for xml path('')
),1,1,''
)
)
if #table_name = 'sys_attachment'
set #result = (
select stuff(
(
select ',/' + filepath from sys_attachment
where ','+#enum+',' like '%,'+cast(aid as varchar(10))+',%' for xml path('')
),1,1,''
)
)
return #result
end
GO
only way to deal it what i can think of, to switch which sql will be exec by a flag. when other relation apearance, add it to the switch list.
select
dbo.fn_enum2str(a.uids, 'sys_user') as names,
dbo.fn_enum2str(a.attachids, 'sys_attachment') as filepaths
from my_table a
so that it can be overlay. yes, it is difficult to remember stuff or for xml path or listagg(oracle), and result to a long sql, and i am lazy.😄
if you have any anwser better, tell me, thanks.
After a lot of searching and piecing together the very excellent techniques for converting result sets using the FOR XML and .nodes() commands that are around the web, I was able to create this single query (not a stored procedure) which does a reasonably good job of converting any arbitrary SQL query to a JSON array.
The query will encode each data row as a single JSON object with a leading comma.
The data rows are wrapped by brackets and the whole result set is then expected to be exported to a file.
I'd like to see if anyone out there can see ways to improve its performance?
Here's the query with a sample table:
declare #xd table (col1 varchar(max), col2 int, col3 real, colNull int)
insert into #xd
select '', null, null, null
UNION ALL select 'ItemA', 123, 123.123, null
UNION ALL select 'ItemB', 456, 456.456, null
UNION ALL select '7890', 789, 789.789, null
select '[{}'
UNION ALL
select ',{' + STUFF((
(select ','
+ '"' + r.value('local-name(.)', 'varchar(max)') + '":'
+ case when r.value('./#xsi:nil', 'varchar(max)') = 'true' then 'null'
when isnumeric(r.value('.', 'varchar(max)')) = 1
then r.value('.', 'varchar(max)')
else '"' + r.value('.', 'varchar(max)') + '"'
end
from rows.nodes('/row/*') as x(r) for xml path(''))
), 1, 1, '') + '}'
from (
-- Arbitrary query goes here, (fields go where t.* is, table where #xd t is)
select (select t.* for xml raw,type,elements XSINIL) rows
from #xd t
) xd
UNION ALL
select ']'
My biggest critique of it, is that it's insanely slow.
It currently takes about 3:30 for ~42,000 rows.
My other big critique is that it currently assumes that everything that looks like a number is a number. It doesn't try to discover column type in the least (and I'm not even sure if it can).
A final minor critique is that the first data row will have a comma up front and technically it shouldn't. To compensate for that it requires that empty JSON object in the first row that starts the JSON array.
Other critiques (preferably with solutions) invited, the only real limitation I have is that the solution be decently repeatable on many arbitrary SQL queries without having to explicitly identify the column names.
I'm using SQL Server 2012.
Thanks and to anyone else like me who was looking for a generalized SQL Results -> JSON Array converter, ENJOY!
I say if you really want to kick up performance, use metaprogramming. The example below tries this with 40,000 rows and returns results in less than a second (not counting inserting the initial 40k rows, which in this example only takes about 2 seconds). It also takes into account your data types to not enclose numbers in quotes.
declare #xd table (col1 varchar(max), col2 int, col3 real, colDate datetime, colNull int);
declare #i int = 0;
while #i < 10000 begin
set #i += 1;
insert into #xd
select '', null, null, null, null
union all select 'ItemA', 123, 123.123, getDate(), null
union all select 'ItemB', 456, 456.456, getDate(), null
union all select '7890', 789, 789.789, getDate(), null;
end;
select *
into #json_base
from (
-- Insert SQL Statement here
select * from #xd
) t;
declare #columns table (
id int identity primary key,
name sysname,
datatype sysname,
is_number bit,
is_date bit);
insert into #columns(name, datatype, is_number, is_date)
select columns.name, types.name,
case when number_types.name is not NULL
then 1 else 0
end as is_number,
case when date_types.name is not NULL
then 1 else 0
end as is_date
from tempdb.sys.columns
join tempdb.sys.types
on (columns.system_type_id = types.system_type_id)
left join (values ('int'), ('real'), ('numeric'),
('decimal'), ('bigint'), ('tinyint')) as number_types(name)
on (types.name = number_types.name)
left join (values ('date'), ('datetime'), ('datetime2'),
('smalldatetime'), ('time'), ('datetimeoffset')) as date_types(name)
on (types.name = date_types.name)
where object_id = OBJECT_ID('tempdb..#json_base');
declare #field_list varchar(max) = STUFF((
select '+'',''+' + QUOTENAME(QUOTENAME(name, '"') + ':', '''')
+ '+' + case when is_number = 1
then 'COALESCE(LTRIM('
+ QUOTENAME(name) + '),''null'')'
when is_date = 1
then 'COALESCE(QUOTENAME(LTRIM(convert(varchar(max), '
+ QUOTENAME(name) + ', 126)),''"''),''null'')'
else 'COALESCE(QUOTENAME('
+ QUOTENAME(name) + ',''"''),''null'')'
end
from #columns
for xml path('')),
1, 5, '');
create table #json_result (
id int identity primary key,
line varchar(max));
declare #sql varchar(max) = REPLACE(
'insert into #json_result '
+ 'select '',{''+{f}+''}'' '
+ 'from #json_base', '{f}', #field_list);
exec(#sql);
update #json_result
set line = STUFF(line, 1, 1, '')
where id = 1;
select '['
UNION ALL
select line
from #json_result
UNION ALL
select ']';
drop table #json_base;
drop table #json_result;
From Firoz Ansari:
CREATE PROCEDURE [dbo].[GetJSON] (
#ParameterSQL AS VARCHAR(MAX)
)
AS
BEGIN
DECLARE #SQL NVARCHAR(MAX)
DECLARE #XMLString VARCHAR(MAX)
DECLARE #XML XML
DECLARE #Paramlist NVARCHAR(1000)
SET #Paramlist = N'#XML XML OUTPUT'
SET #SQL = 'WITH PrepareTable (XMLString) '
SET #SQL = #SQL + 'AS ( '
SET #SQL = #SQL + #ParameterSQL+ ' FOR XML RAW, TYPE, ELEMENTS '
SET #SQL = #SQL + ') '
SET #SQL = #SQL + 'SELECT #XML = XMLString FROM PrepareTable '
EXEC sp_executesql #SQL, #Paramlist, #XML=#XML OUTPUT
SET #XMLString = CAST(#XML AS VARCHAR(MAX))
DECLARE #JSON VARCHAR(MAX)
DECLARE #Row VARCHAR(MAX)
DECLARE #RowStart INT
DECLARE #RowEnd INT
DECLARE #FieldStart INT
DECLARE #FieldEnd INT
DECLARE #Key VARCHAR(MAX)
DECLARE #Value VARCHAR(MAX)
DECLARE #StartRoot VARCHAR(100); SET #StartRoot = ''
DECLARE #EndRoot VARCHAR(100); SET #EndRoot = ''
DECLARE #StartField VARCHAR(100); SET #StartField = ''
SET #RowStart = CharIndex(#StartRoot, #XMLString, 0)
SET #JSON = ''
WHILE #RowStart > 0
BEGIN
SET #RowStart = #RowStart+Len(#StartRoot)
SET #RowEnd = CharIndex(#EndRoot, #XMLString, #RowStart)
SET #Row = SubString(#XMLString, #RowStart, #RowEnd-#RowStart)
SET #JSON = #JSON+'{'
-- for each row
SET #FieldStart = CharIndex(#StartField, #Row, 0)
WHILE #FieldStart > 0
BEGIN
-- parse node key
SET #FieldStart = #FieldStart+Len(#StartField)
SET #FieldEnd = CharIndex(#EndField, #Row, #FieldStart)
SET #Key = SubString(#Row, #FieldStart, #FieldEnd-#FieldStart)
SET #JSON = #JSON+'"'+#Key+'":'
-- parse node value
SET #FieldStart = #FieldEnd+1
SET #FieldEnd = CharIndex('0 SET #JSON = SubString(#JSON, 0, LEN(#JSON))
SET #JSON = #JSON+'},'
--/ for each row
SET #RowStart = CharIndex(#StartRoot, #XMLString, #RowEnd)
END
IF LEN(#JSON) > 0 SET #JSON = SubString(#JSON, 0, LEN(#JSON))
SET #JSON = '[' + #JSON + ']'
SELECT #JSON
END