Within T-SQL, commands like EXEC
and SP_EXECUTESQL
allow us to execute text strings that contain code. I would like to run a validation across text strings before execution to require that if the text string drops a table then it must also recreates that table.
So this text string would pass:
DECLARE @good_text = '
DROP TABLE dbo.my_table
CREATE TABLE dbo.my_table (
col1 INT,
col2 CHAR(1)
)
'
And this text would fail as there is no create table statement:
DECLARE @bad_text = '
DROP TABLE dbo.my_table
'
And this text would fail because the two table have different names:
DECLARE @bad_text = '
DROP TABLE dbo.my_table
CREATE TABLE dbo.different_table_name (
col1 INT,
col2 CHAR(1)
)
'
If I did not need to match the table name, then it would be straight forward to check @text LIKE '%DROP TABLE%'
and @text LIKE '%CREATE TABLE%'
.
Something like:
SET @contains_drop = IIF(@text LIKE '%DROP TABLE%', 1, 0)
SET @contains_create = IIF(@text LIKE '%CREATE TABLE%', 1, 0)
IF NOT (@contains_drop = 1 AND @contains_create = 0)
EXEC @text
But these statements don't extend easily to include the table name. So this method would incorrectly pass the case when the drop and create table names differ.
Outside of SQL I would do this with regex/grep but I am not sure how to do so within SQL.
(Yes, I know there are good reasons not to pass this kind of text to EXEC
or SP_EXECUTESQL
. But changing this is not within the current scope, hence why I want to create this additional validation.)
No need for answers to handle table_name
vs [table_name]
or DROP TABLE IF EXISTS
vs DROP TABLE
. I can add all of that text cleaning separately.
Edit with additional details
One of our production tasks involves running a significant number of scripts. Rather than do these manually, we have designed a procedure that loops over a metadata table containing all the file names: loads the code within each script (into a variable like @text
above), validates, and then executes the script (using EXEC
or SP_EXECUTESQL
).
As part of the validation, we would like to be sure that DROP TABLE
statements never occur without a recreating CREATE TABLE
statement. Ideally this validation occurs at runtime so we can be confident it is always passed before execution.
I am not concerned about malicious code execution. My concern is more to catch mistakes if a script is updated and there is a typo in the table name, or a maintainer includes one statement but fails to include the other.
While I could use a different programming language to validate the scripts, a maintainer could forget to run this or could ignore its results. Hence, the preference to valid at runtime.
While there are alternatives to dropping and recreating a table, not all the maintainers have the skill to use the alternative approaches. And some of those alternatives should have the same validation (for example, I expect to use the same method to check TRUNCATE
commands are paired with INSERT INTO
commands).
Edit following answer
It seems like most of the comments & critique are focused on handling the problem in the most general of situations "of all the ways we could drop and create tables, how can we detect if the same table is dropped and created". This was not the intention of the question - though I can see why it was interpreted this way - hopefully this edit clarifies.
The intended interpretation is: "In the specific instance when tables are dropped and created in the same piece of text using the DROP TABLE and CREATE TABLE commands, how can I be sure both commands occur together and both refer to the same table?"
Hi I think this can solve your issue:
Explanation: What I did is that I prepared 2 temp tables and stored all the table names in them for dropped and created. Then joined those table and check for null in one and in case of null there is a mismatch.
Scenarios to fail :
Handled a scenario where if drop is after create then also fails. Schema is also handled.
DECLARE @good_text nvarchar(MAX) =
' DROP TABLE dbo.my_table1 asdf
CREATE TABLE dbo.my_table1 (col1 INT,col2 CHAR(1))
CREATE TABLE dbo.my_table2 (col1 INT,col2 CHAR(1))
DROP TABLE dbo.my_table2
adf
DROP TABLE dbo.my_table3 1234ea sdf
CREATE TABLE dbo.my_table3 (col1 INT,col2 CHAR(1))'
--Select Charindex('Drop Table',@good_text)+10
--Remove Line changes from the script for ease of parsing
SELECT
@good_text = Replace(Replace(@good_text,Char(13),' '),char(10),' ')
--Remove doulble spaces from the script
WHILE CHARINDEX(' ',@good_text) > 0
BEGIN
SET @good_text = Replace(@good_text,' ',' ')
END
DECLARE @drop1 nvarchar(MAX) = REPLACE(@good_text,'DROP TABLE','~')
, @create1 nvarchar(MAX) = REPLACE(@good_text,'CREATE TABLE','~')
DECLARE @dropcount int = LEN(@drop1) - len(Replace(@drop1, '~',''))
, @createCount int = LEN(@create1) - len(Replace(@create1, '~',''))
--Select @dropcount, @createCount
IF object_id('tempdb..#drop') IS NOT NULL
DROP TABLE #drop
IF object_id('tempdb..#Create') IS NOT NULL
DROP TABLE #Create
CREATE TABLE #drop (
id int IDENTITY
, table_name varchar(1000)
, WordLoc INT
, tSchema varchar(100) )
CREATE TABLE #Create (
id int IDENTITY
, table_name varchar(1000)
, WordLoc INT
, tSchema varchar(100) )
DECLARE @TableName varchar(100)
, @NewString nvarchar(MAX)
, @count int = 0
, @storeText nvarchar(MAX) = @good_Text
--Fill Drop table
WHILE (@count < @dropcount)
BEGIN
SELECT
@TableName =
SUBSTRING(@good_text
,(Charindex('Drop Table',@good_text)+10)
,Charindex(' ',(SUBSTRING(@good_text,Charindex('Drop Table',@good_text)+11,LEN(@good_text))))
)
SELECT
@NewString = SUBSTRING(@good_text, CHARINDEX('Drop Table'+@TableName,@good_text)+len(@TableName),len(@good_text))
INSERT INTO #drop ( table_name, WordLoc )
SELECT
@TableName
, CHARINDEX('Drop Table'+@TableName,@storeText)
SELECT
@good_text = @NewString
--select @NewString
SET @count += 1
END
SELECT
@good_text = @storeText
--Fill Create table
SET @count = 0
WHILE (@count < @createCount)
BEGIN
SELECT
@TableName =
SUBSTRING(@good_text
,(Charindex('Create Table',@good_text)+12)
,Charindex(' ',(SUBSTRING(@good_text,Charindex('Create Table',@good_text)+13,LEN(@good_text))))
)
SELECT
@NewString = SUBSTRING(@good_text, CHARINDEX('Create Table'+@TableName,@good_text)+len(@TableName),len(@good_text))
INSERT INTO #Create ( table_name, WordLoc )
SELECT
@TableName
, CHARINDEX('Create Table'+@TableName,@storeText)
SELECT
@good_text = @NewString
--select @NewString
SET @count += 1
END
DECLARE @TableTRIM nvarchar(100) = '#drop'
--Ltrim and rtrim all the data
IF OBJECT_ID('tempdb..##check') IS NOT NULL
BEGIN
DROP TABLE ##check
END
EXEC ('SELECT name, 0 AS done INTO ##check FROM tempdb.sys.columns WHERE OBJECT_ID = OBJECT_ID(''tempdb..'+@TableTRIM+''') AND is_identity = 0 AND system_type_id NOT IN (SELECT system_type_id FROM sys.types WHERE Name = ''float'')')
WHILE ((SELECT
count(*)
FROM ##check
WHERE done = 0)<>0)
BEGIN
DECLARE @col VARCHAR(100) = (SELECT TOP 1
'['+name+']'
FROM ##check
WHERE done = 0)
EXEC ('UPDATE '+@TableTRIM+' SET '+@col+' = ltrim(rtrim(REPLACE('+@col+',''"'','''')))')
UPDATE ##check
SET done = 1
WHERE '['+name+']' = @col
END
SET @TableTRIM = '#Create'
--Ltrim and rtrim all the data
IF OBJECT_ID('tempdb..##check') IS NOT NULL
BEGIN
DROP TABLE ##check
END
EXEC ('SELECT name, 0 AS done INTO ##check FROM tempdb.sys.columns WHERE OBJECT_ID = OBJECT_ID(''tempdb..'+@TableTRIM+''') AND is_identity = 0 AND system_type_id NOT IN (SELECT system_type_id FROM sys.types WHERE Name = ''float'')')
WHILE ((SELECT
count(*)
FROM ##check
WHERE done = 0)<>0)
BEGIN
SET @col = (SELECT TOP 1
'['+name+']'
FROM ##check
WHERE done = 0)
EXEC ('UPDATE '+@TableTRIM+' SET '+@col+' = ltrim(rtrim(REPLACE('+@col+',''"'','''')))')
UPDATE ##check
SET done = 1
WHERE '['+name+']' = @col
END
UPDATE #drop
SET tSchema = SUBSTRING(table_name,1,CHARINDEX('.',table_name))
UPDATE #Create
SET tSchema = SUBSTRING(table_name,1,CHARINDEX('.',table_name))
UPDATE #drop
SET tSchema = 'dbo.'
WHERE isnull(tSchema,'') = ''
UPDATE #Create
SET tSchema = 'dbo.'
WHERE isnull(tSchema,'') = ''
UPDATE #drop
SET table_name = REPLACE(table_name,tSchema,'')
UPDATE #Create
SET table_name = REPLACE(table_name,tSchema,'')
IF EXISTS (
SELECT
*
FROM #drop D
FULL OUTER JOIN #Create C ON C.table_name = D.table_name
AND c.tSchema = D.tSchema
AND D.WordLoc < C.WordLoc --If you don't want to fail if drop is after create then comment this section
WHERE C.id IS NULL
OR d.id IS NULL
)
RAISERROR('Script contains mismatch drop and create',16,1)
The Fiddle for experimentations
Let me know if you find any different scenarios in this to fail so that I can update the code.
Thanks