Alter Database Statement in DDL Trigger - sql

I am trying to alter database through a DDL trigger which will fire on creation. However I am getting a below error.
CREATE TRIGGER ddl_trig_database
ON ALL SERVER
FOR CREATE_DATABASE
AS
declare #dbname as nvarchar(100)
declare #sql as nvarchar(max)
select #dbname =
CAST(eventdata().query(
'/EVENT_INSTANCE/DatabaseName[1]/text()'
) as NVarchar(128))
select #sql = N'SET IMPLICIT_TRANSACTIONS OFF
ALTER DATABASE ' + #dbname+ N' SET COMPATIBILITY_LEVEL = 110
SET IMPLICIT_TRANSACTIONS ON'
exec (#sql)
GO
create database test
Error:
Msg 226, Level 16, State 6, Line 22
ALTER DATABASE statement not allowed within multi-statement transaction.
The statement has been terminated.
I am on SQL Server 2014 on Windows 2012.

If you want a specific compatibility level for each new database created - just set that compatibility level in the model database which is the "template" for all new databases being created ...
No need for a system-level trigger for this ....

I realized the DDL trigger will be on its own transaction and Alter is not allowed if a transaction is already started. So to workaround with this problem I have created SQL Job. and put the Alters in the Job and modified the Trigger to call msdb..start_sql_job.
--Trigger
CREATE TRIGGER ddl_trig_database
ON ALL SERVER
FOR CREATE_DATABASE
AS
exec msdb..sp_start_job 'Initialize Database'
GO
--Job
declare #dbname as nvarchar(100)
declare #sql as nvarchar(max)
select top 1 #dbname = name from sys.databases
where name like 'gtp%' and create_date >= getdate() - .08
order by create_date desc
IF #dbname is not null
begin
select #sql = N'ALTER DATABASE ' + #dbname+ N' SET COMPATIBILITY_LEVEL = 110'
exec sp_executesql #sql
print 'Altered database'
end
print 'completed'

Related

Create trigger in CREATE DATABASE stored procedure

We have a stored procedure that creates a database for each of our customers. This stored procedure runs in the context of master. A database name is passed in as a parameter to the stored procedure.
I am trying to modify the stored procedure to add a trigger to a table. I understand the stored procedure must switch to the new database to create triggers, so have appended the following to the stored procedure:
SET #str = ('USE ' + QUOTENAME (#db_name) + ' GO
CREATE TRIGGER ...')
EXEC (#str);
I get the error
Msg 102, Level 15, State 1, Line 3
Incorrect syntax near 'GO'.
Msg 111, Level 15, State 1, Line 4
'CREATE TRIGGER' must be the first statement in a query batch.
Now I assume the second error is a consequence of the first, but I am blowed if I can work out how to switch databases in the stored procedure in order to create the triggers.
We are using SQL Server 2019. How do I create triggers in a create database stored procedure?
Another approach is to execute statement one after another.
DECLARE #str VARCHAR(MAX)
DECLARE #db_name SYSNAME = 'YourDBName'
SET #str = 'USE ' + QUOTENAME (#db_name)
EXEC (#str)
SET #str = 'CREATE TRIGGER ...'
exec (#str);
For completeness, as mentioned by #DaleK, adding the approach mentioned by Aaron Bertnard in the Stackexchange link
DECLARE #str VARCHAR(MAX)
DECLARE #db_name SYSNAME = 'master'
SET #str = 'EXEC '+ #db_name + '..sp_executesql #stmt=N''CREATE PROCEDURE usp_test AS SELECT 1;'''
exec (#str);

How to fix that procedure in sql

I created procedure which count not null rows in the column, but query throws errors: #tableName is not declared and invalid object name tempTable. I don't know why code throws that errors, because all variables are declared.
Msg 1087, Level 16, State 1, Procedure getLenCol, Line 7 [Batch Start Line 0]
Must declare the table variable "#tableName".
Msg 208, Level 16, State 1, Line 11
Invalid object name 'tempTable'.
CREATE OR ALTER PROC getLenCol
#tableName varchar(255),
#colName varchar(255)
as
DECLARE #tempTable Table(smth varchar(255));
DECLARE #query varchar(255)
insert into #tempTable(smth) select #colName from #tableName where #colName is not null
exec (#query)
select ##ROWCOUNT
GO
exec getLenCol 'users','name'
Also when I make that program in another way, that code throw
Msg 1087, Level 15, State 2, Line 11
error.
Must declare the table variable "#tempTable".
CREATE OR ALTER PROC getLenCol
#tableName varchar(255),
#colName varchar(255)
as
DECLARE #tempTable Table(smth varchar(255))
DECLARE #query varchar(255)
SET #query = concat('insert into #tempTable(smth) select ',#colName,' from ',#tableName,' where ',#colName,' is not null');/*#colName from #tableName where #colName is not NULL*/
exec (#query)
select ##ROWCOUNT
GO
exec getLenCol 'users','name'
Is it a way to fix that error?
Obviously, your code is subject to SQL injection attacks -- as the comments on the question have explained.
But your issue is the scoping rules around your table variable. You can fix that by using:
set #query = concat('select ', #colName, ' from ', #tableName, ' where ', #colName,' is not null');
insert into #tempTable (smth)
exec(#query);
I don't think there is any way around the SQL injection vulnerabilities for the logic you have suggested. However, your code is so non-sensical that I doubt that it is really representative of your actual code.
As it seems that many are not aware of the dangers of SQL Injection, including Gordon, I wanted to expand on that first. Let's, take the accepted answer (at time of writing), which gives the following:
CREATE OR ALTER PROC getLenCol
#tableName varchar(255),
#colName varchar(255)
as
DECLARE #query varchar(255)
DECLARE #tempTable Table(smth varchar(255))
set #query = concat('select ', #colName, ' from ', #tableName, ' where ', #colName,' is not null');
insert into #tempTable (smth)
exec(#query);
GO
Now, let's be someone malicious:
EXEC dbo.getLenCol #colName = N'1; CREATE LOGIN NewLogin WITH PASSWORD = ''1'', CHECK_POLICY = OFF;/*',
#tableName =N'*/ ALTER SERVER ROLE sysadmin ADD MEMBER NewLogin;--';
So, what does the above, in the dynamic SQL run? Let's find out by adding PRINT #query; to the SP's definition:
select 1; CREATE LOGIN NewLogin WITH PASSWORD = '1', CHECK_POLICY = OFF;/* from */ ALTER SERVER ROLE sysadmin ADD MEMBER NewLogin;-- where 1; CREATE LOGIN NewLogin WITH PASSWORD = '1', CHECK_POLICY = OFF;/* is not null
And, with a little formatting for ease of reading:
select 1;
CREATE LOGIN NewLogin WITH PASSWORD = '1', CHECK_POLICY = OFF;
/* from */
ALTER SERVER ROLE sysadmin ADD MEMBER NewLogin;
-- where 1; CREATE LOGIN NewLogin WITH PASSWORD = '1', CHECK_POLICY = OFF;/* is not null
OH. OHHHHHHHHHHH. Congratulations you are the new proud owner of a SQL Server that has a new sysadmin LOGIN!
NEVER, inject unsanitised string into a string in SQL. NEVER.
Rather than repeating myself, I'm going to link to my article Dos and Don'ts of Dynamic SQL, however, you can easily make the above query secure with a few of uses of QUOTENAME:
CREATE OR ALTER PROC getLenCol
#schemaName sysname = N'dbo', --You should define the schema too
#tableName sysname, --An object can't be longer than 128 characters, so sysname is best
#colName sysname
AS
BEGIN
DECLARE #query nvarchar(MAX);
DECLARE #tempTable Table(smth varchar(255));
SET #QUERY = CONCAT(N'SELECT ', QUOTENAME(#colName),N' FROM ', QUOTENAME(#schemaName), N'.', QUOTENAME(#tableName), N' WHERE ', QUOTENAME(#colName), N' IS NOT NULL;');
PRINT #query;
INSERT INTO #tempTable (smth)
EXEC sys.sp_executesql #query;
END;
GO
And what happens if we run the above EXEC statement before? Well you get the statement below (with added formatting):
SELECT [1; CREATE LOGIN NewLogin WITH PASSWORD = '1', CHECK_POLICY = OFF;/*]
FROM [dbo].[*/ ALTER SERVER ROLE sysadmin ADD MEMBER NewLogin;--]
WHERE [1; CREATE LOGIN NewLogin WITH PASSWORD = '1', CHECK_POLICY = OFF;/*] IS NOT NULL;
And no surprised, that generated the error
Invalid object name 'dbo.*/ ALTER SERVER ROLE sysadmin ADD MEMBER NewLogin;--'.
Now your dynamic statement is safe from injection.
I would highly recommend against this approach, firstly calling this procedure is as much, if not more typing that just doing a count. Compare the two
EXECUTE dbo.getLenCol #tableName = 'dbo.SomeTable', #colName = 'ID';
SELECT COUNT(ID) FROM dbo.SomeTable;
Even with the shortened exec, and not using named parameters it is longer:
EXEC dbo.getLenCol dbo.SomeTable', 'ID';
It is very, very rare that a catch all query like this, with object names being passed as parameters is going to be the correct approach. There are some maintenance queries where it is useful, but these are the exception, not the rule.
If you must do this though, you should do a little bit of validation first, and check that both the table name and column name are valid before executing any dynamic SQL by using COL_LENGTH(#tableName, #ColName). e.g
CREATE OR ALTER PROC getLenCol #tableName SYSNAME, #colName SYSNAME
AS
BEGIN
IF COL_LENGTH(#tableName, #ColName) IS NOT NULL
BEGIN
DECLARE #SQL NVARCHAR(MAX) = CONCAT('SELECT COUNT(', #colName, ') FROM ', #tableName, ';');
EXECUTE sp_executesql #SQL;
RETURN;
END
-- TABLE OR COLUMN WAS NOT VALID RETURN -1 TO INDICATE THAT
SELECT -1;
END

SQL Server : error assigned name to a local variable [duplicate]

This question already has answers here:
How to use a variable for the database name in T-SQL?
(4 answers)
Closed 2 years ago.
I have a problem with assigning a database name to a variable. When I do this, I get a message that there is no such database.
What am I doing wrong that this script doesn't work?
DECLARE #db_name varchar(10)
SET #db_name = 'xxx'
ALTER TABLE [#db_name].[dbo].[Table_Name]
DROP CONSTRAINT [Constraint_Name]
GO
ALTER TABLE [#db_name].[dbo].[Table_Name] WITH CHECK
ADD CONSTRAINT [Constraint_Name] CHECK (QUERY)
GO
ALTER TABLE [#db_name].[dbo].[Table_Name] CHECK CONSTRAINT [Constraint_Name]
GO
I'm getting this error message:
Msg 2702, Level 16, State 2, Line 5
Database '#db_name' does not exist.
Msg 2702, Level 16, State 2, Line 8
Database '#db_name' does not exist.
Msg 2702, Level 16, State 2, Line 11
Database '#db_name' does not exist.
let's say for example that you have a table created like this:
create table xxx(id int
CONSTRAINT AK_TransactionID UNIQUE(id))
Then you will need to cerate two variables(one for the object name and one for the query to be executed...):
DECLARE #db_name varchar(10) ;
DECLARE #query nvarchar(500) ;
Set them to the values you need:
SET #db_name = 'xxx';
set #query = 'ALTER TABLE ['+ #db_name +' ] DROP CONSTRAINT [AK_TransactionID]';
And then execute the query:
EXEC sp_executesql #query;
Here is a demo
A query - meaning all text until each GO command - is compiled before it is run.
Database names inside queries have to be be known at compile time, because the database + any table or column you specify are inspected by the query compiler to see if they are valid.
This means that Database names + Table names + Column names all cannot come from #-variables, because at compile time variables don't yet exist and have no value.
The fix is to use dynamic SQL:
DECLARE #db_name varchar(10) = 'xxx'
DECLARE #sql varchar(1000) = 'ALTER TABLE [' + #db_name + '].[dbo].[Table_Name] DROP CONSTRAINT [Constraint_Name]'
EXEC (#sql)
Unfortunately in SQL Server, you cannot pass schema, database, table or column names as a parameter - if you want to do this, you must use dynamic SQL. You could do the below to achieve this:
DECLARE #db_name VARCHAR(MAX)
DECLARE #SQL VARCHAR(MAX)
SET #db_name = [xxx]
SET #SQL = 'ALTER TABLE' + #db_name+ '.[dbo].[Table_Name] DROP CONSTRAINT [Constraint_Name]'
EXEC (#SQL)
SET #SQL = 'ALTER TABLE' + #db_name + '.[dbo].[Table_Name] WITH CHECK ADD CONSTRAINT [Constraint_Name] CHECK (QUERY)'
EXEC (#SQL)
SET #SQL = 'ALTER TABLE' + #db_name + '.[dbo].[Table_Name] CHECK CONSTRAINT [Constraint_Name]'
EXEC (#SQL)

RESTORE database now in Emergency

I posted yesterday because we have a database that was basically used for application purposes but it was using "master" - bad indeed. We created a new database called school that is now being used (same structure as how master was minus a table renamed correctly). I was trying to restore a .bak file from the old db (master from sql 2008) to the new school db (school in sql 2016).
Problem is, running the script gave me a bunch of lines saying it's updating, then the restore terminated abnormally... My db was then in a recovery pending state, I tried running emergency code but it seems pretty broken and i'm not sure 1. why it failed in the first place and 2. what to do now.
Below is the script code and the error message
use school;
DECLARE #TableSchema sys.sysname = N'dbo'
DECLARE #TableName sys.sysname = N'rolerights'
DECLARE #OldTableName sys.sysname = N'rolerigths'
DECLARE #OldTableWithSchema NVARCHAR(256) = QUOTENAME(#TableSchema) + '.' + QUOTENAME(#OldTableName)
DECLARE #TableWithSchema NVARCHAR(256) = QUOTENAME(#TableSchema) + '.' + QUOTENAME(#TableName)
IF (EXISTS (SELECT * FROM INFORMATION_SCHEMA.TABLES
WHERE TABLE_SCHEMA = #TableSchema
AND TABLE_NAME = #TableName))
BEGIN
EXEC sp_rename #TableWithSchema, #OldTableName
END
DECLARE #Table TABLE ([LogicalName] varchar(128),[PhysicalName] varchar(128), [Type] varchar, [FileGroupName] varchar(128), [Size] varchar(128),
[MaxSize] varchar(128), [FileId]varchar(128), [CreateLSN]varchar(128), [DropLSN]varchar(128), [UniqueId]varchar(128), [ReadOnlyLSN]varchar(128), [ReadWriteLSN]varchar(128),
[BackupSizeInBytes]varchar(128), [SourceBlockSize]varchar(128), [FileGroupId]varchar(128), [LogGroupGUID]varchar(128), [DifferentialBaseLSN]varchar(128), [DifferentialBaseGUID]varchar(128),
[IsReadOnly]varchar(128), [IsPresent]varchar(128), [TDEThumbprint]varchar(128), [SnapshotUrl]varchar(128)
)
DECLARE #Path varchar(1000)='C:\Program Files\Microsoft SQL Server\MSSQL13.SQLEXPRESS\MSSQL\Backup\SQL2008backup.bak'
DECLARE #LogicalNameData varchar(128),#LogicalNameLog varchar(128)
INSERT INTO #table
EXEC('
RESTORE FILELISTONLY
FROM DISK=''' +#Path+ '''
')
SET #LogicalNameData=(SELECT LogicalName FROM #Table WHERE Type='D')
SET #LogicalNameLog=(SELECT LogicalName FROM #Table WHERE Type='L')
SELECT #LogicalNameData, #LogicalNameLog
use master;
declare #MasterData nvarchar(512)
exec master.dbo.xp_instance_regread N'HKEY_LOCAL_MACHINE', N'Software\Microsoft\MSSQLServer\MSSQLServer\Parameters', N'SqlArg0', #MasterData output
select #MasterData=substring(#MasterData, 3, 255)
select #MasterData=substring(#MasterData, 1, len(#MasterData) - charindex('\', reverse(#MasterData)))
print #MasterData
print #LogicalNameData
declare #MasterLog nvarchar(512)
exec master.dbo.xp_instance_regread N'HKEY_LOCAL_MACHINE', N'Software\Microsoft\MSSQLServer\MSSQLServer\Parameters', N'SqlArg2', #MasterLog output
select #MasterLog=substring(#MasterLog, 3, 255)
select #MasterLog=substring(#MasterLog, 1, len(#MasterLog) - charindex('\', reverse(#MasterLog)))
print #MasterLog
print #LogicalNameLog
declare #DefaultData nvarchar(512)
select isnull(#DefaultData, CONVERT(nvarchar(512), #MasterData))
declare #DefaultLog nvarchar(512)
select isnull(#DefaultLog, CONVERT(nvarchar(512), #MasterLog))
declare #NewDefaultData nvarchar(512) = #MasterData + '\' + 'school.MDF'
declare #NewDefaultLog nvarchar(512) = #MasterLog + '\' + 'school.LDF'
SET DEADLOCK_PRIORITY 10
ALTER DATABASE school
SET SINGLE_USER
WITH ROLLBACK IMMEDIATE;
RESTORE DATABASE school FROM DISK=#Path
WITH MOVE #LogicalNameData TO #NewDefaultData,
MOVE #LogicalNameLog TO #NewDefaultLog,
REPLACE
IF (EXISTS (SELECT * FROM INFORMATION_SCHEMA.TABLES
WHERE TABLE_SCHEMA = #TableSchema
AND TABLE_NAME = #OldTableName))
BEGIN
EXEC sp_rename #OldTableWithSchema, #TableName
END
And he is the emergency script
ALTER DATABASE [school] SET EMERGENCY;
GO
ALTER DATABASE [school] set single_user
GO
DBCC CHECKDB ([school], REPAIR_ALLOW_DATA_LOSS) WITH ALL_ERRORMSGS;
GO
ALTER DATABASE [school] set multi_user
GO
And the error:
Msg 5069, Level 16, State 1, Line 3
ALTER DATABASE statement failed.
Msg 946, Level 14, State 1, Line 5
Cannot open database 'school' version 677. Upgrade the database to the latest version.
Msg 946, Level 14, State 1, Line 7
Cannot open database 'school' version 677. Upgrade the database to the latest version.
Msg 5069, Level 16, State 1, Line 7
ALTER DATABASE statement failed.
Looking at the original logs... this is why it failed. An exception was thrown about filestream garbage collection apparently...
FILESTREAM Failed to find the garbage collection table.
The way to go into single user mode to restore the master db is to use the -m switch and then restore the master db from SQLCMD. If you want to go into single user mode you probably want to do so using this form:
Set Single_User with rollback immediate;
Please see this page how to use switches at startup:
https://learn.microsoft.com/en-us/sql/database-engine/configure-windows/database-engine-service-startup-options?view=sql-server-2017

change recovery model on current database

I am trying to change the recovery model of the current database.
This is what I have:
DECLARE #dbName VARCHAR(50)
SELECT #dbName = DB_NAME()
ALTER DATABASE #dbName SET RECOVERY SIMPLE WITH NO_WAIT
#dbName gives me:
Incorrect syntax near '#dbName'.
I tried:
ALTER DATABASE database_id SET RECOVERY SIMPLE WITH NO_WAIT
database_id gives me:
Msg 5011, Level 14, State 5, Line 3 User does not have permission to
alter database 'database_id', the database does not exist, or the
database is not in a state that allows access checks.
How should I execute this on the current database?
DECLARE #sql NVARCHAR(MAX) = N'ALTER DATABASE '
+ QUOTENAME(DB_NAME())
+ ' SET RECOVERY SIMPLE WITH NO_WAIT;';
EXEC sp_executesql #sql;