Create SQL Server trigger - dynamic SQL too long - sql

Currently I am working on an audit trail using SQL Server triggers to identify inserts, updates and deletes on tables.
Tables can be created dynamically in the database, therefore when this happens I need to create the trigger dynamically.
Therefore at this point I call a stored procedure and pass in the table name.
CREATE PROCEDURE [dbo].[AUDIT_CreateTableTrigger]
#STR_TableName NVARCHAR(MAX)
WITH EXECUTE AS CALLER
AS
BEGIN
DECLARE #STR_Trig NVARCHAR(MAX) = ''
SET #STR_Trig = #STR_Trig + '
CREATE TRIGGER [dbo].[' + #STR_TableName + '_Audit] ON [dbo].[' + #STR_TableName + ']
WITH EXECUTE AS CALLER AFTER
INSERT, UPDATE, DELETE AS
BEGIN
-- do the insert stuff
-- update
-- + delete
END'
EXEC (#STR_Trig) -- then execute the sql
My issue is that I am noticing that the exec isn't reading the statement completely and cuts the procedure off.
I need a way of executing a long piece of SQL code (I have one solution, this involves splitting the dynamic SQL into 3 triggers i.e insert, update and delete to get around this, however would prefer to keep 1 trigger to handle all)
Any suggestions would be appreciated, Thanks

Got this issue fixed: Broke up the query see below for solution
DECLARE #sql1 NVARCHAR(4000) = '',
#sql2 NVARCHAR(4000) = '',
#sql3 NVARCHAR(MAX)
SET #sql1 += '
CREATE TRIGGER [dbo].[' + #STR_TableName + '_Audit] ON [dbo].[' + #STR_TableName + ']
WITH EXECUTE AS CALLER AFTER
INSERT, UPDATE, DELETE AS
BEGIN
BEGIN TRY
--sql query
'
SET #sql2 = '
--more sql query
END'
SET #sql3 = CAST(#sql1 AS nvarchar(MAX)) + CAST (#sql2 AS nvarchar(MAX))
EXEC sp_executesql #sql3

Related

How to Create DELETE Statement Stored Procedure Using TableName, ColumnName, and ColumnValue as Passing Parameters

Here is what i'm trying to do. I'm trying to create a stored procedure where I could just enter the name of the table, column, and column value and it will delete any records associated with that value in that table. Is there a simple way to do this? I don't know too much about SQL and still learning about it.
Here is what I have so far.
ALTER PROCEDURE [dbo].[name of stored procedure]
#TABLE_NAME varchar(50),
#COLUMN_NAME varchar(50),
#VALUE varchar(5)
AS
BEGIN
SET NOCOUNT ON;
DECLARE #RowsDeleted int;
DECLARE #sql VARCHAR(500);
SET #sql = 'DELETE FROM (name of table).' + #TABLE_NAME + ' WHERE ' + #COLUMN_NAME + '=' + '#VALUE'
EXEC(#sql)
SET #RowsDeleted=##ROWCOUNT
END
GO
Couple issues
First, you don't need (name of table)
SET #sql = 'DELETE FROM ' + #TABLE_NAME + etc.
In general you should try to include the appropriate schema prefix
SET #sql = 'DELETE FROM dbo.' + #TABLE_NAME + etc.
And in case your table name has special characters perhaps it should be enclosed in brackets
SET #sql = 'DELETE FROM dbo.[' + #TABLE_NAME + ']' + etc.
Since #Value is a string, you must surround it with single quotes when computing the value for #SQL. To insert a single quote into a string you have to escape it by using two single quotes, like this:
SET #SQL = 'DELETE FROM dbo.[' + #TABLE_NAME + '] WHERE [' + #COLUMN_NAME + '] = '''' + #VALUE + ''''
If #VALUE itself contains a single quote, this whole thing will break, so you need to escape that as well
SET #SQL = 'DELETE FROM dbo.[' + #TABLE_NAME + '] WHERE [' + #COLUMN_NAME + '] = '''' + REPLACE(#VALUE,'''','''''') + ''''
Also, ##ROWCOUNT will not populate from EXEC. If you want to be able to read ##ROWCOUNT, use sp_ExecuteSQL instead
EXEC sp_ExecuteSql #SQL
And finally, let me editorialize for a minute--
This sort of stored procedure is not a great idea. I know it seems pretty cool because it is flexible, and that kind of thinking is usually smart when it comes to other languages, but in the database world this approach causes problems, e.g. there are security issues (e.g. injection, and the fact that you need elevated privileges to call sp_executeSql) and there issues with precompilation/performance (because the SQL isn't known ahead of time, SQL Server will need to generate a new query plan each and every time you call this) and since the caller can supply any value for table and column name you have no idea whether this delete statement will be efficient and use indexes or if it will cause a huge performance issue because the table is large and the column is not indexed.
The proper approach is to have a series of appropriate stored procedures with strongly-typed inputs that are specific to each data use case where you need to delete based on criteria. Database engineers should not be trying to make things flexible; you should be forcing people to think through what exactly they are going to need, and implement that and only that. That is the only way to ensure people are following the rules, keeping R/I intact, efficient use of indexes, etc.
Yes, this may seem like repetitive and redundant work, but c'est la vie. There are tools available to generate the code for CRUD operations if you don't like the extra typing.
In addition to some of the information John Wu provided you have to worry about data types and ##ROWCOUNT may not be accurate if there are triggers on your tables and things..... You can get around both of those issues though by casting to nvarchar() and using OUTPUT clause with a temp table to do the COUNT().
So just for fun here is a way you can do it:
CREATE PROCEDURE dbo.[ProcName]
#TableName SYSNAME
,#ColumnName SYSNAME
,#Value NVARCHAR(MAX)
,#RecordCount INT OUTPUT
AS
BEGIN
DECLARE #SQL NVARCHAR(1000)
SET #SQL = N'IF OBJECT_ID(''tempdb..#DeletedOutput'') IS NOT NULL
BEGIN
DROP TABLE #DeletedOutput
END
CREATE TABLE #DeletedOutput (
ID INT IDENTITY(1,1)
ColumnValue NVARCHAR(MAX)
)
DELETE FROM dbo.' + QUOTENAME(#TableName) + '
OUTPUT deleted.' + QUOTENAME(#ColumnName) + ' INTO #DeletedOutput (ColumnValue)
WHERE CAST(' + QUOTENAME(#ColumnName) + ' AS NVARCHAR(MAX)) = ' + CHAR(39) + #Value + CHAR(39) + '
SELECT #RecordCountOUT = COUNT(ID) FROM #DeletedOutput
IF OBJECT_ID(''tempdb..#DeletedOutput'') IS NOT NULL
BEGIN
DROP TABLE #DeletedOutput
END'
DECLARE #ParmDefinition NVARCHAR(200) = N'#RecordCountOUT INT OUTPUT'
EXECUTE sp_executesql #SQL, #ParmDefinition, #RecordCountOUT = #RecordCount OUTPUT
END
So the use of QOUTENAME will help against the injection attack but not be perfect. And I use CHAR(39) instead of the escape sequence for a single quote on value because I find it easier when string building at that point.... By using Parameter OUTPUT from sp_executesql you can still return your count.
Keep in mind just because you can do something in SQL doesn't always mean you should.

Creating a SQL Server trigger from a variable doesn't work

When I run the below code, it runs and prints the SQL output perfectly.
However the last EXEC statement throws an error
Incorrect syntax near the keyword 'TRIGGER'
The code basically strings together some SQL to create a trigger for all existing tables.
When I manually take that PRINT output at the end and execute it in SSMS it works fine, but the EXEC in the code just won't run it.
DECLARE #sql AS NVARCHAR(MAX)
SET #sql = ''
SELECT #sql = #sql + 'CREATE TRIGGER [tr_' + table_name +'] ON
[' + table_schema + '].[' + table_name + '] FOR INSERT, UPDATE, DELETE AS
SELECT 1 GO' + CHAR(13) + CHAR(10)
FROM INFORMATION_SCHEMA.TABLES
WHERE table_type = 'BASE TABLE'
PRINT #sql; -- output is correct and I can paste this and it works
EXEC sp_executesql #sql -- doesn't work
The SQL command to execute has to be a single batch. So, you can't use GO.
You better declare a CURSOR and execute the script for each table.

Merging two dynamic SQL statements into one

So I made a procedure that makes a dynamic view using dynamic SQL, but I used two sql executions: One for the if clause and one for the else, it should be possible to put it all under one SQL string or am i wrong? Because I tried it and get an error over and over again. Im not the best in writing dynamic sql, so it is probably my mistake or cant it be done and im losing my time on trying to do this?
create procedure test_view
(#table_name varchar(30))
as
BEGIN
declare# sqlQuery varchar(100)
if exists(select 1 from sp_iqview('v_anon_' + #table_name) where view_name = 'v_anon_' + #table_name)
begin
set# sqlQuery = ('drop view ' + 'v_anon_' + #table_name)
EXECUTE(#sqlQuery)
end
else
begin
set# sqlQuery = ('CREATE VIEW ' + 'v_anon_' + #table_name + ' AS SeLECT * FROM ' + #table_name)
EXECUTE(#sqlQuery)
select# sqlQuery
end
END
try this query.... Here else statement is not required.... if the object exists, it will drop in the first step itself. If not, it create new one...
create procedure test_view
(#table_name varchar(30))
as
BEGIN
declare #DropQuery varchar(100)
declare #CreateQuery varchar(100)
IF EXISTS(select 1 from sp_iqview('v_anon_' + #table_name) where view_name = 'v_anon_' + #table_name)
BEGIN
SET #DropQuery= 'drop view v_anon_' + #table_name
EXEC sp_executesql #DropQuery
END
SET #CreateQuery = 'CREATE VIEW v_anon_' + #table_name + ' AS SeLECT * FROM ' + #table_name
EXEC sp_executesql #CreateQuery
SELECT #CreateQuery
END
You would need a go between the two statements, but dynamic SQL doesn't support the GO keyword as it's not valid T-SQL. You would need to execution them separately...
However you could add go and then go with a solution proposed here I suppose...
Execute Dynamic Query with go in sql

Dynamic Sql - Creating a database - Problems with syntax

I'm having issues with a dynamic SQL script in particular this bit:EXEC('
if db_id(''' + $(db) + ''') is null
BEGIN
CREATE DATABASE ' + $(db) + '
END
The if statement part seems to work fine, I know this because if the database exists then the create database line is not run but when it needs to run I just get syntax errors near that line.
I have also tried:
CREATE DATABASE ''' + $(db) + '''
with no luck
Any Ideas?
DECLARE #DB_NAME NVARCHAR(128) = N'Test_DB'
DECLARE #Sql NVARCHAR(MAX);
IF DB_ID(#DB_NAME) IS NULL
BEGIN
SET #Sql = N' CREATE DATABASE ' + QUOTENAME(#DB_NAME)
EXECUTE sp_executesql #Sql
END
Important Note
Make sure your database name is in accordance with the Rules for Regular Identifiers

IF EXISTS in SQL Server Cursor Not Working

I have a cursor which works fine but when it gets to this part of the script, it seems to still run the update even though the table doesn't exists:
SET #sql = 'IF (EXISTS (SELECT * FROM ps_vars_' + #datasetid + '))
BEGIN
UPDATE ps_vars_' + #datasetid + '
SET programming_notes = replace(programming_notes, ''Some of the variables listed are source variables.'')
END';
EXEC SP_EXECUTESQL #sql
What am I missing? The #datasetid variable gets passed in correctly too.
DECLARE #tablename sysname
SET #tablename = 'ps_vars' + #datasetid
IF (OBJECT_ID(#tablename, 'U') IS NOT NULL)
BEGIN
SET #sql = ' UPDATE ' + QUOTENAME(#tablename) + '
SET programming_notes = replace(programming_notes, ''Some of the variables listed are source variables.'') ';
EXEC sp_executesql #sql
END
When you use the EXISTS with the table name to see if the table exists you're actually trying to access the table - which doesn't exist. That's why you're getting an error, not because of your UPDATE statement.
Try this instead:
SET #sql = 'IF (OBJECT_ID(''ps_vars_' + #datasetid + ''') IS NOT NULL)
BEGIN
UPDATE ...
END'
Then think about what might be wrong with your database design that requires you to use dynamic SQL like this. Maybe your design is exactly how it needs to be, but in my experience 9 out of 10 times (probably much more) this kind of code is a symptom of a poor design.