Get an ADO recordset from dynamic SQL - sql-server

I need to be able to run a dynamic query within SQL Server, and store the results in an ADO recordset (in a VBA application). I've read that it's impossible to run dynamic SQL in a function (even a multi-statement function) - is this correct? I've also read that it's impossible to return a table variable from a stored procedure - is this also correct? If so, how do I do this?
The only ways I can think of involve:
having a permanent table, inserting into it with a stored procedure, and then running a function to get the results, or
querying the information I need to create the dynamic SQL into ADO, using VBA to generate the SQL string, and then querying that separately.
Neither of these are ideal. What I'd really like to have is this:
-- =============================================
-- Author: <snip>
-- Create date: 7/5/2022
-- Description: This function is for displaying details about report/query variant filters.
-- It takes the Report Variant ID, and returns all the display details from [list].[ReportAndQueryVariantDefaultFilterListInt]
-- =============================================
CREATE FUNCTION [dbo].[udf_Reports_DefaultFilterInfoForVariantID]
(
-- Add the parameters for the function here
#ReportVariantID int
)
RETURNS
#Tbl TABLE
(
-- Add the column definitions for the TABLE variable here
ListIDTypeID int
, PKID int
, [IDTypeDescription] varchar(50)
, ValueDisplay varchar(200)
)
AS
BEGIN
-- Fill the table variable with the rows for your result set
DECLARE #SQL nvarchar(max)
DECLARE #MainTblSchema varchar(8), #MainTblName varchar(100), #MainTblPKName varchar(50), #DisplayColumn varchar(50)
SET #SQL = 'INSERT INTO #Tbl (ListIDTypeID, PKID, IDTypeDescription, ValueDisplay) '
DECLARE Csr CURSOR LOCAL FAST_FORWARD FOR
SELECT DISTINCT tpk.[SchemaName], tpk.[TableName], tpk.[PKName], tpk.[DisplayColumnName]
FROM [list].[TablePrimaryKeys] tpk
INNER JOIN [list].[ReportAndQueryVariantDefaultFilterListInt] df ON tpk.ListIDTypeID = df.ListIDTypeID
WHERE df.ReportVariantID = #ReportVariantID
OPEN Csr
FETCH NEXT FROM Csr INTO #MainTblSchema, #MainTblName, #MainTblPKName, #DisplayColumn
WHILE ##fetch_status = 0
BEGIN
-- Quotename is needed if you ever use special characters
-- in table/column names. Spaces, reserved words etc.
-- Other changes add apostrophes at right places.
SET #SQL = CONCAT(#SQL, 'SELECT df.ListIDTypeID, df.PKID, tpk.IDTypeDescription, mt.' + QUOTENAME(#DisplayColumn) + '
FROM [list].[ReportAndQueryVariantDefaultFilterListInt] df
INNER JOIN [list].[TablePrimaryKeys] tpk ON df.ListIDTypeID = tpk.ListIDTypeID
INNER JOIN [' + QUOTENAME(#MainTblSchema) + '].[' + QUOTENAME(#MainTblName) + '] mt ON df.PKID = mt.' + QUOTENAME(#MainTblPKName) + '
WHERE df.ReportVariantID = #ReportVariantID ')
FETCH NEXT FROM Csr INTO #MainTblSchema, #MainTblName, #MainTblPKName, #DisplayColumn
IF ##FETCH_STATUS = 0
BEGIN
SET #SQL = CONCAT(#SQL, 'UNION ')
END
END
EXEC sp_executeSQL #SQL
CLOSE Csr
DEALLOCATE Csr
RETURN
END
Is there any way to accomplish this and return a recordset with a single ADO call?

