Wednesday, 8 July 2015

MSSQL - Stored procedure to replace placeholders in texts (dynamic SQL)

It is often necessary to make the query text from constant pieces and variables.

For example we have a many tables with such structure:
  1. (
  2. ID INT IDENTITY(1,1) PRIMARY KEY,
  3. Number INT,
  4. ChangeDate DATETIME
  5. )
and we need dynamically, depending on variables with table name, [Number] min value and [ChangeDate] min value send queries to one or other table.
We can do it using Dynamic SQL method, for example so:

  1. CREATE PROCEDURE MySP
  2. @ObjectName SYSNAME,
  3. @MinNumber INT,
  4. @ValidFrom DATETIME
  5. AS BEGIN
  6. DECLARE @SQL NVarChar(MAX);
  7.  
  8. SET @SQL = '
  9. SELECT
  10. *
  11. FROM
  12. ' + @ObjectName + '
  13. WHERE
  14. Number >= ' + CAST(@MinNumber AS VARCHAR(20)) + '
  15. AND
  16. ChangeDate >= CONVERT(DATETIME, '''
  17. + CONVERT(VARCHAR(30), @ValidFrom, 121) + ''', 121);';
  18.  
  19. PRINT @SQL;
  20.  
  21. EXEC sp_executesql @SQL;
  22. END;
  23. GO
  24.  
  25. EXEC MySP 'MyTableNr1', 123, '2015-01-01 12:00';

