Thursday, February 20, 2020

Grappling with sp_describe_undeclared_parameters(). The Hows, Whys and Wherefores.

sp_describe_undeclared_parameters() promises a great deal, but delivers less. It was added mostly for executing application queries via sp_prepare using a TDS link, but has several quirks and restrictions that rather spoils its more general use.

It is designed to provide you with all the parameters of a batch. You might use it when a SQL statement could contain parameters and it has to be somehow executed by eliciting the intended values. One can imagine it being used for a rather clunky user-interface.

It would be nice to have a utility that will do the @Params parameter for the sp_executeSQL system procedure when you are doing a lot of routine work but there are problems. It introduce restrictions, both intentionally and accidentally to such an extent that its use is liable to be restrictive

sp_describe_undeclared_parameters returns an error if …

  • You use an input or output variable twice in a query
  • You try to use table variables
  • You use the wrong case for the parameter of a procedure called within the batch
  • the statement references temporary tables.
  • Any expression has two arguments without data types
  • The query creates a permanent table that is then used
  • It cannot successfully parse the input @tsql
  • If you supply a known parameter that is not syntactically valid, or you declare any parameter more than one time.
  • the input batch declares a local variable of the same name as a parameter that you supply in @params.

If you try to use an input or output variable twice in a query, it won’t work.

sp_describe_undeclared_parameters N'
SELECT parent.object_id, parent.name, child.name
FROM sys.objects parent
  INNER JOIN sys.objects child 
  ON child.parent_object_id = parent.object_id
    AND (child.parent_object_id=@ObjectID
      OR  parent.object_id=@ObjectID)'

This gives you an error

Msg 11508, Level 16, State 1, Line 142
The undeclared parameter '@ObjectID' is used more than once in the batch being analyzed.

This is an odd restriction when one considers that it works fine with sp_executeSQL

sp_executeSQL N'
SELECT parent.object_id, parent.name, child.name
FROM sys.objects parent
  INNER JOIN sys.objects child 
  ON child.parent_object_id = parent.object_id
    AND (child.parent_object_id=@ObjectID
      OR  parent.object_id=@ObjectID)',N'@objectid int',@Objectid=94623380

… which gives …

You can get around the problem of not being allowed to use input values by assigning the input value to a local variable in the batch.

exec sp_describe_undeclared_parameters N'
Declare @ObjectID int=@TheParameter
SELECT parent.object_id, parent.name, child.name
  FROM sys.objects parent
    INNER JOIN sys.objects child 
      ON child.parent_object_id = parent.object_id
             AND (child.parent_object_id=@ObjectID
                 OR  parent.object_id=@ObjectID)'

You can cope with output variables too.