You can't execute dynamic SQL in functions. But you can with stored procedures.
There are other issues also:
You have extra brackets before and after QUOTENAME (that function will add the brackets anyway.
You need to pass parameters properly in using sp_executesql. Outer parameters and variables do not exist in the dynamic scope automatically, you need to pass them.
Inserting into a table variable is not necessary, you can select straight out of the dynamic code.
You don't need the cursor, you can just use STRING_AGG
CREATE OR ALTER PROCEDURE [dbo].[udf_Reports_DefaultFilterInfoForVariantID]
#ReportVariantID int
AS
SET NOCOUNT, XACT_ABORT ON; -- always use these two
DECLARE #SQL nvarchar(max) = (
-- Quotename is needed if you ever use special characters
-- in table/column names. Spaces, reserved words etc.
-- Other changes add apostrophes at right places.
SELECT STRING_AGG(CAST('
SELECT
df.ListIDTypeID,
df.PKID,
tpk.IDTypeDescription,
mt.' + QUOTENAME(tpk.DisplayColumn) + ' AS ValueDisplay
FROM list.ReportAndQueryVariantDefaultFilterListInt df
INNER JOIN list.TablePrimaryKeys tpk ON df.ListIDTypeID = tpk.ListIDTypeID
INNER JOIN ' + QUOTENAME(tpk.SchemaName) + '.' + QUOTENAME(tpk.TableName) + ' mt ON df.PKID = mt.' + QUOTENAME(tpk.PKName) + '
WHERE df.ReportVariantID = #ReportVariantID
'
AS nvarchar(max)), 'UNION ALL ')
FROM list.TablePrimaryKeys] tpk
INNER JOIN list.ReportAndQueryVariantDefaultFilterListInt df ON tpk.ListIDTypeID = df.ListIDTypeID
WHERE df.ReportVariantID = #ReportVariantID
GROUP BY
tpk.SchemaName,
tpk.TableName,
tpk.PKName,
tpk.DisplayColumnName
);
PRINT #SQL; -- your friend;
EXEC sp_executesql #SQL,
N'#ReportVariantID int',
#ReportVariantID = #ReportVariantID;
There is a more efficient way of doing the dynamic query. You can union all the dynamic tables together first, then join ReportAndQueryVariantDefaultFilterListInt etc afterwards.
CREATE OR ALTER PROCEDURE [dbo].[udf_Reports_DefaultFilterInfoForVariantID]
#ReportVariantID int
AS
SET NOCOUNT, XACT_ABORT ON; -- always use these two
DECLARE #SQL nvarchar(max) = '
SELECT
df.ListIDTypeID,
df.PKID,
tpk.IDTypeDescription,
mt.ValueDisplay
FROM list.ReportAndQueryVariantDefaultFilterListInt df
INNER JOIN list.TablePrimaryKeys tpk ON df.ListIDTypeID = tpk.ListIDTypeID
INNER JOIN (
' + (
-- Quotename is needed if you ever use special characters
-- in table/column names. Spaces, reserved words etc.
-- Other changes add apostrophes at right places.
SELECT STRING_AGG(CAST('
SELECT mt.' + QUOTENAME(tpk.PKName) + ', mt.' + QUOTENAME(tpk.DisplayColumn) + '
FROM ' + QUOTENAME(tpk.SchemaName) + '.' + QUOTENAME(tpk.TableName) + ' mt
'
AS nvarchar(max)), 'UNION ALL ')
FROM list.TablePrimaryKeys] tpk
INNER JOIN list.ReportAndQueryVariantDefaultFilterListInt df ON tpk.ListIDTypeID = df.ListIDTypeID
WHERE df.ReportVariantID = #ReportVariantID
GROUP BY
tpk.SchemaName,
tpk.TableName,
tpk.PKName,
tpk.DisplayColumnName
) + '
) AS mt(PK, ValueDisplay) ON df.PKID = mt.PK
WHERE df.ReportVariantID = #ReportVariantID
';
PRINT #SQL; -- your friend;
EXEC sp_executesql #SQL,
N'#ReportVariantID int',
#ReportVariantID = #ReportVariantID;

