SQL Server Ignoring IF Statement - sql-server

Every night, I backup my production server and push the backup to my dev server. My dev server then has a job that runs which first checks if the backup file exits, if so check if the database exists in dev and if so drop the database, then restore from file. This all works fine, unless the file is not yet complete due to slow transfer, etc. If the file is not completely downloaded when the job runs then the first step sees it exists and drops the database. The next step tries to restore and of course fails. The next day when the job runs I would expect that when it checks if the database exists, it would see that it does not and shouldn't attempt to drop it and just restore. However, what's happening is the job is unable to drop the database and just fails at that point. This requires manual intervention to get the database restored, which is my problem. I'm less concerned with the fact of having no database existing on the server for a day (in theory) as I can tweak the schedule further to restore sooner. What I am concerned with is why is the IF statement not working to check if the database exists and attempts to drop a database regardless? Here's the T-SQL code that I am using:
DECLARE #output INT
DECLARE #SqlPath varchar(500) = 'C:\Program Files\Microsoft SQL Server\MSSQL10_50.MSSQLSERVER\MSSQL\Backup\PROD-01_prod_backup.bak'
EXECUTE master.dbo.xp_fileexist #SqlPath, #output OUT
IF #output = 1
BEGIN
IF (EXISTS (SELECT name FROM master.dbo.sysdatabases WHERE ('[' + name + ']' = '[PROD-01]')))
BEGIN
ALTER DATABASE [PROD-01] SET SINGLE_USER WITH ROLLBACK IMMEDIATE
DROP DATABASE [PROD-01]
END
RESTORE DATABASE [PROD-01] FROM DISK = #SqlPath
END

I'm not sure how this is happening, as I am unable to reproduce, but a TRY / CATCH block is an ideal solution for this case:
SET XACT_ABORT ON
GO
DECLARE #output INT
DECLARE #SqlPath varchar(500) = 'C:\Program Files\Microsoft SQL Server\MSSQL10_50.MSSQLSERVER\MSSQL\Backup\PROD-01_prod_backup.bak'
EXECUTE master.dbo.xp_fileexist #SqlPath, #output OUT
IF #output = 1
BEGIN
IF (EXISTS (SELECT name FROM master.dbo.sysdatabases WHERE (name = 'PROD-01')))
BEGIN TRY
ALTER DATABASE [PROD-01] SET SINGLE_USER WITH ROLLBACK IMMEDIATE
DROP DATABASE [PROD-01]
END TRY
BEGIN CATCH
SELECT ERROR_MESSAGE();
END CATCH
RESTORE DATABASE [PROD-01] FROM DISK = #SqlPath
END

Related

How to perform ALTER DATABASE within a TRANSACTION In SqlServer

I am doing some work on a remote sql server database which take some time and i need to block any other connection to it so no data get lost
i believe i should use single user mode to do this
i need to get it back to multi user mode after i finish my work but
my connection to the remote sever is not reliable and many times will get disconnected before finish and usually just roll back automatically and do it later
the problem is when i try to perform it within transaction i get this error :
ALTER DATABASE statement not allowed within multi-statement transaction
how can i perform
ALTER DATABASE dbName
SET SINGLE_USER WITH ROLLBACK IMMEDIATE
in a transaction and make sure it will roll back to Multi user mode if got disconnected ?
So, we're trying to arrange for a database to be returned to multi_user mode if our connection drops. Here's one way that works, but is as ugly as sin.
First, we set things up appropriately:
create database RevertTest
go
use master
go
create table RevertLock (L int not null)
go
declare #rc int
declare #job_id uniqueidentifier
exec #rc = msdb..sp_add_job #job_name='RevertSingleUser',
#description='Revert the RevertTest database to multi_user mode',
#delete_level=3,
#job_id = #job_id OUTPUT
if #rc != 0 goto Failed
exec #rc = msdb..sp_add_jobstep #job_id = #job_id,
#step_name = 'Wait to revert',
#command = '
WHILE EXISTS (SELECT * FROM RevertLock)
WAITFOR DELAY ''00:00:01''
ALTER DATABASE RevertTest set multi_user
DROP TABLE RevertLock'
if #rc != 0 goto Failed
declare #nowish datetime
declare #StartDate int
declare #StartTime int
set #nowish = DATEADD(minute,30,GETDATE())
select #StartDate = DATEPART(year,#nowish) * 10000 + DATEPART(month,#nowish) * 100 + DATEPART(day,#nowish),
#StartTime = DATEPART(hour,#nowish) * 10000 + DATEPART(minute,#nowish) * 100 + DATEPART(second,#nowish)
exec #rc = msdb..sp_add_jobschedule #job_id = #job_id,
#name='Failsafe',
#freq_type=1,
#active_start_date = #StartDate,
#active_start_time = #StartTime
if #rc != 0 goto Failed
exec #rc = msdb..sp_add_jobserver #job_id = #job_id
if #rc != 0 goto Failed
print 'Good to go!'
goto Fin
Failed:
print 'No good - couldn''t establish rollback plan'
Fin:
Basically, we create a job that tidies up after us. We schedule the job to start running in half an hours time, but that's just to protect us from a small race.
We now run our actual script to do the work that we want it to:
use RevertTest
go
alter database RevertTest set single_user with rollback immediate
go
begin transaction
go
insert into master..RevertLock(L) values (1)
go
exec msdb..sp_start_job #job_name='RevertSingleUser'
go
WAITFOR DELAY '01:00:00'
If you run this script, you'll be able to observe that the database has entered single-user mode - the WAITFOR DELAY at the end is just to simulate us "doing work" - whatever it is that you want to do within the database whilst it's in single-user mode. If you stop this query running and disconnect this query window, within a second you should see that the database has returned to multi_user mode.
To finish your script successfully, just make the last task (before COMMIT) to be to delete from the RevertLock table. Just as with the disconnection, the revert job1 will take care of switching the DB back into multi_user and then cleaning up after itself.
1The job is actually slightly deceptive. It won't actually sit looping and checking the table in master - since your transaction has an exclusive lock on it due to the INSERT. It instead sits and patiently waits to acquire a suitable lock, which only happens when your transaction commits or rolls back.
You cannot include the ALTER statement within your transaction. But you could top and tail your transaction, like so:
ALTER DATABASE TEST SET SINGLE_USER
GO
BEGIN TRANSACTION
-- Generate an error.
SELECT 1/0
ROLLBACK TRANSACTION
GO
ALTER DATABASE TEST SET MULTI_USER
This script sets the db to single user mode. Then encounters an error, before returning to multi user mode.