It works, but SQL-Text format is not readable.
Much more readable looks the text converted to a template with placeholders:

  1. '
  2. SELECT
  3. *
  4. FROM
  5. {ObjectName}
  6. WHERE
  7. Number >= {MinNumber}
  8. AND
  9. ChangeDate >= CONVERT(DATETIME, ''{ValidFrom}'';
  10. '

In this case our example stored procedure will look like this:

  1. CREATE PROCEDURE MySP
  2. @ObjectName SYSNAME,
  3. @MinNumber INT,
  4. @ValidFrom DATETIME
  5. AS BEGIN
  6. DECLARE @SQL NVarChar(MAX);
  7.  
  8. SET @SQL = '
  9. SELECT
  10. *
  11. FROM
  12. {ObjectName}
  13. WHERE
  14. Number >= {MinNumber}
  15. AND
  16. ChangeDate >= CONVERT(DATETIME, ''{ValidFrom}'';'
  17.  
  18. SET @SQL = REPLACE(@SQL, '{ObjectName}', @ObjectName);
  19. SET @SQL = REPLACE(@SQL, '{MinNumber}', CAST(@MinNumber AS VARCHAR(20)));
  20. SET @SQL = REPLACE(@SQL, '{ValidFrom}', CONVERT(VARCHAR(30), @ValidFrom, 121));
  21.  
  22. PRINT @SQL;
  23.  
  24. EXEC sp_executesql @SQL;
  25. END;

It is good for so simple example, but if we will have more dynamical parameters and inside one stored procedure (or plain query) more then one template, then this query will have too many redundant call like this:
  1. SET @SQL = REPLACE(@SQL, ..., ...);

The solution is to create some help procedure which will take text template and dictionary with placeholder name:value pairs as parameters.
Stored procedure can not take table-value parameters directly.
Table-valued parameters have to be declared by using user-defined table types.
In our case:
  1. CREATE TYPE PlaceholderMappingType AS TABLE (
  2. Placeholder NVARCHAR(100),
  3. Value NVARCHAR(MAX)
  4. );

The complete script with usage example:
  1. USE tools;
  2. GO
  3. IF OBJECT_ID('tools.dbo.sp_ReplacePlaceholders') IS NOT NULL BEGIN
  4. PRINT '-- DROP PROCEDURE dbo.sp_ReplacePlaceholders';
  5. DROP PROCEDURE dbo.sp_ReplacePlaceholders;
  6. END;
  7. GO
  8. IF EXISTS (
  9. SELECT *
  10. FROM tools.INFORMATION_SCHEMA.DOMAINS
  11. WHERE DOMAIN_NAME = 'PlaceholderMappingType'
  12. ) BEGIN
  13. PRINT '-- DROP TYPE dbo.PlaceholderMappingType';
  14. DROP TYPE dbo.PlaceholderMappingType;
  15. END;
  16. GO
  17. PRINT '-- CREATE TYPE dbo.PlaceholderMappingType';
  18. -- TABLE-Domain (User Defined Type) for placeholders dictionary
  19. CREATE TYPE PlaceholderMappingType AS TABLE (
  20. Placeholder NVARCHAR(100),
  21. Value NVARCHAR(MAX)
  22. );
  23. GO
  24. CREATE PROCEDURE dbo.sp_ReplacePlaceholders (
  25. @Text NVARCHAR(MAX) OUTPUT,
  26. -- TABLE-Domain (User Defined Type) for placeholders dictionary
  27. @PlaceholderMappings PlaceholderMappingType READONLY,
  28. -- Flag to replace in placeholder values each single apostrof with double apostrof
  29. @EscapeApostrofs BIT = 1,
  30. -- Text to TrimLeft in each text line
  31. @IndentString VARCHAR(MAX) = '',
  32. -- Flag to PRINT additional diagnostic information
  33. @Debug BIT = 0
  34. )
  35. AS BEGIN
  36. SET NOCOUNT ON;
  37.  
  38. IF (@Debug = 1)
  39. PRINT '-- EXEC dbo.sp_ReplacePlaceholders'
  40.  
  41. DECLARE
  42. @CRLF CHAR(2) = CHAR(13)+CHAR(10),
  43. @Indent_CharIndex INT,
  44. @I INT = 0,
  45. @Char CHAR(1),
  46. @Indent VARCHAR(MAX) = '';
  47.  
  48. -- Trim rows left
  49. IF (CHARINDEX(@IndentString, @Text, 0) = 1)
  50. SET @Text = STUFF(@Text, 1, LEN(@IndentString), '');
  51. SET @Text = REPLACE(@Text, @CRLF + @IndentString, @CRLF);
  52.  
  53. -- Declare FOREACH(placeholderRow IN @PlaceholderMappings) cursor
  54. DECLARE
  55. @Placeholder NVARCHAR(102),
  56. @Value NVARCHAR(MAX);
  57. DECLARE Placeholders_Cursor CURSOR
  58. LOCAL STATIC READ_ONLY FORWARD_ONLY
  59. FOR
  60. SELECT
  61. Placeholder = '{' + REPLACE(REPLACE(Placeholder, '{', ''), '}', '') + '}',
  62. Value = CASE @EscapeApostrofs
  63. WHEN 1
  64. THEN REPLACE(Value, '''', '''''')
  65. ELSE Value
  66. END
  67. FROM @PlaceholderMappings;
  68.  
  69. OPEN Placeholders_Cursor;
  70.  
  71. FETCH NEXT FROM Placeholders_Cursor INTO @Placeholder, @Value;
  72. WHILE @@FETCH_STATUS = 0 BEGIN
  73. IF (@Debug = 1) AND (CHARINDEX(@Placeholder, @Text) = 0)
  74. PRINT '-- ==> Could not find Placeholder ' + @Placeholder;
  75. SET @Text = REPLACE(@Text, @Placeholder, @Value);
  76.  
  77. FETCH NEXT FROM Placeholders_Cursor INTO @Placeholder, @Value;
  78. END;
  79.  
  80. CLOSE Placeholders_Cursor
  81. DEALLOCATE Placeholders_Cursor
  82.  
  83. RETURN;
  84. END;
  85. GO
  86. DECLARE
  87. @SQL NVARCHAR(MAX),
  88. @PlaceholderMappings PlaceholderMappingType;
  89.  
  90. SET @SQL = '
  91. SELECT
  92. F1=''{aaa}'',
  93. F2=''{bbb}'',
  94. F3=''{ddd}'';';
  95. INSERT INTO @PlaceholderMappings
  96. VALUES
  97. ('aaa', 'A''A''A'),
  98. ('{bbb}', 'BBB'),
  99. ('ccc', 'CCC');
  100.  
  101. DECLARE @IndentString VARCHAR(MAX) = SPACE(8);
  102. EXEC dbo.sp_ReplacePlaceholders
  103. @SQL OUTPUT,
  104. @PlaceholderMappings,
  105. @IndentString=@IndentString,
  106. @Debug=1;
  107. PRINT '>>>' + @SQL + '<<<';

No comments:

Post a Comment

Note: only a member of this blog may post a comment.