We have databases with associated SQL agent jobs that we keep track in a table called databasename.dbo.sqlagentjobs. Some databases are demos or QA databases and get regularly deleted.
At some point it happens that the administrator forgets to delete the associated SQL Agent jobs. In this case I wanted to write a trigger on database drop like this:
create trigger deleteagentjobs on all server for drop_database as
begin
declare #databasename nvarchar(100)
declare #eventdata xml
declare #query nvarchar(1000)
set #eventdata = EVENTDATA()
set #databasename = #eventData.value('(/EVENT_INSTANCE/DatabaseName)[1]','varchar(128)')
if object_id(#databasename +'.dbo.sqlagentjobs') is not null begin -- check if the database is a database with "our" database scheme
set #query = N'declare c insensitive cursor for select jobname_komplett from ' + #databasename + N'.dbo.sqlagentjobs ' +
N'declare #jobname nvarchar(257) ' +
N'declare #jobid uniqueidentifier ' +
N'open c ' +
N'fetch next from c into #jobname ' +
N'while ##FETCH_STATUS = 0 begin ' +
N' select job_id from msdb.dbo.sysjobs where name = #jobname ' +
N' exec msdb.dbo.sp_delete_job #jobid ' +
N' fetch next from c into #jobname ' +
N'end ' +
N'close c ' +
N'deallocate c '
exec sp_sqlexec #query
end
end
The issue with the trigger is that I get the following logical error message when I try to drop the database as follows:
exec msdb.dbo.sp_delete_database_backuphistory #database_name = n'demo'
go
alter database [demo] set single_user with rollback immediate
go
drop database [demo]
Error message:
Msg 5064, Level 16, State 1, Line 3
Changes to the state or options of database demo cannot be made at this time. The database is in single-user mode, and a user is currently connected to it.
Msg 5069, Level 16, State 1, Line 3
ALTER DATABASE statement failed.
Msg 3702, Level 16, State 4, Line 5
Cannot drop database demo because it is currently in use.
The error makes sense to me since I set the database to single user mode and try to access the database inside of the trigger.
Any ideas how to fix this? Thanks in advance!
Related
I am trying to delete a datapase in azure portal. After removing from SQL pool, I am getting the following error:
Msg 37106, Level 16, State 1, Line 7 The database '{databasename}' on server {'servername}' is in use by job account 'jobagent'. The database cannot be deleted or renamed while associated with a job account.
How should I address fixing this?
If you don't have other databases on it, you could simply drop the server and recreate it. That will get you around the issue.
Else Follow below steps delete database
You can kill a process by a right click on the process in the grid and selecting the Kill Process menu item. You will be asked for a confirmation to kill the related process and then will kill the open connection to the database over this process. This action is just like running to kill sql process t-sql command for a single process.
Way to drop all active connections of a database can be implemented by generating dynamic sql commands that runs a list of "Kill #spId" commands.
DECLARE #DatabaseName nvarchar(50)
SET #DatabaseName = N'Works'
--SET #DatabaseName = DB_NAME()
DECLARE #SQL varchar(max)
SET #SQL = ''
SELECT #SQL = #SQL + 'Kill ' + Convert(varchar, SPId) + ';'
FROM MASTER..SysProcesses
WHERE DBId = DB_ID(#DatabaseName) AND SPId <> ##SPId
-- SELECT #SQL
EXEC(#SQL)
A very similar to the sql code above, an other code block can be used by using the COALESCE as shown below
DECLARE #DatabaseName nvarchar(50)
SET #DatabaseName = N'Works'
DECLARE #SQL varchar(max)
SELECT #SQL = COALESCE(#SQL,'') + 'Kill ' + Convert(varchar, SPId) + ';'
FROM MASTER..SysProcesses
WHERE DBId = DB_ID(#DatabaseName) AND SPId <> ##SPId
--SELECT #SQL
EXEC(#SQL)
You can also refer this article
I'm working with a third party tool that creates databases and tables. I would like a trigger on one of those tables being created. I thought I would try to create a server DDL trigger that fires when the table is created in a new database, which in turn creates a trigger on that table. I cannot add the trigger to the 'model' database because this table is created via the tool dynamically.
I've tried the following:
CREATE TRIGGER ddl_trig_createTable
ON ALL SERVER
FOR CREATE_TABLE
AS
DECLARE #databaseName varchar(255)
DECLARE #AffectedTables varchar(255)
SELECT #AffectedTables = EVENTDATA().value('(/EVENT_INSTANCE/ObjectName)[1]','nvarchar(100)')
IF (#AffectedTables IN ('DynamicallyCreatedTable'))
BEGIN
select #databaseName = CAST(eventdata().query('/EVENT_INSTANCE/DatabaseName[1]/text()') as NVarchar(128))
EXEC('CREATE TRIGGER ' + #databaseName + '.[dbo].[tgrDynamicTableTrigger]
ON
' + #databaseName + '.[dbo].[DynamicallyCreatedTable]
AFTER UPDATE
AS
BEGIN
SET NOCOUNT ON
-- trigger code here
END')
END
GO
Which produces the following error when the table is created;
Msg 166, Level 15, State 1, Line 1 'CREATE/ALTER TRIGGER' does not
allow specifying the database name as a prefix to the object name.
I tried changing the dynamic sql by replacing the fully qualified table name to attempt a 'use' statement:
--- 8< ---
EXEC('use ' + #databaseName + '
CREATE TRIGGER [dbo].[tgrDynamicTableTrigger]
ON
--- 8< ---
However, that produced the following error when the table was created:
Msg 111, Level 15, State 1, Line 2 'CREATE TRIGGER' must be the first
statement in a query batch.
Any ideas?
I'm using SQL Server 2014.
I believe that I have figured it out, thanks mostly to this answer.
Here's the code:
CREATE TRIGGER ddl_trig_createTable
ON ALL SERVER
FOR CREATE_TABLE
AS
DECLARE #statement nvarchar(max) = 'CREATE TRIGGER [dbo].[tgrDynamicTableTrigger]
ON
[dbo].[DynamicallyCreatedTable]
AFTER UPDATE
AS
BEGIN
-- trigger code here
END'
DECLARE #databaseName varchar(255)
DECLARE #AffectedTables varchar(255)
SELECT #AffectedTables = EVENTDATA().value('(/EVENT_INSTANCE/ObjectName)[1]','nvarchar(100)')
IF (#AffectedTables IN ('DynamicallyCreatedTable'))
BEGIN
SET #databaseName = CAST(eventdata().query('/EVENT_INSTANCE/DatabaseName[1]/text()') as NVarchar(128))
DECLARE #sql NVARCHAR(MAX) = QUOTENAME(#databaseName) + '.sys.sp_executesql';
EXEC #sql #statement;
END
GO
Environment: SQL SERVER 2016
I am using the execute as user function in SQL to create a table specific to a session without using dynamic SQL. When I run the code as a single query it performs exactly as expected. Working code is:
DECLARE #strSQL NVARCHAR(MAX)
, #strSession AS NVARCHAR(256)
SET #strSession = 'SESSION_ABCD'
SET #strSQL = 'CREATE SCHEMA ' + #strSession
PRINT (#strSQL)
EXEC (#strSQL)
SET #strSQL = 'CREATE LOGIN ' + #strSession + ' WITH PASSWORD = ''' + CAST(NEWID() AS NVARCHAR(36)) + ''' '
PRINT (#strSQL)
EXEC (#strSQL)
SET #strSQL = 'CREATE USER ' + #strSession
PRINT (#strSQL)
EXEC (#strSQL)
SET #strSQL = 'ALTER USER ' + #strSession + ' WITH DEFAULT_SCHEMA = ' + #strSession
PRINT (#strSQL)
EXEC (#strSQL)
SET #strSQL = 'GRANT ALTER ON SCHEMA::' + #strSession + ' TO ' + #strSession
PRINT (#strSQL)
EXEC (#strSQL)
SET #strSQL = 'GRANT CREATE TABLE TO ' + #strSession
PRINT (#strSQL)
EXEC (#strSQL)
SET #strSQL = 'GRANT SELECT,INSERT,UPDATE,DELETE TO ' + #strSession
PRINT (#strSQL)
EXEC (#strSQL)
EXECUTE AS USER = 'SESSION_ABCD'
-- Create table without including default schema - OK
-- Creates table SESSION_ABCD.Test as expected
CREATE TABLE Test (MyValue INT)
-- Select from table including schema name - OK
SELECT *
FROM SESSION_ABCD.Test
-- Select from table excluding schema name - Works outside of stored procedure
SELECT *
FROM Test
REVERT
However if I run the same code inside a stored [procedure I get unexpected results. Code for this is:
CREATE PROC dbo.SetupUser
AS
BEGIN
DECLARE #strSession AS NVARCHAR(256)
SET #strSession = 'SESSION_ABCD'
DECLARE #strSQL NVARCHAR(MAX)
SET #strSQL = 'CREATE SCHEMA ' + #strSession
PRINT (#strSQL)
EXEC (#strSQL)
SET #strSQL = 'CREATE LOGIN ' + #strSession + ' WITH PASSWORD = ''' + CAST(NEWID() AS NVARCHAR(36)) + ''' '
PRINT (#strSQL)
EXEC (#strSQL)
SET #strSQL = 'CREATE USER ' + #strSession
PRINT (#strSQL)
EXEC (#strSQL)
SET #strSQL = 'ALTER USER ' + #strSession + ' WITH DEFAULT_SCHEMA = ' + #strSession
PRINT (#strSQL)
EXEC (#strSQL)
SET #strSQL = 'GRANT ALTER ON SCHEMA::' + #strSession + ' TO ' + #strSession
PRINT (#strSQL)
EXEC (#strSQL)
SET #strSQL = 'GRANT CREATE TABLE TO ' + #strSession
PRINT (#strSQL)
EXEC (#strSQL)
SET #strSQL = 'GRANT SELECT,INSERT,UPDATE,DELETE TO ' + #strSession
PRINT (#strSQL)
EXEC (#strSQL)
END
GO
CREATE PROC dbo.SetupTable
AS
BEGIN
EXECUTE AS USER = 'SESSION_ABCD'
-- Create table without including default schema - OK
-- Creates table SESSION_ABCD.Test as expected
CREATE TABLE Test (MyValue INT)
-- Select from table including schema name - OK
SELECT *
FROM SESSION_ABCD.Test
-- Select from table excluding schema name - Fails
-- Expecting same result as above
PRINT SCHEMA_NAME()
SELECT *
FROM Test
REVERT
END
GO
EXEC dbo.SetupUser
EXEC dbo.SetupTable
When I execute the script above, the users/sessions/etc configure correctly. (Please note you need to remove these from the system to run them a second time.) In the SetupTable procedure the create table command executes against the default schema, however the select command needs to be qualified with the schema name. While I could build the query up with Dynamic SQL to insert the schema name, this project is entirely around removing old dynamic sql code.
I will be honest and say this is not an approach I have used before, but seems to be the solution most suggested to solve another problem. I'm hoping there is something basic that I am missing, but at the moment I am completely stumped.
Many thanks.
Your second procedure is created in dbo schema, if you don't use fully qualified names within it, the default schema is stored procedure schema (dbo in your case), not user's default schema.
For example, you have user A with default schema a, schema B, and schema dbo.
You create a proc B.sp_test:
create proc B.sp_test as select * from test_tbl
Now user A executes this sp.
Table test_tbl is not schema qualified, so server first looks for B.test_tbl.
If the table exists, it selects from it. If no, it looks for dbo.test_tbl.
If the table is not found, you've got an error.
It's not user's default schema that is verified, but it's procedure's schema and then dbo schema that are verified.
That is different from table creation.
When user creates a table without qualifying it with the schema, it's default scema is used. And this is independent on WHERE the table is created, within sp or not.
How you can prove it to yourself?
Just create a procedure in a different schema.
Create 3 tables with the same name test_tbl, put them in your default schema, in dbo, and in sp schema.
In every table insert it's schema name.
Within sp make a SELECT from test_tbl, run it when all 3 tables exist.
You'll get the data from a table that is in sp's schema.
Now drop this table, run sp again.
This time you'll get the data from dbo.test_tbl.
Finally, drop dbo.test_tbl, run sp. This time you'll get an error.
In one of our SQL Server databases we have many SQL views. One particular view keeps disappearing every few weeks, and I want to find out what is happening.
Is there a way to query SQL Server to find out when and who dropped the view?
Alternatively, is it possible to add a SQL Server trigger on the DROP view command to capture and fail the DROP?
This information is written to the default trace. Below is an example query to glean the information.
SELECT
te.name
,tt.DatabaseName
,tt.StartTime
,tt.HostName
,tt.LoginName
,tt.ApplicationName
,tt.LoginName
FROM sys.traces AS t
CROSS APPLY fn_trace_gettable(
--get trace folder and add base file name log.trc
REVERSE(SUBSTRING(REVERSE(t.path), CHARINDEX(N'\', REVERSE(t.path)), 128)) + 'log.trc', default) AS tt
JOIN sys.trace_events AS te ON
te.trace_event_id = tt.EventClass
JOIN sys.trace_subclass_values AS tesv ON
tesv.trace_event_id = tt.EventClass
AND tesv.subclass_value = tt.EventSubClass
WHERE
t.is_default = 1 --default trace
AND tt.ObjectName = N'YourView'
AND tt.DatabaseName = N'YourDatabase';
Note the default trace is a rollover trace that keeps a maximum of 100MB so it might not have the forensic info if the view was recreated a while ago.
Yes, this is a DDL trigger. Sample trigger text is included in MSDN article about this kind of triggers. I'd say such a trigger is a must on production database for auditing reasons.
https://technet.microsoft.com/en-us/library/ms187909.aspx
Another trick is to create dependent on this object (view) another object (view?) with SCHEMA_BINDING option. This will make impossible to drop any object schema-bound object depends on.
To expand on another answer, here is some code to get started with a DDL trigger for DROP_VIEW. As an example, let's suppose someone dropped the view [HumanResources].[vEmployee] from the AdventureWorks database. The EVENTDATA() will look something like this:
<EVENT_INSTANCE>
<EventType>DROP_VIEW</EventType>
<PostTime>2016-02-26T09:02:58.190</PostTime>
<SPID>60</SPID>
<ServerName>YourSqlHost\SQLEXPRESS</ServerName>
<LoginName>YourDomain\SomeLogin</LoginName>
<UserName>dbo</UserName>
<DatabaseName>AdventureWorks2012</DatabaseName>
<SchemaName>HumanResources</SchemaName>
<ObjectName>vEmployee</ObjectName>
<ObjectType>VIEW</ObjectType>
<TSQLCommand>
<SetOptions ANSI_NULLS="ON" ANSI_NULL_DEFAULT="ON" ANSI_PADDING="ON" QUOTED_IDENTIFIER="ON" ENCRYPTED="FALSE" />
<CommandText>DROP VIEW [HumanResources].[vEmployee]
</CommandText>
</TSQLCommand>
</EVENT_INSTANCE>
And here is a possible DDL trigger statement:
CREATE TRIGGER trgDropView
ON DATABASE
FOR DROP_VIEW
AS
BEGIN
--Grab some pertinent items from EVENTDATA()
DECLARE #LoginName NVARCHAR(MAX) = EVENTDATA().value('(/EVENT_INSTANCE/LoginName)[1]', 'NVARCHAR(MAX)')
DECLARE #TsqlCmd NVARCHAR(MAX) = EVENTDATA().value('(/EVENT_INSTANCE/TSQLCommand/CommandText)[1]','NVARCHAR(MAX)')
--Now do something. Lots of possibilities. Here are two:
--1) Send Email
DECLARE #Subj NVARCHAR(255) = ##SERVERNAME + ' - VIEW DROPPED'
DECLARE #MsgBody NVARCHAR(255) = 'Login Name: ' + #LoginName + CHAR(13) + CHAR(10) +
'Command: ' + #TsqlCmd
EXEC msdb..sp_send_dbmail
#recipients = 'You#YourDomain.com',
#subject = #Subj,
#body = #MsgBody
--2) Log an error
DECLARE #ErrMsg NVARCHAR(MAX) = ##SERVERNAME + ' - VIEW DROPPED' + CHAR(13) + CHAR(10) +
'Login Name: ' + #LoginName + CHAR(13) + CHAR(10) +
'Command: ' + #TsqlCmd
RAISERROR(#ErrMsg, 16, 1) WITH LOG;
END
You can also create a trigger at the server level in order to capture and log DDL changes on the database :
CREATE TRIGGER [Trg_AuditStoredProcedures_Data]
ON ALL SERVER
FOR CREATE_PROCEDURE,ALTER_PROCEDURE,DROP_PROCEDURE,CREATE_TABLE,ALTER_TABLE,
DROP_TABLE,CREATE_FUNCTION,ALTER_FUNCTION,DROP_FUNCTION,CREATE_VIEW,ALTER_VI EW,
DROP_VIEW,CREATE_DATABASE,DROP_DATABASE,ALTER_DATABASE,
CREATE_TRIGGER,DROP_TRIGGER,ALTER_TRIGGER
AS
SET ANSI_PADDING ON
DECLARE #eventdata XML;
SET #eventdata = EVENTDATA();
SET NOCOUNT ON
/*Create table AuditDatabaseObject in order to have a history tracking for every DDL change on the database*/
INSERT INTO AuditDatabaseObject
(DatabaseName,ObjectName,LoginName,ChangeDate,EventType,EventDataXml,HostName)
VALUES (
#eventdata.value('(/EVENT_INSTANCE/DatabaseName)[1]','sysname')
, #eventdata.value('(/EVENT_INSTANCE/ObjectName)[1]', 'sysname')
, #eventdata.value('(/EVENT_INSTANCE/LoginName)[1]', 'sysname')
, GETDATE()
, #eventdata.value('(/EVENT_INSTANCE/EventType)[1]', 'sysname')
, #eventdata
, HOST_NAME()
);
DECLARE #Valor VARCHAR(30),#EvenType VARCHAR(30)
SET #Valor = #eventdata.value('(/EVENT_INSTANCE/LoginName)[1]', 'sysname')
SET #EvenType = #eventdata.value('(/EVENT_INSTANCE/EventType)[1]', 'sysname')
IF (IS_SRVROLEMEMBER('sysadmin',#Valor) != 1 AND #EvenType = 'DROP_DATABASE')
BEGIN
ROLLBACK
END
you can find more information here EVENTDATA()
if an object is dropped from the database you will see a record created on the table AuditDatabaseObject
also keep in mind security as # Chris Pickford mentioned.
I have many databases that on my SQL Server box that start with a prefix zzz.
Is there a way to do a DROP DATABASE (or some other method) that will remove and delete the data files? If a connection is opened, I want it closed.
Basically I just want them gone.
Generate a drop script, copy/paste & run:
exec master.sys.sp_msforeachdb 'if ''?'' like ''ZZZ%'' print ''drop database [?]'''
Or drop directly in the SQL string if your brave.
use master;
go
-- this will drop all dbs that start with t5....
declare #strsql varchar(500)
declare #curname sysname
select #curname = name from sys.databases
where name like 't5%'
while( ##rowcount> 0)
begin
set #strsql ='ALTER DATABASE ' +#curname +' SET OFFLINE WITH ROLLBACK IMMEDIATE'
exec (#strsql)
set #strsql ='drop database '+#curname
exec (#strsql)
select #curname = name from sys.databases
where name like 't5%'
end
You could write a dynamic SQL to do this:
use master
go
declare #dbnames nvarchar(max)
declare #statement nvarchar(max)
declare #closeconnection nvarchar(max)
set #dbnames = ''
set #statement = ''
select #dbnames = #dbnames + ',[' + name + ']' from sys.databases where name like 'zzz%'
if len(#dbnames) = 0
begin
print 'no databases to drop'
end
else
BEGIN
SET #closeconnection = 'alter database ' + substring(#dbnames, 2, len(#dbnames))
+ ' SET SINGLE_USER WITH ROLLBACK IMMEDIATE'
set #statement = 'drop database ' + substring(#dbnames, 2, len(#dbnames))
print #statement
EXEC sp_executesql #closeconnection;
exec sp_executesql #statement;
end
Normally, the syntax to close all active connections to a database is:
--set it to single user to disable any other connections
ALTER DATABASE YourDatabase SET SINGLE_USER WITH ROLLBACK IMMEDIATE
--do your stuff here
--set it back to multiple users
ALTER DATABASE YourDatabase SET MULTI_USER
Alternatively, you could also generate a dynamic select list that populates your drop database statement along with close connection statements like this:
USE master;
Go
SELECT 'DROP DATABASE '+ name,
'alter database ' + name + ' SET SINGLE_USER WITH ROLLBACK IMMEDIATE'
FROM sys.databases WHERE name like 'zzz%';
GO
Courtesy: #SeriousM and OFH
I modified benjamin's script a bit so you only have to declare the prefix once.
use master;
declare #dbPrefix varchar(10)
set #dbPrefix = 'zzz_%';
declare #strsql varchar(500)
declare #curname sysname
select #curname = name from sys.databases
where name like #dbPrefix
while( ##rowcount> 0)
begin
set #strsql ='ALTER DATABASE ' +#curname +' SET SINGLE_USER WITH ROLLBACK IMMEDIATE'
exec (#strsql)
set #strsql ='drop database '+#curname
exec (#strsql)
select #curname = name from sys.databases
where name like #dbPrefix
end