This is just an FYI, in case it helps others. I'd marked Charlieface's answer as the best (thanks, Charlieface!), and it worked perfectly... as long as there was only one record returned in tpk. But if there was more than one type of primary key that needed to be included in the results, it gave a "subquery returned more than 1 value. this is not permitted" error. After some trial and error, here was the modified working code for setting up the value of #SQL (I'd changed a couple of table names in the interim, so the tables aren't exactly the same):
SET #SQL = ( SELECT STRING_AGG(CAST('
SELECT
df.ListIDTypeID,
df.PKID,
tpk.IDTypeDescription,
mt.' + QUOTENAME(tpk.DisplayColumnName) + ' AS ValueDisplay
FROM dbo.ReportAndQueryVariantFilterListInt df
INNER JOIN list.TablePrimaryKeys tpk ON df.ListIDTypeID = tpk.ListIDTypeID
INNER JOIN ' + QUOTENAME(tpk.SchemaName) + '.' + QUOTENAME(tpk.TableName) + ' mt ON df.PKID = mt.' + QUOTENAME(tpk.PKName) + '
WHERE df.ReportVariantID = #ReportVariantID AND df.ListIDTypeID = ' + CAST(df.ListIDTypeID AS nvarchar(10)) + '
'
AS nvarchar(max)), 'UNION ALL ')
FROM list.[TablePrimaryKeys] tpk
INNER JOIN (SELECT DISTINCT ListIDTypeID FROM dbo.ReportAndQueryVariantFilterListInt WHERE ReportVariantID = #ReportVariantID) df ON tpk.ListIDTypeID = df.ListIDTypeID
);

Related

Looping through all columns in a table