Dropping SQL Server database: handling concurrent connections

During my integration tests, I try to drop database using:
USE master
ALTER DATABASE TestXyz SET SINGLE_USER WITH ROLLBACK IMMEDIATE
DROP DATABASE TestXyz
However, quite often (given the number of tests) one of the application background processes manages to get between SET SINGLE_USER and DROP DATABASE, which makes it single user of the database and breaks the DROP.
I can not use RESTRICTED_USER, as the application currently has db_owner permission (due to a large amount of legacy code, some of which requires it, so it will not be changed just for the tests).
I can not use OFFLINE as it does not delete database files from the disk.
How would you solve this problem?
OK plan b... iterate a drop of connections and rename the DB to get it away from the applications domain. Then drop it. To handle iterating through connections a try catch on the rename will hopefully allow it to run until it is able to drop the connection. Example code below creates a DB TestDB; renames it to testdb2 in the while loop before dropping it after the loop has succeeded.
-- Setup a scratch Db for testing
create database testdb
go
use testdb
while exists (select name from sys.databases where name = 'testdb')
Begin
DECLARE #DbName nvarchar(50) SET #DbName = N'testdb'
DECLARE #EXECSQL varchar(max) SET #EXECSQL = ''
SELECT #EXECSQL = #EXECSQL + 'Kill ' + Convert(varchar, SPId) + ';'
FROM MASTER..SysProcesses
WHERE DBId = DB_ID(#DbName) AND SPId <> ##SPId
EXEC(#EXECSQL)
Begin try
EXEC sp_renamedb 'testdb', 'testdb2'
end try
Begin Catch
print 'failed to rename'
End Catch
end
drop database testdb2
Try this once:
Stop application services and run your query.
Stop application services and restart SQL Server Services and then run your query.
I have finally solved it using the following approach:
ALTER LOGIN MyAppUser DISABLE
ALTER DATABASE TestXyz SET SINGLE_USER WITH ROLLBACK IMMEDIATE
DROP DATABASE TestXyz
ALTER LOGIN MyAppUser ENABLE
Since I can use different login for test database management process, this allows me to block application from accessing the DB. (The reason for SINGLE_USER here is just to kick already connected users. I haven't checked if ALTER LOGIN already does that, but I assume it does not).
Alternative option is to delete MyAppUser from the database before dropping it, however I thought about it only now and do not have code for it.

Restore stored procedure

I have a database in SQL Server 2008 R2 and I created this stored procedure for restoring databases:
CREATE PROCEDURE [dbo].[usp_DBRestore]
#DBName nvarchar(60)
,#BackName nvarchar(120)
,#OutMessage nvarchar(4000) output
--,
--#DataName varchar(60),
--#DataFileName varchar(120),
--#LogName varchar(60),
--#LogFileName varchar(120)
AS
BEGIN TRY
USE [master]
ALTER DATABASE #DBName SET SINGLE_USER WITH ROLLBACK IMMEDIATE
RESTORE DATABASE #DBName FROM
DISK = #BackName WITH
FILE = 1, NOUNLOAD,
REPLACE,
PASSWORD = 'TEST'
SET #OutMessage = 'OK';
ALTER DATABASE #DBName SET MULTI_USER WITH ROLLBACK IMMEDIATE
END TRY
BEGIN CATCH
ALTER DATABASE #DBName SET MULTI_USER WITH ROLLBACK IMMEDIATE
INSERT [dbo].[ErrorLog]
(
[UserName],
[ErrorNumber],
[ErrorSeverity],
[ErrorState],
[ErrorProcedure],
[ErrorLine],
[ErrorMessage]
)
VALUES(
CONVERT(sysname, CURRENT_USER),
ERROR_NUMBER(),
ERROR_SEVERITY(),
ERROR_STATE(),
ERROR_PROCEDURE(),
ERROR_LINE(),
ERROR_MESSAGE()
)
END CATCH
When I execute code I see this error :
a USE database statement is not allowed in a procedure, function or
trigger.
How can I solve this error?
You cannot do this in that way - you basically have two options:
stick to a stored procedure, but in that case, you have to use dynamic SQL. Your stored procedure creates a string of SQL statements, which allows it to use USE master and it allows it to dynamically set the database name etc., and then it executes that SQL statement using sp_executesql #sqlRestoreStatement. If you want to check this out, you MUST be all means read (and understand) Erland Sommarskog's seminal article The Curse and Blessings of Dynamic SQL
you can use a regular SQL script, possibly with SQLCMD placeholders (if you have SQLCMD mode enabled in your SQL Server Management Studio) and execute the restore from a regular script (which you can put into your own template folder, for instance). In that case, you'd have something like:
:setvar dbname YourDatabaseNameHere
DECLARE #FileName NVARCHAR(255)
SET #FileName = N'D:\YourBackupDirectory\SomeDatabase.bak'
RESTORE DATABASE [$(dbname)]
FROM DISK = #FileName
WITH FILE = 1,
MOVE N'YourDatabase_Data' TO N'D:\MSSQL\Data\$(dbname).mdf',
MOVE N'YourDatbase_Log' TO N'D:\MSSQL\Data\$(dbname)_Log.ldf',
NOUNLOAD, REPLACE,
STATS = 2
GO
With this setup, you can easily use the SQL script as a template and restore any kind of database using it.
You don't need the USE statement. Best is to remove Use statement and create / Alter this sp on master database itself.
If you want to take a backup execute this SP from master DB. I can not see any other way out.
You can create a linked server and have that referenced in your stored procedure.
For example. LinkedServer.database.[dbo].StoredProcedure
Check out this
How to create the linked server for SQL Server 2008 where we have the database from 2000 and 2005

SQL Server Account Issue

I am currently using SQL Server 2005 and I try to restore a database from couple of months ago but when I restore it when I try to create a account it tells me that the account already exists but you cannot see it under security. I tried dropping that account but it tells me that the account does not exist to drop...But when I try to create it, it tells me that it exists... It is driving me insane. I completely removed that account from the server and all the linked servers and all the databases and I tried to re-create it again it works on all database except one that it keeps on saying it already exists. Any ideas what it could be or what else I should be checking?
You are having orphaned user accounts in database. It is very common issue when you copy/move your database from one server to another server. You will need to remove them from database user Or more better option is to create Server Login and map that login with that orphaned database user.
While back I wrote about it in my blog with some demo script, you may want to look at it. Or just look at this MSDN link for use of SP which can be used to manager orphaned users.
As a DBA, I get this problem all the time. And the GUI is not much of a help. Your SQL server has the accounts you want, the db has the accounts you want, but they don't auto-connect on restore.
The REAL way to fix this is to STOP-USING-INDIVIDUAL-ACCOUNTS. Only provide access to DBs on the basis of being a member of an active directory group. Then you can give access issues to the AD-group. BTW: While individual accounts don't auto-reconnect on a restore, group-access does.
Here is some code I use to address the same issue.
create procedure [dbo].[proc_FarSyncLogins] #TargetDB nvarchar(128)= '' as
begin
SET NOCOUNT ON
declare #cmd varchar(1000)
begin try
drop table master.dbo.NeededUsers
end try
begin catch
print 'could not: drop table master.dbo.NeededUsers'
end catch
set #cmd='select name collate Latin1_General_CI_AS as name,is_disabled
into master.dbo.NeededUsers
from ['+#TargetDB+'].sys.sql_logins'
--print #cmd
exec(#cmd)
print 'logins that need to be enabled'
select * from master.dbo.NeededUsers
declare #UserName nvarchar(128)
declare SyncUsers1 cursor fast_forward for SELECT Name FROM master.dbo.NeededUsers
OPEN SyncUsers1
FETCH NEXT FROM SyncUsers1 INTO #UserName
WHILE ##FETCH_STATUS = 0
BEGIN
set #cmd = ''
if #TargetDB <> ''
begin
set #cmd=#cmd+'Use ['+#TargetDB+']; '+char(10)
end
begin try
SET #cmd = #cmd+'ALTER LOGIN ['+#UserName+'] ENABLE; '+char(10)
SET #cmd = #cmd+'exec sp_change_users_login #Action=''Auto_Fix'', #UserNamePattern ='''+#UserName+'''; '+char(10)
PRINT #cmd
EXEC(#cmd)
end try
begin catch
Print 'Failed :'+#cmd
end catch
FETCH NEXT FROM SyncUsers1 INTO #UserName
END
CLOSE SyncUsers1
DEALLOCATE SyncUsers1
end
Then just
exec master.dbo.proc_FarSyncLogins '<yourDBName>';
It IS determinately HACKY, you will want to read the output carefully to verify you meant to do all those actions. But most of the time you just want to restore everything that you had before.
The account exists in the database and at the server level.
So you need to drop the account from the database itself but not from the server as it deosn't exist there.
have you tried...
use {yourdatabasename}
go
EXEC sp_dropuser '{username}'

How can I ensure that I reset login permissions to MULTI_USER upon SQL script failure?

I have the following SQL script:
USE MyDatabase
ALTER DATABASE MyDatabase SET SINGLE_USER WITH ROLLBACK IMMEDIATE
GO
BEGIN TRANSACTION
-- Write to tables here
COMMIT TRANSACTION
ALTER DATABASE MyDatabase SET MULTI_USER
GO
I want to make sure that my database is not accessed while the script is running, which is why I am setting the database to SINGLE_USER at the top.
Now, if anything inside the transaction fails (e.g. syntax error) I will never reset the database back to MULTI_USER which means it will be stuck in single user mode permanently (not what I want).
Is there a way to ensure we always go back to MULTI_USER, even on failure?
I guess I'm looking for the equivalent of a finally block in SQL, but from what I have read this doesn't exist. Is there another way?
It's worth noting that I cannot move the ALTER DATABASE commands into the transaction, as this is not permitted (since they are committed immediately).
You should be able to use a TRY CATCH to make sure you always go back to MULTI_USER.
You will just need to move the command to switch back to MULTI_USER right after the TRY CATCH block, since FINALLY is not supported in SQL Server.
I ran a quick test with the following SQL and it worked as expected in SQL Server 2005. Just make sure you ROLLBACK the transaction in the CATCH block. I used SELECT 1/0 to force the code into the CATCH block. For debugging purposes, I added the SELECT user_access_desc ... statements to show that the database was indeed switching from single user back to multi user mode.
USE MyDatabase
ALTER DATABASE MyDatabase SET SINGLE_USER WITH ROLLBACK IMMEDIATE
GO
SELECT user_access_desc from sys.databases WHERE Name = 'MyDatabase'
DECLARE #errNum AS INT
DECLARE #errMsg AS VARCHAR(MAX)
SET #errNum = 0
BEGIN TRY
BEGIN TRANSACTION
SELECT 1/0
COMMIT TRANSACTION
END TRY
BEGIN CATCH
SELECT #errNum = ERROR_NUMBER(), #errMsg = ERROR_MESSAGE()
ROLLBACK TRANSACTION
END CATCH
IF #errNum <> 0
SELECT 'An error occurred: ' + CAST(#errNum AS VARCHAR) + '- ' + #errMsg
ALTER DATABASE MyDatabase SET MULTI_USER
GO
SELECT user_access_desc from sys.databases WHERE Name = 'MyDatabase'
EDIT
In my original answer, I had the ALTER ... MULTI_USER statement inside both the TRY and the CATCH block, but that was unnecessary so I moved the statement to right after the TRY CATCH block. I also added some error reporting. Some things to watch out for with this approach are:
If you do any error handling or reporting, you'll need to make sure that SQL doesn't error. I would probably write the #errNum and #errMsg values to a table (wrapped in a TRY CATCH), switch back to MULTI_USER mode, and then perform whatever other error handling measures that are required, as the local variables will go out of scope after the GO statement.
Some errors are unaffected by TRY CATCH. The documentation I linked to above does list out what those conditions are.

Resources