exec sp_describe_undeclared_parameters  N'
Declare @TheDelimiter char(1)=@InputDelimiter
Declare @TheIdentifier sysname=@InputIdentifier
Declare @Return sysname
IF @TheDelimiter NOT IN (''<'',''>'',''['','']'',''"'',''('','')'',''{'',''}'',''`'')
        SET @TheDelimiter=''[''
IF Coalesce(@TheIdentifier, '''') = ''''
    OR CharIndex(NChar(0xFFFF) COLLATE Latin1_General_100_BIN2, @TheIdentifier) <> 0
     Set @RETURN= ''null'';
 IF PatIndex(''%[^a-zA-Z0-9@$#_]%'', @TheIdentifier) > 0
         Set @RETURN = QuoteName(@TheIdentifier,@TheDelimiter);
    ELSE IF PatIndex(''[@#_]%'', @TheIdentifier) > 0
          Set @RETURN= QuoteName(@TheIdentifier,@TheDelimiter);
    ELSE Set @Return=@TheIdentifier
Set @OutputReturn=@Return

… producing …

..and yes, it identified the @OutputReturn as an output parameter too.

Fussy? It all works fine with sp_ExecuteSQL without any need for this.

DECLARE @Delimited sysname
EXECUTE sp_executeSQL N'
IF @TheDelimiter NOT IN (''<'',''>'',''['','']'',''"'',''('','')'',''{'',''}'',''`'')
        SET @TheDelimiter=''[''
IF Coalesce(@TheIdentifier, '''') = ''''
    OR CharIndex(NChar(0xFFFF) COLLATE Latin1_General_100_BIN2, @TheIdentifier) <> 0
     Set @RETURN= ''null'';
    IF PatIndex(''%[^a-zA-Z0-9@$#_]%'', @TheIdentifier) > 0
         Set @RETURN = QuoteName(@TheIdentifier,@TheDelimiter);
    ELSE IF PatIndex(''[@#_]%'', @TheIdentifier) > 0
          Set @RETURN= QuoteName(@TheIdentifier,@TheDelimiter);
    ELSE Set @Return=@TheIdentifier
        ',
        N'@TheDelimiter char(1),@TheIdentifier sysname, @Return sysname OUTPUT ',
        @TheDelimiter=']',@TheIdentifier = 'BusinessEntityContact', @Return =@delimited output 
SELECT @delimited

What if you can live with these restrictions? Here is a stored procedure that can help with a complicated batch with lots of embedded parameters that you need to execute with sp_ExecuteSQL. Just don’t think of using a parameter twice, or any of the other fussy rules! This will work with SQL Server 2017 upwards but it can easily be adapted for earlier versions by changing the string_agg() function for the less-intuitive XML trick. 
If you are using this in SSMS, I’d choose to operate the query pane in TEXT more rather than grid and increase the text length to something reasonable (query->Results to->).

CREATE OR alter PROCEDURE MakeMyBatchExecute @Stmt NVARCHAR(MAX)
/**
Summary: >
  This is a simple utility that takes a SQL batch as a 
  string, complete with embedded parameters and turns it
  into something that can be executed by SP_ExecuteSQL 
  with parameters.
  For example: (I'm not suggestinog you do this)

  Execute sp_ExecuteSQL N'
  Select convert(int,@This), convert(Datetime2,@That)',
  @params=N'@This int, @That datetime2(7)',
  @This=1, @That=''
  
  ... produces ...

  Execute sp_ExecuteSQL N'
  Select convert(int,@This), convert(Datetime2,@That)',
  @params=N'@This int, @That datetime2(7)',
  @This=1, @That=''
  
  If it is a parameter that can be coerced into a string
  it suggests a blank string, otherwise it suggests a number
  that can be filled in. It tells you in the @params what
  it thinks the system type is.
  You may need to help the system it uses for detecting the
  type of parameter you are supplying to the batch. I've shown
  two ways of doing this. CAST works fine too.
Author: Philip Euripedes Factor
Date: 19/02/2020

Examples:
   -  EXECUTE MakeMyBatchExecute N'
      Declare @ObjectID int=@TheParameter
      SELECT parent.object_id, parent.name, child.name
       FROM sys.objects parent
        INNER JOIN sys.objects child 
          ON child.parent_object_id = parent.object_id
               AND (child.parent_object_id=@ObjectID
                   OR  parent.object_id=@ObjectID)'

   -  EXECUTE MakeMyBatchExecute N'
      Select @This+'''',@That+0,@TheOther+'''',@Andanother+0'

   -  EXECUTE MakeMyBatchExecute N'
      Select convert(int,@This), convert(Datetime2,@That)'

   -  EXECUTE MakeMyBatchExecute N'
      Select @MyFirstOutput=convert(int,@This), 
       @MySecondOutput=convert(numeric(8,2),@That)'

Returns: >
  a single column result
**/
AS
DECLARE @Params NVARCHAR(4000);
DECLARE @Tail NVARCHAR(MAX);

DECLARE @temptable TABLE
  (
  parameter_ordinal INT,
  name NVARCHAR(128),
  suggested_system_type_id INT,
  suggested_system_type_name NVARCHAR(128),
  suggested_max_length SMALLINT,
  suggested_precision TINYINT,
  suggested_scale TINYINT,
  suggested_user_type_id INT,
  suggested_user_type_database NVARCHAR(128),
  suggested_user_type_schema NVARCHAR(128),
  suggested_user_type_name NVARCHAR(128),
  suggested_assembly_qualified_type_name NVARCHAR(4000),
  suggested_xml_collection_id INT,
  suggested_xml_collection_database NVARCHAR(128),
  suggested_xml_collection_schema NVARCHAR(128),
  suggested_xml_collection_name NVARCHAR(128),
  suggested_is_xml_document BIT,
  suggested_is_case_sensitive BIT,
  suggested_is_fixed_length_clr_type BIT,
  suggested_is_input BIT,
  suggested_is_output BIT,
  formal_parameter_name NVARCHAR(128),
  suggested_tds_type_id INT,
  suggested_tds_length INT
  );
INSERT INTO @temptable EXEC sp_describe_undeclared_parameters @Stmt;
SELECT 
  @Params =
    String_Agg(
      name + ' ' + suggested_system_type_name + 
         CASE WHEN suggested_is_output = 1 THEN ' OUTPUT' ELSE '' END,
      ', '
      ) WITHIN GROUP(ORDER BY parameter_ordinal ASC),
  @Tail =
    String_Agg(
      name + '=' +
          CASE 
            WHEN suggested_is_output = 1 
                  THEN '@OutputVariable' + Convert(VARCHAR(3), parameter_ordinal) + ' OUTPUT'
        WHEN suggested_system_type_id IN (48,52,56,59,60,62,104,106,108,122,127) 
                  /*a number*/ 
                  THEN '1' 
            ELSE '''''' END,
       ', ' 
           ) WITHIN GROUP(ORDER BY parameter_ordinal ASC)
  FROM @temptable;
SELECT N'Execute sp_ExecuteSQL N''' + Replace(@Stmt, '''', '''''')
       + ''',
  @params=N''' + Replace(@Params, '''', '''''') + ''',
  '    + @Tail AS [executable];
GO

If, for example, we execute this

EXECUTE MakeMyBatchExecute N'
      Declare @ObjectID int=@TheParameter
      SELECT parent.object_id, parent.name, child.name
       FROM sys.objects parent
        INNER JOIN sys.objects child 
          ON child.parent_object_id = parent.object_id
               AND (child.parent_object_id=@ObjectID
                   OR  parent.object_id=@ObjectID)'

We get this as a result…

Execute sp_ExecuteSQL N'
      Declare @ObjectID int=@TheParameter
      SELECT parent.object_id, parent.name, child.name
       FROM sys.objects parent
        INNER JOIN sys.objects child 
          ON child.parent_object_id = parent.object_id
               AND (child.parent_object_id=@ObjectID
                   OR  parent.object_id=@ObjectID)',
  @params=N'@TheParameter int',
  @TheParameter=1

All we need to do now is to paste it from the results into the code pane, change the value of the parameter to something reasonable and bang the button

Execute sp_ExecuteSQL N'
Declare @ObjectID int=@TheParameter
SELECT parent.object_id, parent.name, child.name
  FROM sys.objects parent
    INNER JOIN sys.objects child 
      ON child.parent_object_id = parent.object_id
             AND (child.parent_object_id=@ObjectID
                 OR  parent.object_id=@ObjectID)',
  @params=N'@TheParameter int',
  @TheParameter=94623380

And it all then works fine

Summary

We have quite a useful utility procedure here for dealing with the chore of writing the code for executing batches with several parameters using sp_ExecuteSQL. It could be just my own clumsiness, but I never quite get it right first time without it. I have to warn you though that there are a few restrictions that can be rather irritating unless you know the fixes.

The post Grappling with sp_describe_undeclared_parameters(). The Hows, Whys and Wherefores. appeared first on Simple Talk.



from Simple Talk https://ift.tt/2P80k0e
via

No comments:

Post a Comment