I'm trying to find a way of evaluating all fields in many tables and return % of the data filled in. i need to look for specific things rather than just NULL.
So for instance in the client table, it would return all fields and say Client Name 45% completed,
Address 90% Completed etc.
I need to search on NULL, Blank, UNCODED, -1 vs row count
some of the tables have more than 30+ fields hence why I think a loop my be best.
select
cast(100 - ((select cast(count([ClientName])as decimal(10,2))
from [dbo].[Client]
where
[ClientName] is null
or [ClientName] = ''
or [ClientName] = 'UNCODED'
or [ClientName] = -1
)
/
(select cast(count([ClientName])as decimal(10,2))
from [dbo].[Client]
where
[ClientName] is not null
or [ClientName] <> ''
or [ClientName] <> 'UNCODED'
or [ClientName] <> -1
))as decimal(10,2)) as '%Completed'
The below gets me the table names
select
c.column_id,c.name
from sys.columns c
inner join sys.objects o on c.object_id=o.object_id
where o.name = 'Client'
order by c.column_id
I'm new to SQL and trying to get my head round variables and loops but just not getting it.
You can build the dynamic SQL query you need using simple string concatenation from sys.columns, just don't try to add an ORDER BY as that behavior is undefined and in many cases will leave out rows inexplicably.
-- given these variables/parameters:
DECLARE #c int, #t nvarchar(511) = N'dbo.Client';
-- only proceed if this is actually an object (some protection from SQL injection):
IF OBJECT_ID(#t) IS NOT NULL
BEGIN
-- we can get the row count from metadata instead of scanning the table an extra time:
SELECT #c = SUM(rows) FROM sys.partitions
WHERE [object_id] = OBJECT_ID(#t) AND index_id IN (0,1);
DECLARE #sql nvarchar(max) = N'SELECT [table] = N''' + #t + N'''',
#col nvarchar(max) = N',' + CHAR(13) + CHAR(10) + N'[% $c$ complete] = '
+ N'CONVERT(decimal(5,2), 100.0*SUM(CASE WHEN $qc$ IS NULL '
+ N'OR $qc$ = SPACE(0) OR RTRIM($qc$) IN (''UNCODED'',''-1'') '
+ N'THEN 0 ELSE 1 END)/#c)';
SELECT #sql += REPLACE(REPLACE(#col, N'$c$', name), N'$qc$', QUOTENAME(name))
FROM sys.columns WHERE [object_id] = OBJECT_ID(#t);
SELECT #sql += N' FROM #t;';
SELECT #sql = REPLACE(#sql, N'#t', #t);
PRINT #sql;
--EXECUTE sys.sp_executesql #stmt = #sql, #params = N'#c int', #c = #c;
END
When you are satisfied the output looks like you expect, uncomment the EXECUTE. Note that you may have to filter out certain data types (I don't know what happens with XML, binary, or types like hierarchyid or geography, and don't have the energy to test those right now).

Update with dynamic tables

I have to write update using dynamic sql becaus i know only name of column that I want to update and names of columns which I will use to join tables in my update. But I don't know the numbers of tables and names. Names of tables I will get in parameter of my procedure in this way
declare #Tables = N'Customer,Employee,Owner'
So I want to have update like this:
update t
set [Status] = 100
from
TemporaryTable t
left join Customer t1 on t1.RecordId = t.RecordId
left join Employee t2 on t2.RecordId = t.RecordId
left join Owner t3 on t3.RecordId =t.RecordId
where
t1.RecordId is null
and t2.RecordId is NULL
and t3.RecordId is null
I know that each table will have column RecordId and want to left join this tables to my TemporaryTable on this column but I don't know the names and numbers of tables. For example I will have one, two, or ten tables with different names. I know that this tables names will be save in parameter #Tables in that way:
#Tables = N'Customer,Employee,Owner'
There is possilble to write this update in dynamic way?
This is an answer, which helps ... to write update using dynamic sql ... and only shows how to generate a dynamic statement. It's based on string splitting. From SQL Server 2016+ you may use STRING_SPLIT() (because here the order of the substrings is not important). For previous versions you need to find a string splitting function.
T-SQL:
DECLARE #Tables nvarchar(max) = N'Customer,Employee,Owner'
DECLARE #join nvarchar(max) = N''
DECLARE #where nvarchar(max) = N''
DECLARE #stm nvarchar(max) = N''
SELECT
#join = #join + CONCAT(
N' LEFT JOIN ',
QUOTENAME(s.[value]),
N' t',
ROW_NUMBER() OVER (ORDER BY (SELECT 1)),
N' ON t',
ROW_NUMBER() OVER (ORDER BY (SELECT 1)),
N'.RecordId = t.RecordId'
),
#where = #where + CONCAT(
N' AND t',
ROW_NUMBER() OVER (ORDER BY (SELECT 1)),
N'.RecordId is NULL'
)
FROM STRING_SPLIT(#Tables, N',') s
SET #stm = CONCAT(
N'UPDATE t SET [Status] = 100 ',
N'FROM TemporaryTable t',
#join,
N' WHERE ',
STUFF(#where, 1, 5, N'')
)
PRINT #stm
EXEC sp_executesql #stm
Notes:
One note, that I think is important - consider passing tables names using table value type for parameter, not as comma-separated text.
It seems like this will suit your needs, though I don't fully understand what you're trying to do. Here we're constructing the final SQL in two pieces (#s and #where) and then concatenating into the final SQL at the end.
declare #Tables varchar(100) = N'Customer,Employee,Owner'
declare #tablenames table (tablename nvarchar(100))
insert #tablenames (tablename)
select value
from string_split(#Tables, ',');
declare #where varchar(max) = ''
declare #s varchar(max) = '
update t
set [Status] = 100
from TemporaryTable t'
select #s += '
left join ' + tablename + ' on ' + tablename + '.RecordId = t.RecordId'
, #where += case when #where = '' then '' else ' and ' end + tablename + '.RecordId is null
'
from #tablenames
print #s + char(13) + ' where ' + #where
exec( #s + char(13) + ' where ' + #where)

Get Dynamic Pivot Result into temp table SQL Server

I want to get dynamic pivot result into temp table by which I can send it back to C# code using select query.
DECLARE #partymasterid bigint = 2;
DECLARE #cols AS NVARCHAR(MAX);
DECLARE #query AS NVARCHAR(MAX);
SELECT
#cols = STUFF((SELECT DISTINCT '[' + CAST(dbo.InventoryProductMaster.Name AS nvarchar(max)) + '],'
FROM dbo.InventoryBiltyMaster
INNER JOIN dbo.InventoryPartyProductPriceMaster ON dbo.InventoryBiltyMaster.InventoryPartyProductPriceMasterID = dbo.InventoryPartyProductPriceMaster.ID
INNER JOIN dbo.InventoryProductMaster ON dbo.InventoryPartyProductPriceMaster.InventoryProductMasterID = dbo.InventoryProductMaster.ID
WHERE dbo.InventoryBiltyMaster.PartyMasterID = #partymasterid
FOR XML PATH(''), TYPE).value('.', 'NVARCHAR(MAX)'), 1, 0, '');
SELECT #query =
'
SELECT
*
FROM
(SELECT
Count(dbo.InventoryBiltyMaster.ID) AS BiltyCount,
--dbo.InventoryBiltyMaster.InventoryProductMasterID,
--dbo.InventoryBiltyMaster.VehicleMasterID,
dbo.InventoryProductMaster.Name,
dbo.VehicleMaster.VehicleNumber
FROM
dbo.InventoryBiltyMaster
INNER JOIN dbo.InventoryPartyProductPriceMaster ON dbo.InventoryBiltyMaster.InventoryPartyProductPriceMasterID = dbo.InventoryPartyProductPriceMaster.ID
INNER JOIN dbo.InventoryProductMaster ON dbo.InventoryPartyProductPriceMaster.InventoryProductMasterID = dbo.InventoryProductMaster.ID
INNER JOIN dbo.VehicleMaster ON dbo.InventoryBiltyMaster.VehicleMasterID = dbo.VehicleMaster.ID
WHERE
dbo.InventoryBiltyMaster.PartyMasterID = ' + CAST(#partymasterid as nvarchar(50)) + '
GROUP BY
dbo.InventoryBiltyMaster.InventoryProductMasterID,
dbo.InventoryProductMaster.Name,
dbo.InventoryBiltyMaster.VehicleMasterID,
dbo.VehicleMaster.VehicleNumber
)
AS S
PIVOT
(
MAX(BiltyCount)
FOR [Name] IN (' + LEFT(#cols, LEN(#cols)-1) + ')
)AS pvt';
EXEC SP_EXECUTESQL #query
Here may be result have 3 column or 5 columns or 10-15 columns. it depends on the result of query. and this result i want on temp table for use more this data.
I want to use it in a stored procedure to send back result into ASP.NET MVC 5 with Entity Framework
You can't store inside a # (temp table) without defining it in the parent scope when using sp_executesql. In your case you have a dynamic pivot and you don't know what and how many columns are going to be there.
sp_executesql runs in a different session (sp_executesql creates its own session) and temp tables are session specific.
For your scenario you can use ## (global temp table). You can change your query like following.
SELECT #query =
'
SELECT
* into ##temp
FROM
(SELECT
Count(dbo.InventoryBiltyMaster.ID) AS BiltyCount,
--dbo.InventoryBiltyMaster.InventoryProductMasterID,
--dbo.InventoryBiltyMaster.VehicleMasterID,
dbo.InventoryProductMaster.Name,
dbo.VehicleMaster.VehicleNumber
FROM
dbo.InventoryBiltyMaster
INNER JOIN dbo.InventoryPartyProductPriceMaster ON dbo.InventoryBiltyMaster.InventoryPartyProductPriceMasterID = dbo.InventoryPartyProductPriceMaster.ID
INNER JOIN dbo.InventoryProductMaster ON dbo.InventoryPartyProductPriceMaster.InventoryProductMasterID = dbo.InventoryProductMaster.ID
INNER JOIN dbo.VehicleMaster ON dbo.InventoryBiltyMaster.VehicleMasterID = dbo.VehicleMaster.ID
WHERE
dbo.InventoryBiltyMaster.PartyMasterID = ' + CAST(#partymasterid as nvarchar(50)) + '
GROUP BY
dbo.InventoryBiltyMaster.InventoryProductMasterID,
dbo.InventoryProductMaster.Name,
dbo.InventoryBiltyMaster.VehicleMasterID,
dbo.VehicleMaster.VehicleNumber
)
AS S
PIVOT
(
MAX(BiltyCount)
FOR [Name] IN (' + LEFT(#cols, LEN(#cols)-1) + ')
)AS pvt';
EXEC SP_EXECUTESQL #query
--now you can use ##temp
Note: use of global temp table can lead to unpredictable behavior if it gets updated from multiple sessions, you may think of giving a unique name for each session.

Dynamic SQL with table name from a field

Is there a way to generate dynamic sql statement, such that the field value from one table participates in a table name being joined?
Here is what I am trying to do:
DECLARE #sql NVARCHAR(MAX) = N''
DECLARE #MainProc NVARCHAR(MAX) = N'dbo.MainProc'
SET #sql += N'SELECT ref.*
FROM sys.dm_sql_referenced_entities (' + #MainProc + N', ''OBJECT'') AS ref
INNER JOIN ' + ref.referenced_database_name + '.sys.objects AS o ON ref.referenced_id = o.object_id AND o.type = ''P''
WHERE ref.referenced_schema_name NOT IN (''system'')'
EXEC sp_executesql #sql
How can the value of ref.referenced_database_name be attached to sys.objects?
Yes with a little refactoring. In your original example you were trying to extract a value from dm_sql_referenced_entities before you had queried it.
DECLARE #sql NVARCHAR(MAX) = N''
DECLARE #MainProc NVARCHAR(MAX) = N'Fact.Account_Load'
/* Builds the sys.Objects query.
* Can return multiple queries.
*/
SELECT
#sql +=
'
SELECT
s.*
FROM
' + QUOTENAME(ref.referenced_database_name) + '.sys.objects AS s
WHERE
s.Object_Id = ' + CAST(ref.referenced_id AS VARCHAR(255)) + '
AND s.Type = ''P''
;
'
FROM
sys.dm_sql_referenced_entities('Fact.Account_Load', 'OBJECT') AS ref
WHERE
ref.referenced_database_name IS NOT NULL
GROUP BY
ref.referenced_database_name,
ref.referenced_id
;
-- Executes dynamic query.
EXECUTE(#sql);

Query a stored procedure that exec's a dynamic query. Error Linked server indicates object has no columns

I have a stored procedure that dynamically creates a pivot query.
The procedure works and returns the correct data.
Now I have a requirement to show this data in a reporting system that can only pull from a table or view. Since you can not create a dynamic query in a view I tried to do a select from using openquery.
Example '
Select * from OpenQuery([MyServername], 'Exec
Instance.Schema.StoredProcedure')
I get the error back
"the linked server indicates the object has no columns".
I assume this is because of the first select statement that is stuffing the variable with column names.
CODE FROM PROCEDURE
ALTER PROCEDURE [dbo].[FuelCombustorMatrix]
AS
BEGIN
-- Use Master
SET NOCOUNT ON
Declare #cols nvarchar(2000),
#Tcols nvarchar(2000),
#Sql nvarchar (max)
select #cols = stuff ((
Select distinct '], ['+ModelName + ' ' + CombustorName
from CombustorFuel cf
join Model m on cf.modelid = m.modelid
join Combustors cb on cf.CombustorID = cb.CombustorID
where cf.CombustorID > 0
for XML Path('')
),1,2,'')+']'
Set #Tcols = replace(#Cols, ']', '] int')
--Print #Tcols
--Print #Cols
Set #Sql = 'Select GasLiquid, FuelType, '+ #Cols +'
from
(
Select GasLiquid, FuelType, ModelName+ '' '' +CombustorName ModelCombustor, CombFuelStatus+''- ''+CombFuelNote CombFuelStatusNote
from Frames f
join Family fa on f.Frameid = fa.frameid
join Model m on fa.FamilyID = m.FamilyID
join CombustorFuel cf on m.Modelid = cf.modelid
Join Combustors c on cf.CombustorId = c.CombustorID
join FuelTypes ft on cf.FuelTypeID = ft.FuelTypeID
where cf.CombustorFuelID > 0
and CombustorName <> ''''
) up
Pivot
(max(CombFuelStatusNote) for ModelCombustor in ('+ #Cols +')) as pvt
order by FuelType'
exec (#Sql)
END

Resources