Introduction
Many time you have to use Dynamic SQL in your Stored Procedure but some tools (e.g. Tableau) may reject it because metadata is not returned when sp_describe_first_result_set is called in SQL Prepare phase. You may see error like below in that case.
Metadata Prepare Call
1 |
exec sp_describe_first_result_set N'EXEC [dbo].[usp_GetInvoicesByCountry] @country=''Germany''' |
Error due to Dynamic SQL
Msg 11514, Level 16, State 1, Procedure sp_describe_first_result_set, Line 1 [Batch Start Line 29]
The metadata could not be determined because statement 'EXECUTE (@sqlFull)' in procedure 'usp_GetInvoicesByCountry' contains dynamic SQL. Consider using the WITH RESULT SETS clause to explicitly describe the result set.
Here is example Stored Procedure which may throw above error. We will re-write same stored proc in later section.
Example of Dynamic SQL in Stored Procedure
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
--DROP PROC dbo.usp_GetInvoicesByCountry --GO /* Purpose: Parameterize REST API call via SQL. Call ZappySys Drivers inside SQL Server. */ CREATE PROC dbo.usp_GetInvoicesByCountry @country varchar(100) AS DECLARE @sql varchar(max) --//Escape single ticks carefully SET @sql = 'SELECT OrderID,CustomerID,Country,Quantity FROM $ WITH (Src=''https://services.odata.org/V3/Northwind/Northwind.svc/Invoices?$format=json&filter=Country eq '+ @country +''' ,Filter=''$.value[*]'' ,DataFormat=''OData'' )' DECLARE @sqlFull varchar(max) SET @sqlFull='SELECT * FROM OPENQUERY( LS , ''' + REPLACE( @sql, '''', '''''' ) + ''' )' PRINT @sqlFull --//For DEBUG purpose EXECUTE (@sqlFull) GO -- Example call EXEC dbo.usp_GetInvoicesByCountry @country='Germany' |
How to Fix Metadata error for Dynamic SQL
To fix metadata error due to dynamic SQL you have add WITH RESULT SETS statement after EXECUTE call. This feature was added in SQL 2012 and later. There are two ways to describe Stored Procedure / using Dynamic SQL
Method-1
Add WITH RESULT SETS clause inside Stored Proc if you are using EXECUTE or sp_Executesql like below.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 |
--DROP PROC dbo.usp_GetInvoicesByCountry --GO /* Purpose: Parameterize REST API call via SQL. Call ZappySys Drivers inside SQL Server. */ ALTER PROC dbo.usp_GetInvoicesByCountry @country varchar(100) AS DECLARE @sql varchar(max) --//Escape single ticks carefully SET @sql = 'SELECT OrderID,CustomerID,Country,Quantity FROM $ WITH (Src=''https://services.odata.org/V3/Northwind/Northwind.svc/Invoices?$format=json&filter=Country eq '+ @country +''' ,Filter=''$.value[*]'' ,DataFormat=''OData'' )' DECLARE @sqlFull varchar(max) SET @sqlFull='SELECT * FROM OPENQUERY( LS , ''' + REPLACE( @sql, '''', '''''' ) + ''' )' PRINT @sqlFull --//For DEBUG purpose EXECUTE (@sqlFull) WITH RESULT SETS ( (OrderID int,CustomerID varchar(100),Country varchar(100),Quantity int) --//describe first result ) GO |
Method-2
Another way to describe resultset is add WITH RESULT SETS statement right after you call Stored proc each time from any client tool (see below). This way you don’t have to touch Source stored Proc. First method is still preferred way.
1 |
EXEC [dbo].[usp_GetInvoicesByCountry] @country='Germany' WITH RESULT SETS ( (OrderID int,CustomerID varchar(100),Country varchar(100),Quantity int)) |
How to generate column names and field datatypes for WITH RESULT SETS
If you have many columns to describe then it becomes tedious to type 200 columns by hand. Lets see how to make it simple.
- First step is you need to load your result into some static table.
1SELECT * INTO _tmp FROM ( some sub query ) t - Once you have data we can use it few ways to get datatypes. Lets create a view to query metadata
123456789101112131415161718192021222324252627282930313233CREATE VIEW dbo.vwDataTypesASSELECTc.name as ColumnName,t.name +CASE WHEN t.name IN ('char', 'varchar','nchar','nvarchar') THEN '('+CASE WHEN c.max_length=-1 THEN 'MAX'ELSE CONVERT(VARCHAR(4),CASE WHEN t.name IN ('nchar','nvarchar')THEN c.max_length/2 ELSE c.max_length END )END +')'WHEN t.name IN ('decimal','numeric')THEN '('+ CONVERT(VARCHAR(4),c.precision)+','+ CONVERT(VARCHAR(4),c.Scale)+')'ELSE '' ENDas FriendlyDataType,t.name as DataType,c.[object_id] as [TblObjectId],c.max_length MaxLengthBytes,c.precision as [Precision],c.scale as [Scale],c.is_nullable as IsNullable,ISNULL(i.is_primary_key, 0) as IsPrimaryKeyFROMsys.columns cINNER JOINsys.types t ON c.user_type_id = t.user_type_idLEFT OUTER JOINsys.index_columns ic ON ic.object_id = c.object_id AND ic.column_id = c.column_idLEFT OUTER JOINsys.indexes i ON ic.object_id = i.object_id AND ic.index_id = i.index_id--WHERE c.object_id = OBJECT_ID('_tmp')go - Now you can write below query to generate Comma separated list of columns names and datatypes
1234DECLARE @csv VARCHAR(MAX)SELECT @csv = COALESCE(@csv+', ' ,'') + '[' + ColumnName + '] ' + FriendlyDataTypeFROM vwDataTypes WHERE [TblObjectId]= OBJECT_ID('_tmp')SELECT @csv
1[CustomerID] nvarchar(10), [CustomerName] nvarchar(68), ......... [Freight] nvarchar(16) - Use result of above query and paste in WITH RESULT SETS as below
12345EXECUTE( @sqlFull )WITH RESULT SETS(( your-comma-separated-list-goes-here ))