Find who/when a SQL view is dropped in SQL Server - sql-server

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.

Related

SQL Triggers and Replication

We'd really appreciate some help here, we've been scratching our heads for most of the day.
We have a MS-SQL database on 'Server A' which has a table, in that table there is a trigger. This trigger is for INSERT only and all it does is read the field values from the INSERTED table, create a checksum & date and write the results to the table. I've used this method for very many years and it's never failed me.
SET ANSI_NULLS ON;
SET QUOTED_IDENTIFIER ON;
ALTER TRIGGER [TRG_TestingTable] ON [TestingTable]
AFTER INSERT NOT FOR REPLICATION
AS
BEGIN
SET NOCOUNT ON;
DECLARE #sqlFieldString AS VARCHAR(MAX);
DECLARE #sqlStatementString AS VARCHAR(MAX);
DECLARE #TableName AS VARCHAR(100);
DECLARE #SchemaName AS VARCHAR(20);
DECLARE #Recno INT;
SELECT
#TableName = OBJECT_NAME([sys].[triggers].[parent_id]),
#SchemaName = OBJECT_SCHEMA_NAME([sys].[triggers].[parent_id])
FROM
sys.triggers
WHERE
[sys].[triggers].[object_id] = ##PROCID;
SELECT #sqlFieldString = dbo.fn_GetHashFields(#SchemaName, #TableName);
SELECT #Recno = [inserted].[REC_lngURN] FROM inserted;
SET #sqlStatementString
= 'UPDATE ' + #SchemaName + '.' + #TableName
+ ' SET
[REC_dtUpdated] = GETDATE(),
[REC_checksum] = substring(lower(convert(varchar(32),
HASHBYTES(''MD5'',' + #sqlFieldString + '),1)),3,32) WHERE REC_lngURN = ' + CONVERT(VARCHAR(8), #Recno);
EXEC (#sqlStatementString);
END
Recently we've had to replicate the database to 'Server B' and set up Merge Replication successfully. The problem that we are experiencing is that the INSERT trigger doesn't fire on 'Server A' when replication is enabled, as soon as we remove the replication it works.
We've even tried specifying the order the triggers fire in with
EXEC sp_settriggerorder #triggername=N'[PSD].[TRG_TestingTable]', #order=N'First', #stmttype=N'INSERT'
We've read a lot about NOT FOR REPLICATION but all that does it prevent the Trigger running on 'Server B'.
Can some really brainy person tell us what we're doing wrong?

Create view that selects from identical tables from all databases on the server and can handle any time a new database is added to the server

I have a system that takes in Revit models and loads all the data in the model to a 2016 SQL Server. Unfortunately, the way the system works it created a new database for each model that is loaded. All the databases start with an identical schema because there is a template database that the system uses to build any new ones.
I need to build a view that can query data from all databases on the server but can automatically add new databases as they are created. The table names and associated columns will be identical across all databases, including data types.
Is there a way to pull a list of current database names using:
SELECT [name] FROM sys.databases
and then use the results to UNION the results from a basic SELECT query like this:
SELECT
[col1]
,[col2]
,[col3]
FROM [database].[dbo].[table]
Somehow replace the [database] part with the results of the sys.databases query?
The goal would be for the results to look as if I did this:
SELECT
[col1]
,[col2]
,[col3]
FROM [database1].[dbo].[table]
UNION
SELECT
[col1]
,[col2]
,[col3]
FROM [database2].[dbo].[table]
but dynamically for all databases on the server and without future management from me.
Thanks in advance for the assistance!
***Added Info: A couple suggestions using STRING_AGG have been made, but that function is not available in 2016.
Try this. It will automatically detect and include new databases with the specified table name. If a database is dropped it will automatically exclude it.
I updated the TSQL. STRING_AGG concatenates the string with each database. Without it it only returns the last database. STRING_AGG is more secure than += which also concatenates. I changed the code so it generates and executes the query. In SQL 2019 the query is all in one line using +=. I don't have SQL 2016. It may format it better in SQL 2016. You can uncomment --SELECT #SQL3 to see what the query looks like. Please mark as answer if this is what you need.
DECLARE #TblName TABLE
(
TblName VARCHAR(100)
)
Declare #SQL VARCHAR(MAX),
#SQL3 VARCHAR(MAX),
#DBName VARCHAR(50),
#Count Int,
#LoopCount Int
Declare #SQL2 VARCHAR(MAX) = ''
Select Identity(int,1,1) ID, name AS DBName into #Temp from sys.databases
Select #Count = ##RowCount
Set #LoopCount = 1
While #LoopCount <= #Count
Begin
SET #DBName = (SELECT DBName FROM #Temp Where ID = #LoopCount)
SET #SQL =
' USE ' + #DBName +
' SELECT TABLE_CATALOG FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_NAME = ''table'''
INSERT INTO #TblName (TblName)
EXEC (#SQL)
Set #LoopCount=#LoopCount + 1
End
SELECT #SQL2 +=
' SELECT ' + char(10) + 
' [col1] ' + char(10) + 
' ,[col2] ' + char(10) + 
' ,[col3] ' + char(10) + 
' FROM [' + TblName + '].[dbo].[table] ' + char(10) + 
' UNION '
FROM #TblName
DROP TABLE #Temp
SET #SQL3 = (SELECT SUBSTRING(#SQL2, 1, LEN(#SQL2) - 5))
--SELECT #SQL3
EXEC (#SQL3)

SQL Server 2014 Create DDL trigger on Create Table that creates a trigger for the table

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

procedure to rollback a transaction from audit table in SQL Server

I have created an audit table that is populated by an audit Trail (triggers after every update, delete, and insert) on different tables in my database. I am now asked to create a stored procedure (script) to rollback the data change using the audit id. How do I go about do so. I wrote a script which seems good. The command is accepted by SQL Server (command completed Successfully). Unfortunately when I test it by passing the Audit_id, the command is completed but the data is not rolled back. This is the Procedure I developed. Any help will be greatly appreciated.
create PROCEDURE [dbo].[spAudit_Rollback_2]
#AUDIT_ID NVARCHAR(MAX)
AS
SET Nocount on
BEGIN
DECLARE
#TABLE_NAME VARCHAR(100),
#COLUMN VARCHAR(100),
#OLD_VALUE VARCHAR(200),
#ID varchar(50)
SELECT #TABLE_NAME = TABLE_NAME FROM AUDIT;
SELECT #COLUMN = [COLUMN] FROM AUDIT;
SELECT #AUDIT_ID = AUDIT_ID FROM AUDIT;
SELECT #OLD_VALUE = OLD_VALUE FROM AUDIT
SELECT #ID = ROW_DESCRIPTION FROM AUDIT;
update [Production].[UnitMeasure]
set #COLUMN = #OLD_VALUE
WHERE [Production].[UnitMeasure].[UnitMeasureCode] = #ID
END
[dbo].[spAudit_Rollback_2]'130F0598-EB89-44E5-A64A-ABDFF56809B5
This is the same script but using adventureworks2017 database and data.
If possible I would even prefer to use a variable to retrieve that table name from Audit and use that in the procedure. That too is giving me another error.
Any help with this procedure will be awesome.
This needs to be dynamic SQL because you're updating a column that's defined in a variable. Do the following in place of your current UPDATE statement.
DECLARE #sql VARCHAR(1000) = ''
SET #sql = 'UPDATE [Production].[UnitMeasure] ' +
'SET ' + #COLUMN + ' = ''' + #OLD_VALUE + '''' +
'WHERE [Production].[UnitMeasure].[UnitMeasureCode] = ''' + #ID + ''''
EXEC(#sql)

SQL Server 2016 Change Object Owner

I've inherited a SQL 2008 dbase in which all of its objects are prefixed with the name of the developer as owner, i.e. ownername.sp_get_all_users.
I've restored the dbase onto SQL Server 2016 Express Edition.
There are several hundred dbase objects, is there a way to automate changing the object owners to dbo rather than manually editing each object?
I've tried the following but apparently you can no longer make ad-hoc changes to objects since SQL Server 2005?
SELECT * from sysobjects where uid = user_id('UseNAme')
declare #Return int
exec #Return = sp_configure 'allow updates', '1'
SELECT #Return as 'Returned Code'
GO
reconfigure WITH OVERRIDE
GO
DECLARE #Rows int, #Error int
BEGIN TRANSACTION
update sysobjects set uid = user_id('dbo') where uid = user_id('UseNAme')
SELECT #Error = ##Error, #Rows = ##RowCount
SELECT #Rows as '#Rows'
IF #Rows > 0
BEGIN
SELECT #Rows AS '#Rows'
COMMIT TRANSACTION
END
else
BEGIN
SELECT #Error AS 'Error #'
ROLLBACK TRANSACTION
END
exec sp_configure 'allow updates', '0'
reconfigure WITH OVERRIDE
go
Any help most appreciated.
You have to use Alter Schema...
ALTER SCHEMA oldschemaname TRANSFER dbo.Address;
To Automate use below
this will change all tables which have a schema other than system to dbo,note if you have two tables in different schema,they can't exist in same schema
select *,row_number() over (order by (select null)) as rownum
into #tables
from information_Schema.tables
where table_schema in (select name from sys.schemas
where name not in ('dbo','guest','INFORMATION_SCHEMA','sys') and principal_id <16384
)
now move
declare #min int,#max int
select #min=min(rownum),#max=max(rownum)
from #tables
declare #tblname varchar(255),#schemaname sysname
declare #sql varchar(max)
while #min<=#max
Begin
select #tblname=table_name,#schemaname=table_schema from
#tables where rownum=#min
set #sql='alter schema dbo transfer '+ #schemaname+'.'+#tblname
--print #sql
exec(#sql)
Set #min=#min+1
End
sp_change object owner as per documentation states..
This stored procedure only works with the objects available in MicrosoftSQL Server 2000. This feature will be removed in a future version of Microsoft SQL Server. Avoid using this feature in new development work, and plan to modify applications that currently use this feature. Use ALTER SCHEMA or ALTER AUTHORIZATION instead. sp_changeobjectowner changes both the schema and the owner. To preserve compatibility with earlier versions of SQL Server, this stored procedure will only change object owners when both the current owner and the new owner own schemas that have the same name as their database user names.
Use this sp_changeobjectowner
As explained here MSDN
For example: EXEC sp_changeobjectowner 'YourObject', 'dbo'
You can use this to alter schema statement for newer SQL Server DBS
declare #sql varchar(8000), #table varchar(1000), #oldschema varchar(1000), #newschema varchar(1000)
set #oldschema = 'dbo'
set #newschema = 'exe'
while exists(select * from sys.tables where schema_name(schema_id) = #oldschema)
begin
select #table = name from sys.tables
where object_id in(select min(object_id) from sys.tables where schema_name(schema_id) = #oldschema)
set #sql = 'alter schema ' + #newschema + ' transfer ' + #oldschema + '.' + #table
exec(#sql)
end
Your general idea of looping through the objects owned by the developer is a good idea (assuming you've tested the heck out of it). I'd suggest using the ALTER AUTHORIZATION command instead MSDN Doc
In addition to the advice above, the following changes the owner of SPs:
Declare #sql varchar(8000),
#table varchar(1000),
#oldschema varchar(1000),
#newschema varchar(1000)
set #oldschema = 'developername'
set #newschema = 'dbo'
while exists(select * from information_schema.routines where routine_type = 'PROCEDURE' and routine_schema = #oldschema )
begin
select #table = SPECIFIC_NAME from information_schema.routines
where SPECIFIC_NAME in(select SPECIFIC_NAME from information_schema.routines where routine_type = 'PROCEDURE' and routine_schema = #oldschema)
set #sql = 'alter schema ' + #newschema + ' transfer ' + #oldschema + '.' + #table
exec(#sql)
end

Resources