-- Copyright 2008 Maurice Pelchat -- YourSQLDba : Auto-maintenance tools for SQL Server Databases -- Author : Maurice Pelchat : Contact info https://www.linkedin.com/in/maurice-pelchat-9891495/ -- -- GitHub Website Readme : https://github.com/pelsql/YourSqlDba#readme -- Latest release of YourSqlDba : https://github.com/pelsql/YourSqlDba/blob/master/YourSQLDba_InstallOrUpdateScript.sql?raw=true -- Online Documentation : https://1drv.ms/o/c/12c385255443c4ed/Eu3EQ1QlhcMggBKoGwAAAAABRvootARfhNKB2ZzsOSOrfA?e=usHzVk -- Pay attention to the documentation landing page, and the Goals/QuickLinks table of the online documentation for all the subjects below: -- -- For a good overview it is goof to have a look to the complete introduction section -- -- First install? Easy setup to make YourSqlDba run with SQL Agent and Database Mail -- Main entry point for maintenance -- Job reporting and diagnostic -- More on diagnostics -- Version History -- Drop Table if Exists #version create table #Version (version nvarchar(40), VersionDate datetime) set nocount on insert into #Version Values ('7.1.0.6', convert(datetime, '2026-01-01', 120)) --Alter database yoursqldba set single_user with rollback immediate --go --Alter database yoursqldba set multi_user --go --use tempdb --go --RESTORE DATABASE [YourSQLDba] FROM DISK = N'C:\Users\pelchat\Desktop\YourSQLDba_[2012-09-13_01h44m26_Jeu]_database.Bak' WITH FILE = 1, MOVE N'YourSQLDba' TO N'C:\isql2008r2\DBData\YourSQLDba.MDF', MOVE N'YourSQLDbaDb_Log' TO N'C:\isql2008r2\DBLogs\YourSQLDba_Log.LDF', NOUNLOAD, STATS = 10 --GO declare @sql nvarchar(max); set @sql = ' ------------------------------------------------------------------------------------ YourSQLDba : Auto-maintenance tools for SQL Server Databases Author : Maurice Pelchat Contributors : Danielle Paquette-Harvey, Pierre-Luc Denommé, Dominic Perreault Licence : LGPL http://www.opensource.org/licenses/lgpl-2.1.php See point 15. of the licence described to the link above about warranty. ------------------------------------------------------------------------------------ YourSQLDba : Outils de maintenance de bases de données pour SQL Server Auteur : Maurice Pelchat Contributeurs : Danielle Paquette-Harvey, Pierre-Luc Denommé, Dominic Perreault License : LGPL http://www.opensource.org/licenses/lgpl-2.1.php Voir point 15. de la licence décrite au lien ci-dessus concernant la garantie. ------------------------------------------------------------------------------------ ' set @sql = replace(@sql, '"', '''') print @sql -- If YourSqlDba.Do_Maint is actually running wait until it ends, and prevent it -- to run until this script is done. We don't want to break running code -- when replacing SQL Modules with new versions Use tempdb If DB_ID('YourSqlDba') IS NOT NULL Begin set nocount on Set @Sql = N' Use YourSqlDba declare @cnt int =1 While (1=1) Begin If APPLOCK_TEST ("public", "YourSqlDba.Do_Maint", "Exclusive", "Session")=1 Break Raiserror ("Waiting for YourSqlDba.Do_maint to terminate...", 0, 1) with nowait Select "Check messages for more info: Waiting for YourSqlDba.Do_maint to terminate..., " Waitfor Delay "00:00:01" -- wait a sec, some msg are out End ' Set @Sql=replace(@sql collate database_default,'"','''') Exec (@Sql) End Use tempdb -- -- Installation procedure -- -- Step 1 : Simply launch this script, it install YourSQLDba database and objects requiered for maintenance -- Step 2 : ON FIRST INSTALL ONLY, run YourSQLDba_SetupOf_SqlAgent_MaintenanceJobs_DatabaseMail. -- This script setup SQLAgent Tasks, Database Mail -- using supplied parameters. -- Example for it is supplied at the end of this script. -- First two parameters are backups directories for log and for complete database backups -- Thrid parameter is email of the database admin or other operator -- Forth parameter is the address or name of a mail server that will -- accept anonymous smtp from your sql server (from database mail). -- -- Upgrades : Just rerun the new version of the script as described in Step 1 only -- -- Customizations are possible int the SQL Agent Job generated by the setup. -- Maintenance parameters are customizable at the job step level. -- Once defined, more steps can be added to the job with different parameters for different databases sets -- More steps can be added for operating systems commands especially to move backups files -- -- ===================================================================================================== -- ***************************************************************************************************** -- FOR CONTRIBUTORS -- -- You can send your comments and/or source code to pelsql@hotmail.com -- -- Any upgrades must take into account that the script must finds itself the state of the current -- installed version and bring it to the latest version (the actual script). -- -- Samples of this process are kept from previous version as if it ever existed -- but because of translation to identifiers in english, this version never really existed. -- -- However the method proove to be 100% successful with the previous french version. -- Many users of the original version upgraded their solution without problem and from any previous version -- -- It helped very much in making the project easy to upgrade, as it is always the same : -- re-run the latest script to upgrade to the last version. -- -- ***************************************************************************************************** -- For users of previous version set new solution to 'YourSQLDba' database declare @msg nvarchar(max) = NULL ;With Coll(name) as (select convert(sysname, SERVERPROPERTY('Collation'))) select @Msg = 'YourSqlDba doesn''t supported servers with case sensitive or binary collations ' From Coll Where name Like '%[_]CS[_]%' Or name Like '%[_]BIN' if @msg IS NOT NULL Raiserror (@Msg, 25,1) WITH LOG GO If not exists ( select * from sys.configurations Where name = 'show advanced options' And value_in_use = 1 ) Begin EXEC sp_configure 'show advanced options', 1 Reconfigure End GO If not exists ( select * from sys.configurations Where name = 'allow updates' And value_in_use = 0 ) Begin EXEC sp_configure 'allow updates', 0 Reconfigure End GO If not exists ( select * from sys.configurations Where name = 'clr enabled' And value_in_use = 1 ) Begin Exec sp_configure 'clr enabled', 1 End GO If not exists ( select * from sys.configurations Where name = 'Agent XPs' And value_in_use = 1 ) Begin EXEC sp_configure 'Agent XPs', 1 Reconfigure with override End GO -- Adjust SQL Server error logs archive to maximum of 30 error log cycle (by reboot or explicitely asked daily by YourSqlDba) Set nocount On EXEC xp_instance_regwrite N'HKEY_LOCAL_MACHINE', N'Software\Microsoft\MSSQLServer\MSSQLServer', N'NumErrorLogs', REG_DWORD, 30 GO -- If Sql Service Broker in not enabled on MSDB, enable it, to have database mail working if exists(select name, is_broker_enabled from sys.databases where name = 'msdb' and is_broker_enabled = 0) begin print 'Sql Service broker activation in MSDB' exec('alter database msdb set enable_broker with ROLLBACK IMMEDIATE') end -- else -- print 'Broker already enabled on MSDB' GO If Db_name() <> 'TempDb' Use TempDb; GO -- **************************************************************************************************** -- Save actual copy of YourSqlDba, in case a rollback is needed or past yoursqldba logs would be useful -- @@MARK: Upgrading process of actual YourSqlDba to this script version -- **************************************************************************************************** If databasepropertyEx('YourSQLDba','status') IS NOT NULL -- db is there Begin -- save data about some YourSqlDba tables Drop table if Exists ##JobHistory; Drop table if Exists ##JobHistoryDetails; Drop Table if Exists ##JobHistoryLineDetails; -- instead of presenting event info in XML, it is presented in readable form Drop table if Exists ##JobLastBkpLocations; Drop table if Exists ##TargetServer; Drop table if Exists ##JobSeqUpdStat; Drop table if Exists ##NetworkDrivesToSetOnStartup; -- perform some cleanup for YourSqlDba Maint.JobHistory table While (1=1) Begin Print 'Cleanup YourSqlDba job history for jobs older than 30 days' Delete top (50) H -- drop 50 jobs at the time to let the log file clear itself, cascading deletes does the rest of the job From ( Select distinct JobNo -- From YourSqlDba.Maint.JobHistory Where JobStart < dateadd(dd, -30, getdate()) ) as T join YourSqlDba.Maint.JobHistory H On H.JobNo = T.JobNo If @@rowcount = 0 Break End -- If table exists in previous version save its content If Object_id('YourSqlDba.Maint.JobHistory') IS NOT NULL Begin Declare @JsonPrms Int, @colIdJsonPrms Int, @colIdMainSqlCmd Int Exec sp_executeSql N'Use YourSqlDba; Set @colIdJsonPrms=Columnproperty(Object_id(''YourSqlDba.Maint.JobHistory''), ''JSonPrms'', ''ColumnId'') Set @colIdMainSqlCmd=Columnproperty(Object_id(''YourSqlDba.Maint.JobHistory''), ''MainSqlCmd'', ''ColumnId'') ' , N'@colIdJsonPrms Int Output, @colIdMainSqlCmd Int Output' , @colIdJsonPrms Output , @colIdMainSqlCmd Output If @colIdJsonPrms IS NOT NULL -- table is up to date for column JsonPrms, make the copy of the table Exec('Select * Into ##JobHistory From YourSqlDba.Maint.JobHistory') Else Exec( -- JsonPrms column is missing, brew the new version of the table with just JsonPrm ' Select JobNo , JobStart , JobEnd , Spid , JsonPrms= ( Select , JobName , DoInteg, DoUpdStats, DoReorg, DoFullBkp, DoDiffBkp, DoLogBkp, JobName, JobStart, JobEnd , IncDb, ExcDb, ExcDbFromPolicy_CheckFullRecoveryModel , TimeStampNamingForBackups, FullBkpRetDays, LogBkpRetDays , NotifyMandatoryFullDbBkpBeforeLogBkp , SpreadUpdStatRun, SpreadCheckDb , FullBackupPath, LogBackupPath, , FullBkExt, LogBkExt, , ConsecutiveDaysOfFailedBackupsToPutDbOffline , MirrorServer , MigrationTestMode , ReplaceSrcBkpPathToMatchingMirrorPath , ReplacePathsInDbFilenames , JobId, StepId , BkpLogsOnSameFile , EncryptionAlgorithm, EncryptionCertificate From YourSqlDba.Maint.JobHistory For JSON PATH, WITHOUT_ARRAY_WRAPPER ) Into ##JobHistory From YourSqlDba.Maint.JobHistory ' ) -- if column MainSqlCmd is missing add it also to the copy, assume other columns are missing since they are part of the same update If @colIdMainSqlCmd IS NULL Begin alter table ##JobHistory add MainSqlCmd nvarchar(max) null alter table ##JobHistory add Who Nvarchar(128) null alter table ##JobHistory add Host Nvarchar(128) null alter table ##JobHistory add Prog Nvarchar(128) null alter table ##JobHistory add SqlAgentJobName Nvarchar(128) null alter table ##JobHistory add jobId uniqueIdentifier Null alter table ##JobHistory add StepId Int Null End End -- make copies of other tables If Object_id('YourSqlDba.Maint.JobHistoryDetails') IS NOT NULL Select * Into ##JobHistoryDetails From YourSqlDba.Maint.JobHistoryDetails If Object_id('YourSqlDba.Maint.JobHistoryLineDetails') IS NOT NULL Select * Into ##JobHistoryLineDetails From YourSqlDba.Maint.JobHistoryLineDetails If Object_id('YourSqlDba.Maint.JobLastBkpLocations') IS NOT NULL Select * Into ##JobLastBkpLocations From YourSqlDba.Maint.JobLastBkpLocations If Object_id('YourSqlDba.Mirroring.TargetServer') IS NOT NULL Select * Into ##TargetServer From YourSqlDba.Mirroring.TargetServer If Object_id('YourSqlDba.Maint.JobSeqUpdStat') IS NOT NULL Select top 1 * Into ##JobSeqUpdStat From YourSqlDba.Maint.JobSeqUpdStat If Object_id('YourSqlDba.Maint.NetworkDrivesToSetOnStartup') Is NOT NULL Select * Into ##NetworkDrivesToSetOnStartup From YourSqlDba.Maint.NetworkDrivesToSetOnStartup -- if database is not upgraded yet, do a save, but avoid if it is to the same version If Not Exists(Select * from YourSqlDba.Install.VersionInfo () Actual join #Version NextVersion On Actual.VersionNumber = NextVersion.Version Collate database_default) Begin Declare @pathBkp Nvarchar(512); Exec master.dbo.xp_instance_regread N'HKEY_LOCAL_MACHINE' , N'Software\Microsoft\MSSQLServer\MSSQLServer' , N'BackupDirectory' , @pathBkp OUTPUT , 'no_output' -- SAVING A BACKUP COPY OF YOURSQLDBA, BEFORE WIPING IT AND RECRATING IT -- Don't use SaveDbOnNewFileSet but use "lighter" code to reduce code dependencies Declare @Language nvarchar(512) Exec YourSqlDba.yInstall.InstallationLanguage @language Output If RIGHT(@pathBkp, 1) <> '\' Set @pathBkp = @pathBkp + '\' Declare @bkpFile nvarchar(512) Select @bkpFile = @pathBkp+'YourSqlDba_'+REPLACE(CONVERT(nvarchar, Getdate(), 120), ':', '_')+'.bak'; Declare @version nvarchar(10) Select @version = VersionNumber From YourSqlDba.Install.VersionInfo () print '******************************************************************************************************************************' print 'Saving a copy of version '+@version+' of YourSqlDba to ' + @bkpFile print '******************************************************************************************************************************' Declare @bkpName nvarchar(512) Set @bkpName = 'Backup version '+@version+' of ' + @bkpFile -- bkpName must be <= 128 otherwise it complains about data truncation in msdb backup history details If len(@bkpName) > 128 Set @bkpName = Left(@bkpName, 125)+ '...' Backup Database YourSqlDba To Disk = @bkpFile With Init, name = @bkpname End Exec ('Alter database YourSqlDba Set single_user with rollback immediate') Exec ('WaitFor Delay ''00:00:05''; ') Exec ('Drop database YourSqlDba') End Go -- ------------------------------------------------------------------------------ -- Recreate a new YourSqlDba -- ------------------------------------------------------------------------------ Declare @sql nvarchar(max) Declare @pathData nvarchar(512) Declare @pathLog nvarchar(512) -- Read actual file locationof YourSqlDba if it exists and is available ;With PathOfFileLocations as ( select reverse(Stuff(Reverse(Physical_Name), 1, charindex('\', Reverse(Physical_Name)), '')) collate database_default as path , Case when Charindex(Name collate database_default, 'master, model, tempdb, msdb') > 0 Or Name like 'ReportServer%' Then 0 Else 1 End as SystemDb , type , database_id , Case When database_Id = db_id('YourSqlDba') Then 1 Else 0 End as IsYourSqlDba -- to identify YourSqlDba files if its there from sys.master_files Where DatabasePropertyex(Db_name(Database_id), 'Status') = 'Online' ) -- Select * From PathOfFileLocations , EvalFileLocation as ( Select path, IsYourSqlDba, Systemdb, type, Count (distinct database_id) Nb -- count by business cases, YourSqlDba first, then non system Db, then systemDb From PathOfFileLocations Group by path, IsYourSqlDba, SystemDb, type ) --Select * From EvalFileLocation , RankBestFileLocations as ( Select * -- Rank best choice by : It is YourSqlDba actual location, non systemDb (0 before 1), and number of files , Row_Number() Over (partition By type Order by IsYourSqlDba, systemDb, nb desc) as best From EvalFileLocation ) -- Select * From RankBestFileLocations Select -- trick to select good values that comes from different rows, on a single result row @pathData = max(case when type = 0 Then path else '' End) , @pathLog = max(case when type = 1 Then path else '' End) From RankBestFileLocations Where best = 1 -- @@MARK: Recreate YourSqlDba Database Set @sql = ' Create Database YourSQLDba ON PRIMARY ( Name = "YourSQLDba", FILENAME = "\YourSQLDba.MDF", SIZE = 60MB, FILEGROWTH = 20MB ) LOG ON ( Name = "YourSQLDba_Log", FILENAME = "\YourSQLDba_Log.LDF", SIZE = 5MB, FILEGROWTH = 5MB ) COLLATE LATIN1_GENERAL_CI_AI ALTER Database YourSQLDba Set RECOVERY SIMPLE ' Set @sql = Replace (@sql, '"', '''') Set @sql = Replace (@sql, '', @pathData) Set @sql = Replace (@sql, '', @pathLog) Exec (@sql) GO -- Create YourSqlDba login, with unknown password. If required DBA can change it. -- break links with logn YourSqlDba --drop login Yoursqldba --Select * From sys.databases where SUSER_SNAME(owner_Sid)='YourSqlDba' ----ALTER AUTHORIZATION ON Database::[YourSQLDba] To [sa] --Select * From msdb.dbo.sysjobs where SUSER_SNAME(owner_Sid)='YourSqlDba' --EXEC msdb.dbo.sp_update_job -- @job_name = 'YourSQLDba_FullBackups_And_Maintenance', -- Replace with the name of your job -- @owner_login_name = 'SA'; -- Replace with the new owner's login name Drop table If Exists #Pwd Select uPwd = convert(nvarchar(400), HASHBYTES('SHA1', convert(nvarchar(100),newid())), 2) Into #Pwd Declare @Sql Nvarchar(Max) Select @Sql = Sql.Sql From ( Select Sql= ' create login Yoursqldba With Password = ''#Upwd#'' , DEFAULT_DATABASE = YourSqlDba , CHECK_EXPIRATION = OFF , CHECK_POLICY = OFF , DEFAULT_LANGUAGE=US_ENGLISH ' Where Not Exists (select * from sys.sql_logins where name='YourSQLDba') UNION ALL Select Sql= ' CREATE CREDENTIAL YourSqlDbaRemoteServerCred WITH IDENTITY = ''YourSqlDba'', -- Remote SQL Login SECRET = ''#UPwd#''; -- Same password as YourSqlDba; ALTER LOGIN YourSqlDba WITH CREDENTIAL = YourSqlDbaRemoteServerCred; ' Where Not Exists (select * from sys.credentials where name='YourSqlDbaRemoteServerCred') ) as ToDo CROSS APPLY (Select Sql=REPLACE(Sql, '#UPwd#', Upwd) From #Pwd) as Sql Print @Sql Exec (@Sql) Drop Table #Pwd GO Exec sp_addsrvrolemember @loginame= 'YourSqlDba' , @rolename = 'sysadmin' GO ALTER AUTHORIZATION ON Database::[YourSQLDba] To [YourSqlDba] ALTER Database YourSqlDba Set TRUSTWORTHY OFF GO If DB_NAME()<> 'YourSqlDba' Use YourSQLDba --use tempdb --GO --alter database YourSQLDba set single_user with rollback immediate --GO --RESTORE DATABASE [YourSQLDba] FROM DISK = N'G:\SQL2005Backups\YourSQLDba_database[2][Mardi].Bak' WITH FILE = 1, NOUNLOAD, STATS = 5 --GO -- create schemas to identify fonctions Exec('Create schema Audit authorization dbo;') Exec('Create schema yAudit authorization dbo;') Exec('Create schema Export authorization dbo;') Exec('Create schema yExport authorization dbo;') Exec('CREATE SCHEMA yExecNLog AUTHORIZATION dbo') Exec('Create schema Install authorization dbo;') Exec('Create schema yInstall authorization dbo;') Exec('Create schema Maint authorization dbo;') Exec('Create schema yMaint authorization dbo;') Exec('Create schema Mirroring authorization dbo;') Exec('Create schema yMirroring authorization dbo;') Exec('Create schema PerfMon authorization dbo;') Exec('Create schema yPerfMon authorization dbo;') Exec('CREATE SCHEMA Upgrade AUTHORIZATION dbo') Exec('CREATE SCHEMA yUpgrade AUTHORIZATION dbo') Exec('Create schema Tools authorization dbo;') Exec('Create schema yUtl authorization dbo;') Exec('Create schema S# authorization dbo;') -- for copying new code from S# library GO -- @@MARK: MaintenanceEnums - A way to get enums (or constants) in queries Create or Alter View Maint.MaintenanceEnums AS Select -- useful, general use constants to describe maintenance parameters -- Those prefixed by HV$ are for function Maint.HistoryView HV$ShowErrOnly = 1 , HV$ShowAll = 0 , HV$ShowOnlyErrorOfJobFromSessionContext = 2 , HV$Now , HV$FromMidnight , HV$FromYesterdayMidnight , HV$Since12Hours , HV$Since10Min , HV$Since1Hour From (Select HV$Now=Getdate()) as Now CROSS APPLY (Select HV$FromMidnight=DateAdd(Day, DateDiff(Day, 0, HV$Now), 0)) as FromMidnight CROSS APPLY (Select HV$FromYesterdayMidnight=DateAdd(Day, DateDiff(Day, 0, HV$Now)-1, 0)) as FromYesterdayMidnight CROSS APPLY (Select HV$Since12Hours=DateAdd(Hour, DateDiff(Hour, 0, HV$Now)-12, 0)) as Since12Hours CROSS APPLY (Select HV$Since10min=DateAdd(Mi, DateDiff(Mi, 0, HV$Now)-10, 0)) as Since10Min CROSS APPLY (Select HV$Since1Hour=DateAdd(hh, DateDiff(hh, 0, HV$Now)-1, 0)) as Since1Hour GO -- -------------------------------------------------------------------------------------------- -- Shorthand to generate cr/lf -- -------------------------------------------------------------------------------------------- Create Or Alter Function S#.Nl () -- pour alléger écriture des concaténations ajout de saut de ligne Returns nvarchar(max) as Begin return (nchar(13)+nchar(10)) End /*===KeyWords=== Constants ===KeyWords===*/ GO Create Or Alter View S#.Enums -- alter extra is allowed from SQL2016 as Select -- useful, general use constants to generate query or display them cr , Lf , NL = cr+Lf , SQuote='''' , DQuote='"' , Dot='.' , SizesNameInNumbers.* -- constants for S#.ScriptCodeToScriptRowData , ScriptCodeToScriptRowData@InsertsFromAliasedVal = 1 , ScriptCodeToScriptRowData@InsertsFromValuesList = 2 , ScriptCodeToScriptRowData@SelectFromAliasedVal = 3 , ScriptCodeToScriptRowData@SelectFromValuesList = 4 , ScriptCodeToScriptRowData@CsvRows = 5 , ScriptCodeToScriptRowData@AddGo=99 -- constants for managing S#.ScriptManageEventSession , ExtendedEventSession@Start = 1 , ExtendedEventSession@Stop = 2 , ExtendedEventSession@Clear = 3 -- Constants for Help Module , Help@Keywords=0 , Help@ModuleNameOnly=1 , Help@AllInfo=2 -- Constants for -- Constant for runScript , RunScript@ErrMsgTemplate = '---------------------------------------------------------------------------------------------- Error from S#.RunScript when running script above ---------------------------------------------------------------------------------------------- Msg: #ErrMessage# Error: #ErrNumber# Severity: #ErrSeverity# State: #ErrState##atPos# ---------------------------------------------------------------------------------------------- ' From (Select Cr = Nchar(13), Lf = Nchar(10)) as CrLf -- real values in number of bytes or storage capacity names Cross Apply ( Select * From (Select KB=convert(bigInt,1024)) as Kb cross apply (Select KiloByte=Kb) as KiloByte cross apply (Select MB=Kb*kb) as Mb cross apply (Select MegaByte=Mb) as MegaByte cross apply (Select GB=Mb*kb) as Gb cross apply (Select Gigabyte=Gb) as Gigabyte cross apply (Select TB=Gb*Kb) as Tb cross apply (Select Terabyte=Tb) as Terabyte cross apply (Select PB=Tb*Kb) as Pb cross apply (Select Petabyte=Pb) as Petabyte cross apply (Select EB=Pb*Kb) as Eb cross apply (Select ExaByte=Eb) as ExaByte cross apply (Select ZB=Pb*Kb) as Zb cross apply (Select ZettaByte=Zb) as ZettaByte cross apply (Select YB=Pb*Kb) as Yb cross apply (Select Yottabyte=Yb) as Yottabyte ) as SizesNameInNumbers /*===Purpose=== This view is an handy way to define constants that define specific parameters or more generally more useful constant. By cross joining this view, all constants becomes accessible to queries using them, at no cost. ***BONUS***: When using S#.Enums intellisence helps to find constants for a given module easily when they are in the form Modulename@option, just by starting typing module name ===Purpose===*/ /*===Samples=== -- Run this sample by setting query results to text -- When query results are in grid mode carriage return are invibible -- but can be copied if SQL Server Management Studio options -- \query results\Sql Server\Results to grid\Retain CR/LF on copy or save is checked Select MultiLineText From S#.Enums -- to have access to "newline" Nl constant CROSS APPLY (Select MultiLineText='First line'+Nl+'Second line'+Nl+'Third line' ===Samples===*/ /*===KeyWords=== Enumeration,constants,parameters ===KeyWords===*/ GO -- ------------------------------------------------------------------------------ -- Extended Charindex which can search a string backward from end of another string -- when startPos is made negative -- If gives leftPos and rightPos of the searched string found -- ------------------------------------------------------------------------------ Create Or Alter Function S#.CharindexEx(@srch nvarchar(max), @str nvarchar(max), @startPos Integer) returns Table as -------------------------------------------------------------------------- -- Useful function to allow searching strings in both directions forward or vackward -- @@MARK: string handling - search -------------------------------------------------------------------------- Return ( Select Trc.* From (Select s=@str, srch=@srch, StartPos=ISNULL(@StartPos,1)) as Prm CROSS APPLY ( Select * From ( Select startPos , s , dStr=IIF(StartPos>=0,s,Reverse(s)) , srch , dSrch=IIF(StartPos>=0,srch,Reverse(srch)) , lgStr=Len(s) , lgSrch=Len(srch) ) as E CROSS APPLY (Select StrPos=Charindex(dSrch, dStr, Abs(startPos))) as dPos -- searches from start OUTER APPLY (Select LeftPosOfStrP = StrPos Where StrPos>0 And StartPos>0) as LeftOfStr OUTER APPLY (Select RightPosOfStrP = LeftPosOfStrP+LgSrch-1 Where LeftPosOfStrP Is Not NULL) as RightPosOfStrP -- searches from end OUTER APPLY (Select LeftPosOfStrN = (LgStr-StrPos+1-lgSrch+1) Where StrPos>0 And StartPos<0) as LeftPosOfStrN OUTER APPLY (Select RightPosOfStrN = LgStr-StrPos+1 Where LeftPosOfStrN Is Not NULL) as RightOfStrN -- choose search result (note they are null as soon as they are not of the direction asked (negative=from end), (>0=from start)) CROSS APPLY (Select LeftPos=COALESCE(LeftPosOfStrP, LeftPosOfStrN, 0)) as LeftPos CROSS APPLY (Select RightPos=COALESCE(RightPosOfStrP, RightPosOfStrN, 0)) as RightPos ) as Trc ) /*===KeyWords=== String, Parsing ===KeyWords===*/ GO Create Or Alter Function S#.ParseFileParts (@FileExp as Nvarchar(512)) Returns Table As -------------------------------------------------------------------------- -- Useful function to get file parts -- @@MARK: manage files - get file parts -------------------------------------------------------------------------- Return Select * From (Select FileExp=@FileExp) as FileExp OUTER APPLY (Select Drive=LEFT(fileExp,2) Where FileExp LIKE '[a-z]:%') As Drive OUTER APPLY (Select StartPath=CHARINDEX('\', FileExp) Where FileExp LIKE '%\%') as StartPath CROSS APPLY (Select LgFileExp=LEN(FileExp)) as LgFileExp -- If there is no '\' endPath is at best equal to startPath OUTER APPLY (Select EndPath=LeftPos From S#.CharindexEx('\', FileExp, -1)) As EndPath -- Path start at position of first '\', otherwise PathWithNoDrive becomes null OUTER APPLY (Select DirAlone=SUBSTRING(fileExp, StartPath, EndPath-StartPath+1) Where StartPath is Not NULL) as DirAlone -- The Dot that precedes the extension must be the last one, and must not be followed by '\' OUTER APPLY (Select LastDot=LeftPos From S#.CharindexEx('.', FileExp, -1) Where FileExp LIKE '%.%' And FileExp Not Like '%.%\%') as LastDot -- If a LastDot that obey above conditions, extension follows it till the end of the file name OUTER APPLY (Select Ext=SUBSTRING(FileExp, LastDot+1, LgFileExp)) as Ext -- file name without drive is needed CROSS APPLY (Select DriveRemoved=IIF(Drive IS NULL, FileExp, STUFF(FileExp,1,2,''))) as DriveRemoved -- file name without drive and path si needed CROSS APPLY (Select DriveAndPathRemoved=IIF(DirAlone IS NULL, DriveRemoved, STUFF(DriveRemoved,1,Len(DirAlone),''))) as DriveAndPathRemoved -- if file name without drive and path isn't empty this is a file name OUTER APPLY (Select FileName=DriveAndPathRemoved Where DriveAndPathRemoved<>'') as FileName -- Si l'extension est NULL, l'expression avant suffit, sinon on le reitre de l'expression avant, NULL ne fait pas sauter LEFT OUTER APPLY (Select ExtLessName=IIF(Ext IS NULL, FileName, LEFT(FileName, Len(FileName)-Len(Ext)-1 ))) as NameNoExt -- On obtient le path et le drive en substituant par '' les valeurs manquantes, si au moins une des deux valeurs y sont OUTER APPLY ( Select DriveAndPath=ISNULL(Drive,'')+ISNULL(DirAlone,'') Where Drive IS NOT NULL OR DirAlone IS NOT NULL ) as FileExpWithNoFileName /*===Purpose=== Parse multiple file parts ===Purpose===*/ /*===Samples=== SELECT TestFile, P.fileExp, P.Drive, P.DirAlone, P.DriveRemoved, P.DriveAndPathRemoved, P.DriveAndPath, P.FileName, P.ExtLessName, P.Ext FROM ( Values -- test data ('C:\Windows\System32\Drivers\Etc\Hosts.Sam') , ('C:\Etc\Hosts') , ('C:Hosts') , ('C:Hosts.Sam') , ('C:') , ('C:\') , ('C:\FalseDot.Extension\Hehe') , ('C:\FalseDot.Extension\') , ('C:\FalseDot.\') , ('Hosts') , ('\Windows\System32\Drivers\Etc\Hosts') , (NULL) ) as Test(TestFile) OUTER APPLY S#.ParseFileParts (Test.TestFile) as P ===Samples===*/ /*===KeyWords=== String ===KeyWords===*/ GO -- @@MARK: Generating code - Get code Template from comments (in batch, sp or other SQL module), so no more worrring about quote doubling even for nest code Create Or Alter Function [S#].[GetCmtBetweenDelim] (@delim sysname, @ModuleRef Sql_Variant) Returns table as ----------------------------------------------------------------------------------------------- -- Get text between start /*Delim and Delim*/ where Delim is a parameter -- from source text which can -- be the current batch is @moduleRef is NULL -- the calling query if moduleRef = '' -- the definition of the function/view/proc for which moduleRef is a name (varchar) -- the definition of the function/view/proc for which object_id passed as a parameter ----------------------------------------------------------------------------------------------- Return ( Select * From (Select Delim=@delim, ModuleRef=@ModuleRef) as ModuleRef CROSS APPLY (Select Typ=IIF(ModuleRef IS NULL, NULL, Convert (nvarchar(30), sql_variant_property(ModuleRef, 'BaseType')))) as Typ OUTER APPLY (Select ObjId=Convert(int, moduleRef) Where Typ = 'int') as ObjId OUTER APPLY (Select PrmIsName=Convert(sysName,moduleRef) Where Typ Like '%varchar') as PrmIsName -- GET A MODULE NAME THROUGH OBJECT_ID or IF NAME EXACTLY in OBJECT REF -- module name Will be null if moduleRef is set to NULL on purpose (get source text from running batch or proc) -- or if invalid object id is passed OUTER APPLY ( Select moduleName=QuoteName(Object_Schema_name(ObjId))+'.'+QuoteName(Object_name(ObjId)) Where ObjId IS NOT NULL UNION ALL Select ModuleName=PrmIsName Where PrmIsName Is NOT NULL ) as ModuleName -- select from three different methods to get source code text to parse. -- source text can comme from : 1) a sql module, 2) The calling query, 3) from either the batch or the running proc OUTER APPLY ( -- The module text like a view or a inline function -- SrcTxt can be NULL if ModuleName isn't NULL but do not exists (no valid object_id), Select SrcTxt=OBJECT_DEFINITION(Object_Id(ModuleName)) collate database_default Where Typ Is NOT NULL And ModuleName IS Not NULL And ModuleName <> '' UNION ALL -- The top call stack of calling SQL, when moduleName is an empty string Select SrcTxt=event_info Collate Database_default From sys.dm_exec_input_buffer(@@spid, null) Where Typ Is NOT NULL And ModuleName = '' And event_type = 'Language Event' UNION ALL -- the current batch, or the current bacth of a running stored proc, when moduleRef is NULL -- when typ is NULL, this is because there is NULL for moduleRef param. Select SrcTxt=qt.text Collate Database_default From sys.dm_exec_requests er Cross Apply sys.dm_exec_sql_text(er.sql_handle) as qt Where Typ IS Null And er.session_id = @@SPID ) as SrcTxt CROSS APPLY (Select LgCt=LEN(Delim) ) as LgCt -- if srcTxt is NULL, startIndex, EndIndex are going to be null and Found will be null CROSS APPLY (Select StartIndex=charindex('/*'+Delim, SrcTxt)) As StartIndex CROSS APPLY (Select EndIndex=charindex(Delim+'*/', SrcTxt)-1 ) as EndIndex OUTER APPLY (Select Found=1 Where StartIndex>0 And EndIndex>0 And LgCt>0) as Found OUTER APPLY -- compute returns when everything is ok ( Select * FROM (Select StartP=StartIndex+LgCt+2 ) As StartP CROSS APPLY (Select EndP=EndIndex ) as EndP CROSS APPLY (Select DelimTxtFound=Cast(Substring(SrcTxt, StartP, EndP-StartP+1) as nvarchar(max))) as DelimTxtFound Where Found=1 ) as FoundOk -- in case input is invalid as when start and/or end comment ar missing CROSS JOIN (Select IdFct='!S#.GetCmtBetweenDelim: ') as IdFct OUTER APPLY (Select InvalidDelimMsg=IdFct+'Comment Delim is null!' Where Delim is NULL) as InvalidDelimMsg OUTER APPLY ( Select InvalidModuleMsg=IdFct+'No text to search because inexisting module'+moduleName+'!' Where found IS NULL And PrmIsName IS NOT NULL And SrcTxt IS NULL UNION ALL Select InvalidModuleMsg=IdFct+'No text to search because inexisting module id '+CONVERT(nvarchar, ObjId)+'!' Where found IS NULL And Typ = 'Int' And SrcTxt is NULL ) as InvalidModuleMsg OUTER APPLY (Select NotFoundMsg=IdFct+'No comment text between start Delim /*'+Delim+' and end Delim '+Delim+'*/!' Where found IS NULL) as NotFound CROSS APPLY (Select FailedGetTemplate=Coalesce(InvalidDelimMsg, InvalidModuleMsg, NotFoundMsg)) as FailedGetTemplate CROSS APPLY (Select TxtInCmt=Coalesce(FailedGetTemplate, DelimTxtFound)) as ResGet /*===Purpose=== This function is to get some SQL Code wrapped in a multiline comment that must starts and ends by a Delim of your choice /*"Delim" and end by "Delim"*/ It simplifies coding of code definition or code template instead of using litteral strings by having not to care about quotes. The comment may be extracted depending @Moduleref Sql_Variant parameter from: ■ The running batch or directly in a stored procedure. ■ @@ProcId (id of the running SP) ■ Any SQL module name including itself (like a stored proc, function, Trigger Or View) ■ The topmost calling SQL (ex: a comment in the batch starting before the called stored proc) This function have a very extensive use in this library! ===Purpose===*/ -- Test code to Keep! --Select Sql --From -- ( -- Select FctName=QuoteName(Object_schema_name(Object_id))+'.'+QuoteName(Object_name(Object_id)), type_desc, type -- From Sys.Objects -- Where type_desc like '%Function%' And Type_Desc Not like 'Clr%' -- ) As FctName -- Cross Apply S#.GetCmtBetweenDelim('===DropAllFct===', NULL) As C -- /*===DropAllFct=== -- Drop Function if exists #FctName# -- ===DropAllFct===*/ -- CROSS Apply (Select Sql=Replace(C.TxtInCmt, '#FctName#', FctName)) as Sql --go --------------------------------------------------------------------------- --Drop Function if exists dbo.ScriptFunctionDrops --go --Create Or Alter Function dbo.ScriptFunctionDrops () --returns table --as --return --Select Sql --From -- ( -- Select FctName=Object_schema_name(Object_id)+'.'+Object_name(Object_id), type_desc, type -- From Sys.Objects -- Where type_desc like '%Function%' And Type_Desc Not like 'Clr%' -- And Object_id <> Object_Id('dbo.ScriptFunctionDrops') -- dont drop myself -- ) As FctName -- -- instead of null for @moduleRef, use self function name. -- Cross Apply S#.GetCmtBetweenDelim('===DropAllFct===', 'dbo.ScriptFunctionDrops') As C -- /*===DropAllFct=== -- Drop Function If exists #FctName# -- ===DropAllFct===*/ -- CROSS Apply (Select Sql=Replace(C.TxtInCmt, '#FctName#', FctName)) as Sql --go --Select * from dbo.ScriptFunctionDrops() --go --Drop Function if exists dbo.ScriptFunctionDrops --go --------------------------------------------------------------------------- --drop Proc if exists dbo.DropsFunctions --go --Create Or Alter Proc dbo.DropsFunctions --as --Begin -- Declare @Sql Nvarchar(max) = '' -- Select @sql=@sql+Sql -- From -- ( -- Select FctName=Object_schema_name(Object_id)+'.'+Object_name(Object_id), type_desc, type -- From Sys.Objects -- Where type_desc like '%Function%' And Type_Desc Not like 'Clr%' -- And Object_id <> Object_Id('dbo.DropsFunctions') -- dont drop myself -- ) As FctName -- -- instead of null for @moduleRef, use system variable that give current procedure id -- Cross Apply S#.GetCmtBetweenDelim('===DropAllFct===', @@ProcId) As C -- /*===DropAllFct=== -- Drop Function if exists #FctName# -- ===DropAllFct===*/ -- CROSS Apply (Select Sql=Replace(C.TxtInCmt, '#FctName#', FctName)) as Sql -- Print @Sql --End --go --Exec dbo.DropsFunctions --go --Drop proc if exists dbo.DropsFunctions --go ------------------------------------------------------------------------------------ ---- KEEP THIS COMMENT, THIS IS A TEST ASSERTION FOR THIS FUNCTION ---- Test fail When this bunch of select + union all are selected and executed and returns something ---- Every select has a fail condition which resolves to true is the result is incorrect --------------------------------------------------------------------------------------- --Select Test=1, * from S#.GetCmtBetweenDelim (NULL, 'S#.GetCmtBetweenDelim') --Where isnull(TxtInCmt,'') <> '!S#.GetCmtBetweenDelim: Comment Delim is null!' --union all --Select Test=2,* from S#.GetCmtBetweenDelim ('Haha', NULL) /*HeheTextBetweenHehe*/ ---- not like with ____ to avoid finding itself in where condition --Where isnull(TxtInCmt,'') not like '!S#.GetCmtBetweenDelim: No comment text between start Delim /*____ and end Delim ____*/!' --union all --Select Test=3,* from S#.GetCmtBetweenDelim ('Haha', 'S#.GetCmtBetweenDelim') /*HeheTextBetweenHehe*/ ---- not like with ____ to avoid finding itself in where condition --Where isnull(TxtInCmt,'') not like '!S#.GetCmtBetweenDelim: No comment text between start Delim /*____ and end Delim ____*/!' --union all --Select Test=4,* from S#.GetCmtBetweenDelim ('Hehe', NULL) /*HeheTextBetweenHehe*/ --Where isnull(TxtInCmt,'') <> 'TextBetween' --union all --Select Test=5,* from S#.GetCmtBetweenDelim ('Hehe', 'S#.GetCmtBetweenDelim') --Where isnull(TxtInCmt,'') <> 'TextBetween' ------------------------------------------------------------------------------------------------------------- ) --S#.GetCmtBetweenDelim GO -- @@MARK: Generating code - Multiple replaces in one call, using Json key pair list Create Or Alter Function S#.MultipleReplaces (@Template nvarchar(max), @JsonDataSource Nvarchar(max)) Returns Table as Return With TagSrc as ( Select RowKey -- AttributeRowSeq is useful as it allows to specify a replace order from JsonDataSource attribute by relative position -- example If tagSrc='This is #F#', and JsonDataSource contains '[{"F":"contains #b#","B":"b contains #c#,"C":"last"}]' -- replace seq will be: 'This is #F#' -> 'This is contains #b#' -> 'This is contains last' , AttributeRowSeq = Row_number() Over (Partition by Rowkey Order By Pos) , AttributeNbOfRow = COUNT(*) Over (Partition by RowKey) , AttributeTag = '#'+[Key]+'#' collate database_default , AttributeValue = isnull(Value,'') collate database_default -- tags with no values are replaced by empty string , rowvalue , SrcTemplate From (Select JsonDataSource=@JsonDataSource, SrcTemplate=@Template) as Prm -- for internal testing use the alternate Prm derived table below and comment the above one --( --Select -- SrcTemplate='Backup #DbName# TO Disk =''#path#'' With #appendOption#' --, JsonDataSource=(Select dbName='Msdb', path='C:\SQL\Bkps', AppendOption='Init' For JSON Path) --) as Prm CROSS APPLY (select Rowkey=[key], RowValue=Value, Pos=CharIndex('"'+[Key]+'":', JsonDataSource) From openjson (JsonDataSource)) as Rows cross apply openJson (RowValue) ) , TagReplacements As ( Select RowKey, AttributeRowSeq, AttributeNbOfRow, AttributeTag, AttributeValue, LastReplace=Cast (REPLACE (SrcTemplate, AttributeTag, AttributeValue) as nvarchar(max)) From tagSrc Where AttributeRowSeq = 1 UNION ALL Select T.Rowkey, T.AttributeRowSeq, T.AttributeNbOfRow, T.AttributeTag, T.AttributeValue, LastReplace=Cast (REPLACE (LastReplace, T.AttributeTag, T.AttributeValue) as nvarchar(max)) From (Select Rowkey, LastReplace, LastAttributeRowSeq=AttributeRowSeq From TagReplacements) as Prev JOIN TagSrc AS T On T.RowKey = Prev.RowKey And T.AttributeRowSeq = Prev.LastAttributeRowSeq+1 ) Select replacedTxt=LastReplace from TagReplacements Where AttributeNbOfRow = AttributeRowSeq -- -------------------------------------------------------------------------- -- very useful function for turning templates into real code -- S#.GetCmtBetweenDelim get template and cross apply it to this function -- use json key pairs. For example SomeTagName="value" replace #SomeTagName# by its "Value" -- -------------------------------------------------------------------------- /*===KeyWords=== Scripting ===KeyWords===*/ GO -- @@MARK: Generating code - Get template and do multiple replaces from Json key pair list (combination of S#.GetCmtBetweenDelim and S#.MultipleReplaces) Create Or Alter Function S#.GetTemplateFromCmtAndReplaceTags (@delim sysname, @CmtSource Sql_Variant, @JSonDataSource Nvarchar(max)) ------------------------------------------------------------------------------------- -- combine two useful functions for code template processing -- S#.GetCmtBetweenDelim and S#.MultipleReplaces -- -- See @Delim parameter in S#.GetCmtBetweenDelim for more details -- See @JsonDataSource parameter in S#.MultipleReplaces for more details -- -- Find template code in comment by use of S#.GetCmtBetweenDelim -- Process multiple replaces in the template code by use of S#.MultipleReplaces -- -- This function is useful to get a template code from a comment and replace tags in it -- @jSonDataSource is a json string that contains "tagName", "TagValue" pairs -- tags are expressed as #TagName# in source code and are replaced by "TagValue" -- It is cleaner to produce the @jsonDataSource with a cross apply query that returns -- a single row with a single column in the form of -- Cross Apply (select jsonDataSource=(Select * from (Values ('tag1', 'value1'), ('tag2', 'value2')) as t(tag, value) for json auto)) as JsonDataSource -- Avoid the use of the option Without_array_wrapper in the JSON expression (array wrappers are needed for openjson) -- -- Code is the useful column, the other are useful for diagnosis in case of problems. ------------------------------------------------------------------------------------- Returns table As Return Select Code=Code.replacedTxt , Prm.Delim , Prm.CmtSource , Template.moduleName , FailedGetTemplate , TxtInCmt , Prm.JSonDataSource From (Select Delim=@Delim, CmtSource=@CmtSource, JSonDataSource=@JSonDataSource) as Prm CROSS APPLY S#.GetCmtBetweenDelim (Prm.delim, Prm.CmtSource) as Template OUTER APPLY (Select * From S#.MultipleReplaces (Template.TxtInCmt, Prm.JsonDataSource) Where FailedGetTemplate IS NULL) as Code GO -- ------------------------------------------------------------------------------------------------------------- -- Function return full object name of a specific object_id. Very useful to get fully qualified -- object name from an object_id or from object_id function. -- ------------------------------------------------------------------------------------------------------------- Create Or Alter Function S#.FullObjName ( @object_id Int ) Returns sysname as Begin Return (QUOTENAME(object_schema_name(@Object_id))+'.' +QUOTENAME(object_name(@Object_Id))) End /*===KeyWords=== Scripting,Object Management ===KeyWords===*/ -- Select S#.FullObjName (object_id) From sys.Objects GO -- Ensure there is at least a S#.LogTable function If Object_id('S#.LogTable') IS NULL Exec ('Create Or Alter Function S#.LogTable() Returns sysname as Begin Return('''') End') GO -- @@MARK: Running batch of generated code -- Set Log table -- ------------------------------------------------------------------------------------------- -- Register log table for RunScriptToRun and create a function that returns its name -- ------------------------------------------------------------------------------------------- Create Or Alter Procedure S#.RegisterLogTable @LogTable sysname = NULL As Begin Set Nocount On --Declare @LogTable sysname = 'S#.YourSqlDbaInstallLog' Declare @Self Int = @@procId -- to test code below, replace @@procId by NULL Declare @Sql nvarchar(max) Select @Sql=B.TxtInCmt From -- instead of S#.GetTxtBtwnTagCmt S#.GetCmtBetweenDelim('===DropLogTableFunction===', @Self) as B /*===DropLogTableFunction=== If Object_Id('S#.LogTable') IS NOT NULL DROP Function S#.LogTable ===DropLogTableFunction===*/ Exec (@sql) Select @Sql=CreateLogTable From S#.GetCmtBetweenDelim('===CreateLogTable===', @Self) as B CROSS APPLY (Select CreateLogTable=Replace(B.TxtInCmt, '#logTable#', @LogTable) ) as vCreateLogTable /*===CreateLogTable=== Drop Function If Exists S#.logTable Drop Table If Exists #logTable# Create table #logTable# (Line nvarchar(max), seq int identity, batchNo BigInt) Create index [i#logTable#] on #logTable# (batchNo desc) ===CreateLogTable===*/ Where isnull(@logTable,'') <> '' Exec (@Sql) Select @Sql=CreateLogTableFunction From S#.GetCmtBetweenDelim('===CreateLogTableFunction===', @Self) as B CROSS APPLY (Select CreateLogTableFunction=Replace(B.TxtInCmt, '#logTable#', isnull(@logTable,'') ) ) as vCreateLogTableFunction /*===CreateLogTableFunction=== Create Or Alter Function S#.LogTable () Returns sysname as Begin Return('#logTable#') End ===CreateLogTableFunction===*/ Exec (@Sql) --Exec S#.RegisterLogTable 'aLogTable'-- test it /*===KeyWords=== Scripting,Logging ===KeyWords===*/ End GO Exec S#.RegisterLogTable 'S#.YourSqlDbaInstallLog' GO Create Or Alter Procedure S#.PrintAndLog @LogText Nvarchar(Max) -- text to log , @nextBatch Int = 0 -- As Begin Set Nocount On Declare @SqlToPrintAndLog nvarchar(max) Select @SqlToPrintAndLog=Sql From (Select LogTable=S#.LogTable()) as LogTable CROSS APPLY(Select PrintPart='Print @LogText') as PR OUTER APPLY ( Select PrintAndLog=PrintPart+AndLog From S#.GetCmtBetweenDelim('===AndLog===', @@PROCID) as G CROSS APPLY (Select AndLog=Replace(G.TxtInCmt, '#LogTable#', LogTable)) as AndLog /*===AndLog=== Insert into #LogTable# (line, batchNo) Select LogText, BatchNo From (Select LogText=@LogText) as vLogText OUTER APPLY (Select Top 1 BatchNumberInLogTable=BatchNo From #logTable# Order By BatchNo Desc) as BatchNumberInLogTable CROSS APPLY (Select BatchNo=ISNULL(BatchNumberInLogTable, 0) + @NextBatch) as vBatchNo ===AndLog===*/ Where LogTable <> '' ) as AL CROSS APPLY (Select Sql=COALESCE(PrintAndLog, PrintPart)) as Sql Exec Sp_ExecuteSql @SqlToPrintAndLog, N'@LogText Nvarchar(Max), @nextBatch Int', @LogText, @nextBatch /* Exec S#.RegisterLogTable '' Exec S#.PrintAndLog 'test text', 0 Exec S#.RegisterLogTable 'S#.LogTest' Exec S#.PrintAndLog 'test text', 1 Exec('Select * from S#.LogTest') Exec S#.PrintAndLog 'next test text', 0 Exec('Select * from S#.LogTest') Exec S#.PrintAndLog 'next batch test text', 1 Exec('Select * from S#.LogTest') */ End /*===KeyWords=== Scripting,Logging ===KeyWords===*/ GO -- @@MARK: Running batch of generated code -- return SQL on multiple lines for readability -- ------------------------------------------------------------------------------------------- -- Help printing out SQL Code, by dividing lines at cr/lf -- Workaround for SQL Print output limited to 8000 chars in SQL Management Studio -- ------------------------------------------------------------------------------------------- Create or Alter Function S#.SplitSqlCodeLinesIntoRows(@Sql Nvarchar(Max)) returns @TxtSql table (LineNum int, Line nvarchar(max) collate database_default) As Begin If @Sql Is Null Or @Sql = '' Return -- normalize line ends Set @Sql = REPLACE(@Sql, NCHAR(13) + NCHAR(10), NCHAR(10)) Set @Sql = REPLACE(@Sql, NCHAR(13), NCHAR(10)) Declare @Start Int, @End Int, @Line Nvarchar(Max), @EolPos Int, @LineNo Int Set @Start = 1 Set @End=0 Set @LineNo = 0 While(@End < LEN(@Sql)) Begin Set @EolPos = CHARINDEX(NCHAR(10), @Sql, @Start) Set @End = Case When @EolPos > 0 Then @EolPos Else LEN(@Sql)+1 End -- End of String @Sql Set @LineNo = @LineNo + 1 insert into @TxtSql (LineNum, Line) Values (@lineNo, ISNULL(SUBSTRING(@Sql, @Start, @End-@Start),'')) Set @Start = @End+1 End Return -- Select * From S#.SplitSqlCodeLinesIntoRows(Object_definition(object_id('S#.ColInfo'))) as r -- Select * From S#.SplitSqlCodeLinesIntoRows(Object_definition(object_id('S#.SplitSqlCodeLinesIntoRows'))) as r End /*===KeyWords=== Scripting,Logging ===KeyWords===*/ GO -- @@MARK: Running batch of generated code -- return SQL on multiple lines (numbered) for readability Create Or Alter Function S#.SplitSqlCodeInNumberedRowLines(@Sql Nvarchar(Max)) Returns table as Return (select LineNum, '/* '+STR(LineNum,5)+' */'+Line as Line from S#.SplitSqlCodeLinesIntoRows(@Sql)) -- Select * From S#.SplitSqlCodeInNumberedRowLines(Object_definition(object_id('S#.ColInfo'))) as r /*===KeyWords=== Scripting,Logging ===KeyWords===*/ GO -- ------------------------------------------------------------------------------------------- -- Wrap around previous function in which error messages are not allowed -- This procedure add error message if text of the query to print is found NULL -- and add a workaround to help push out faster print output to client -- ------------------------------------------------------------------------------------------- -- @@MARK: Running batch of generated code -- print SQL on multiple lines for readability Create Or Alter Procedure S#.PrintSplittedSqlCode @Sql Nvarchar(Max) , @Label Nvarchar(Max) = Null , @NextBatch Int = 1 As Begin Set Nocount On Declare QueryCursor Cursor Local FORWARD_ONLY For Select Line='/* ' + @Label + ' */', LineNum=1, NextBatch=1 -- when a Label print this implies beginning of a new SQL Batch Where @Label IS NOT NULL Union All Select NullSqlMsg='Bug : Null Sql in @Sql parameter!!', LineNum=2, NextBatch=0 Where @Sql IS NULL UNION ALL Select L.Line, L.LineNum, NextBatch=NextBatchForFirstQryOnly From -- NextBatch is turned off when a label is printed because before printing label implies that -- NextBatch is also turned off as soon a the first line is printed S#.SplitSqlCodeInNumberedRowLines(@Sql) as L Cross Apply (Select NextBatchForFirstQryOnly=IIF(L.LineNum > 1 Or @Label IS NOT NULL, 0, @NextBatch)) as vNextBatch Where @Sql Is NOT NULL -- don't do useless call Order By LineNum Open QueryCursor Declare @line nvarchar(max), @LineNum Int While(1=1) Begin Fetch Next From QueryCursor Into @line, @LineNum, @nextBatch If @@FETCH_STATUS <> 0 Break Exec S#.PrintAndLog @line, @NextBatch End Raiserror ('',10,1) With NoWait -- help to finalize print output Close QueryCursor Deallocate QueryCursor /* Declare @Sql nvarchar(max) = ' Select * From UnitTest ' -- just print no log Exec S#.RegisterLogTable '' Exec S#.PrintSplittedSqlCode @Sql, '*** label ****', 1 -- print and log now Exec S#.RegisterLogTable 'S#.LogTest' Exec('Select * from S#.LogTest') -- see if it is still true -- print label text followed by SQL text, no line numbering on label, line numbering starts after. BatchNO remains the same Exec S#.PrintSplittedSqlCode @Sql, '*** label ****', 1 Exec('Select * from S#.LogTest') -- see if it is still true -- append another SQL query to existing batch, BatchNo Stays the same Exec S#.PrintSplittedSqlCode 'Select * into #StillInSameSqlBatch From AnotherTable ', NULL, 0 Exec('Select * from S#.LogTest') -- see if it is still true -- start a new batch BatchNo it'll grow by 1 and be equal to 2 Exec S#.PrintSplittedSqlCode @Sql, NULL, 1 Exec S#.PrintAndLog 'log this message in same batch', 0 Exec('Select * from S#.LogTest') -- see if it is still true */ /*===KeyWords=== Scripting,Logging ===KeyWords===*/ End GO ----------------------------------------------------------------------- -- accepts a error message template and replace tags values associated -- with corresponding parameters which comes from error functions -- like ERROR_NUMBER(), ERROR_SEVERITY(), ERROR_STATE(), ERROR_LINE(), ERROR_PROCEDURE(), ERROR_MESSAGE() -- This function is useful to format error messages in a consistent way -- and takes care of adding extra information, if the error comes from a batch or -- stored procedure, it adds the line number and the procedure name -- to the error message, it they aren't null -- help to improve consistency in error messages ------------------------------------------------------------------------ -- @@MARK: Running batch of generated code -- Error message reporting Create Or Alter Function S#.FormatRunTimeMsg ( @MsgTemplate Nvarchar(max) , @error_number Int , @error_severity Int , @error_state int , @error_line Int , @error_procedure nvarchar(128) , @error_message nvarchar(4000) ) Returns Table as Return ( Select * From ( Select ErrorMsgFormatTemplate = '---------------------------------------------------------------------------------------------- -- Msg: #ErrMessage# -- Error: #ErrNumber# Severity: #ErrSeverity# State: #ErrState##atPos# ----------------------------------------------------------------------------------------------' Where @MsgTemplate IS NULL Union All Select ErrorMsgFormatTemplate = @MsgTemplate Where @MsgTemplate IS NOT NULL ) as MsgTemplate CROSS APPLY (Select ErrMessage=ISNULL(@error_message, 'No Err Message')) as ErrMessage CROSS APPLY (SeLect ErrNumber=ISNULL(CAST(@error_number as nvarchar), 'No Err Number')) as ErrNumber CROSS APPLY (SeLect ErrSeverity=@error_severity) as ErrSeverity CROSS APPLY (SeLect ErrState=@error_state) as ErrState CROSS APPLY (SeLect ErrLine=@error_line) as ErrLine CROSS APPLY (SeLect ErrProcedure=@error_procedure) as vStdErrProcedure Cross Apply (Select FmtErrMsg0=Replace(ErrorMsgFormatTemplate, '#ErrMessage#', ErrMessage) ) as FmtStdErrMsg0 Cross Apply (Select FmtErrMsg1=Replace(FmtErrMsg0, '#ErrNumber#', ErrNumber)) as FmtErrMsg1 Cross Apply (Select FmtErrMsg2=Replace(FmtErrMsg1, '#ErrSeverity#', CAST(ErrSeverity as nvarchar)) ) as FmtErrMsg2 Cross Apply (Select FmtErrMsg3=Replace(FmtErrMsg2, '#ErrState#', CAST(ErrState as nvarchar)) ) as FmtErrMsg3 Cross Apply (Select AtPos0=ISNULL(' at Line:'+CAST(ErrLine as nvarchar), '') ) as vAtPos0 Cross Apply (Select AtPos=atPos0+ISNULL(' in Sql Module:'+ErrProcedure,'')) as atPos Cross Apply (Select ErrMsg=Replace(FmtErrMsg3, '#atPos#', atPos) ) as FmtErrMsg /*======= CODE INSIDE THIS COMMENT ARE FOR MANUAL TESTS OF S#.FormatRunTimeMsg AND MUST BE KEPT!!! ================ BEGIN TRY -- Generate a divide-by-zero error. SELECT 1/0; END TRY BEGIN CATCH Select * From (Select MsgTemplate= '---------------------------------------------------------------------------------------------- -- Msg: #ErrMessage# -- Error: #ErrNumber# Severity: #ErrSeverity# State: #ErrState##atPos# ----------------------------------------------------------------------------------------------' ) as MsgTemplate CROSS APPLY S#.FormatRunTimeMsg (MsgTemplate, ERROR_NUMBER (), ERROR_SEVERITY(), ERROR_STATE(), ERROR_LINE(), ERROR_PROCEDURE (), ERROR_MESSAGE ()) as Fmt END CATCH; ======= Keep this comment for manual tests!!! =====*/ ) /*===KeyWords=== Scripting,Logging ===KeyWords===*/ GO -- Function to generate a formatted error message for the most recent runtime error using the current error context. -- Parameters: -- @MsgTemplate: The custom error message template (if NULL, uses the default template). -- Returns: -- A table containing the formatted error message using the latest error context (ERROR_* functions). -- This function is the one mostly typically used for default error messaging -- @@MARK: Running batch of generated code -- Error message reporting - With user defined msgTemplate or default CREATE OR ALTER FUNCTION S#.FormatCurrentMsg (@MsgTemplate nvarchar(4000)) Returns table as Return Select * From S#.FormatRunTimeMsg ( @MsgTemplate, -- Custom template for error message formatting. ERROR_NUMBER(), -- Number of the last error that occurred in the session. ERROR_SEVERITY(), -- Severity of the last error. ERROR_STATE(), -- State code of the last error. ERROR_LINE(), -- Line number of the last error. ERROR_PROCEDURE(), -- Procedure or function where the last error occurred. ERROR_MESSAGE() -- Text message of the last error. ) as Fmt /* -- quick test artefact Begin try Select 1/0 ENd try Begin Catch Declare @Msg Nvarchar(max) = (Select ErrMsg From S#.FormatCurrentMsg(null)) Raiserror (@Msg, 11,1 ) End Catch */ GO -------------------------------------------------------------------------------------------------------- -- for a given message format, put typical values that can be returned from ERROR_functions of catch -- or other sources (like events) -- typically use with ERROR_functions from catch block matching parameter names -- WARNING : THIS FUNCTION MUST BE CAREFULLY MANUALLY TESTED, BECAUSE IT IS HEAVILY USED IN AUTOMATIC -- TESTS FOR THIS LIBRARY -------------------------------------------------------------------------------------------------------- -- @@MARK: Execute DBCC and report errors Create Or Alter Procedure S#.PerformCheckDbAndReportErrorsIfAny @Db Sysname As Begin Set nocount on -- Creating temporary table for CheckDB result. Valid for SQL2012 and more... CREATE TABLE #CheckDB ( [Error] Int , [Level] Int , [State] Int , [MessageText] varchar(7000) , [RepairLevel] nvarchar(100) , [Status] Int , [DbId] Int , [DbFragId] Int , [ObjectID] Int , [IndexId] Int , [PartitionId] BigInt , [AllocUnitId] BigInt , [RidDbId] Bigint , [RidPruId] Bigint , [File] Bigint , [Page] Bigint , [Slot] Bigint , [RefDbID] Bigint , [RefPruId] Bigint , [RefFile] Bigint , [RefPage] Bigint , [RefSlot] Bigint , [Allocation] Bigint ); -- Execute CheckDB and insert result to temp table Begin try INSERT INTO #CheckDB ([Error], [Level], [State], [MessageText], [RepairLevel], [Status], [DbId], [DbFragId], [ObjectID], [IndexId], [PartitionId], [AllocUnitId], [RidDbId], [RidPruId], [File], [Page], [Slot], [RefDbID], [RefPruId], [RefFile], [RefPage], [RefSlot], [Allocation]) --EXEC ('DBCC CHECKDB(''DemoCorruption'') WITH TABLERESULTS'); EXEC ('DBCC CHECKDB('''+@Db+''') WITH TABLERESULTS'); End try Begin catch Throw End catch If exists (Select * From #CheckDb Where Level>10) Begin Declare @InitialMsg Nvarchar(max) Select @InitialMsg = InitialMsg From S#.Enums as cst Cross Apply ( Select InitialMsg = Replicate('-', 95) + cst.nl + 'Error(s) from S#.RunScript issued by S#.PerformCheckDbAndReportErrorsIfAny on '+ @Db + cst.Nl + Replicate('-', 95) ) as InitialMsg Exec S#.PrintAndLog @InitialMsg, 0 Select Seq=Identity(int, 1,1), * Into #CheckDbSeq From #CheckDb Declare LogCursor Cursor local fast_forward For Select --Seq, Err.Error, Err.Level, Err.State, NULL, NULL, Err.MessageText, L.Line From (select maxLevel=Max(level) From #CheckDBSeq) as ErrWithMaxlevel CROSS APPLY ( Select * From #CheckDBSeq Where maxLevel > 10 And Error=8990 UNION ALL Select * From #CheckDBSeq Where Level > 10 ) as Err CROSS APPLY ( Select MsgTemplate= 'DBCC CheckDb Msg: #ErrMessage# Error: #ErrNumber# Severity: #ErrSeverity# State: #ErrState' ) As MsgTemplate CROSS APPLY S#.FormatRunTimeMsg(MsgTemplate, Err.Error, Err.Level, Err.State, NULL, NULL, Err.MessageText) as Fmt CROSS APPLY S#.SplitSqlCodeLinesIntoRows(fmt.ErrMsg) as L Where Line <> '' Order By IIF(Error=8990, 0, Error), State, Seq, LineNum Declare @Line Nvarchar(4000) Open LogCursor While(1=1) Begin Fetch Next From LogCursor Into @Line If @@FETCH_STATUS <> 0 Break Exec S#.PrintAndLog @Line, 0 End Close LogCursor End /* Exec S#.PerformCheckDbAndReportErrorsIfAny DemoCorruptionx -- general interception (throw because database do not exists) Exec S#.PerformCheckDbAndReportErrorsIfAny DemoCorruption -- special intercept (to grab DBCC errors through with tableResults) */ End /*===KeyWords=== Scripting,DbMaintenance ===KeyWords===*/ GO --------------------------------------------------------------------------------------------------------------- -- S# use extended event session to capture error messages and other information when running scripts that are -- expected to produce multiple error messages (like backup commands, restore commands, code with prints etc.) -- -- This function generate queries needed to manage event session used to create, start, stop, drop the session -- used to manage extended event session for error messages. -- -- It generates script depending of the state of the session -- (which name is something like 'S#_RunscriptErrors'+@@spid) -- -- It may not generate any script if the session is already started (when asked to be started) -- or does not exists anymore (when asked to be stopped or dropped) -- -- It also handles event session of the same kind, if they are orphaned which can only be guest -- if a "SPID" part of the session of S#_RunscriptErrorSPID do not match any session_id -- of active sessions -- -- It is also used for testing purposes of function GetErrorMessagesAndInfo ------------------------------------------------------------------------------------------------ -- @@MARK: Running batch of generated code -- Multiple Error message handling - Setup tool for extended events for this purpose Create Or Alter Function S#.ScriptManageEventSession(@action as Integer) Returns table as Return ( SELECT EventSessionName -- Extended event session name for extended errors for the current session -- or orphanded session to clean up , SessionExists -- 1 if session exists, 0 otherwise , SessionStarted -- 1 if session is started, 0 otherwise , SessionActionAndSeq.TemplateDelim , ProperActionSequence -- sequence of action to perform, as sort key , Sql FROM S#.Enums as En CROSS APPLY (Select Action=@Action) as Action CROSS APPLY (Select EventSessionNamePrefix='YourSqlDbaQueryMsgs') as EventSessionNamePrefix -- session name that exists or should exists from this spid CROSS APPLY (Select CurrentSpid=Cast(@@spid as nvarchar)) as CurrentSpid CROSS APPLY (Select CurrentEventSessionName=EventSessionNamePrefix + CurrentSpid) as CurrentEventSessionName -- state of this session exists and if exists started or not -- OUTER sessionExists is either is NULL or 1 and SessionStarted is either is NULL or 1 OUTER APPLY (Select SessionExists=1 From sys.server_event_sessions Where Name = CurrentEventSessionName) as SessionExists OUTER APPLY (Select SessionStarted=1 From sys.dm_xe_sessions Where name = CurrentEventSessionName) as SessionStarted -- Depending session state : run different actions if necessary -- some state requires more actions in the pack that other CROSS APPLY ( Select EventSessionName=CurrentEventSessionName, TemplateDelim, ProperActionSequence=0 From ( -- a stop or clear action implies both action Select CurrentEventSessionName, TemplateDelim='===StopEventSession_S#_RunscriptErrorsForSpecificSpid===', ProperActionSequence=10 UNION ALL Select CurrentEventSessionName, TemplateDelim='===DropEventSession_S#_RunscriptErrorsForSpecificSpid===', ProperActionSequence=11 ) as ActionsOnCurrentSesssion Where Action IN (En.ExtendedEventSession@Stop, En.ExtendedEventSession@Clear) And SessionExists = 1 And SessionStarted = 1 UNION ALL -- create the session if it does not exists Select CurrentEventSessionName, TemplateDelim='===CreateEventSession_S#_RunscriptErrorsForSpecificSpid===', ProperActionSequence=20 Where SessionExists IS NULL And @Action IN (En.ExtendedEventSession@Start) UNION ALL -- start the session if not started (which is also true if it doesn't exists) Select CurrentEventSessionName, TemplateDelim='===StartEventSession_S#_RunscriptErrorsForSpecificSpid===', ProperActionSequence=30 Where @Action IN (En.ExtendedEventSession@Start) And SessionStarted IS NULL -- created before not yet started so start it UNION ALL Select EventSessionName=PotentialOrphanedSessionName, TemplateDelim, ProperActionSequence=10 From ( -- session name built by this process are of the form + spid -- By replacing the prefix by nothing gives the remaining spid in a string form and is translated to integer -- If the spid of this event session has no connection active, it is sure that the session is orphaned -- and should be stopped and dropped -- Obviously existing connection isn't a proof that the session is not orphaned, but eventually -- it may happen to be missing and the cleanup will happen later Select PotentialOrphanedSessionName=name From sys.server_event_sessions cross apply (Select StrSpid=Replace(name, EventSessionNamePrefix, '')) as StrSpid cross apply (Select Spid=Cast(StrSpid as Int)) as Spid Where Action IN (En.ExtendedEventSession@Stop, En.ExtendedEventSession@Clear) And Name Like EventSessionnamePrefix+'%' And Spid NOT IN (Select session_id from Sys.dm_exec_sessions) ) as ses Cross Apply ( -- a stop and clear Select ProperActionSequence=12, TemplateDelim='===StopEventSession_S#_RunscriptErrorsForSpecificSpid===' UNION ALL Select ProperActionSequence=13, TemplateDelim='===DropEventSession_S#_RunscriptErrorsForSpecificSpid===' ) as ActionsOnOrphanedSesssions ) as SessionActionAndSeq -- get template for each action through TemplateDelim CROSS APPLY (Select Template=TxtInCmt From S#.GetCmtBetweenDelim (SessionActionAndSeq.TemplateDelim, 'S#.ScriptManageEventSession')) as C -- replace tags in the template by values for session name and current spid (which is a filter specified in eventSession at creation) CROSS APPLY (Select Sql0=REPLACE(Template, '#EventSessionName#', EventSessionName collate database_default)) as Sql0 CROSS APPLY (Select Sql=REPLACE(Sql0, '#CurrentSpid#', CurrentSpid collate database_default)) as Sql /*===StopEventSession_S#_RunscriptErrorsForSpecificSpid=== ALTER EVENT SESSION #EventSessionName# ON SERVER STATE = STOP; -- StopSession ===StopEventSession_S#_RunscriptErrorsForSpecificSpid===*/ /*===DropEventSession_S#_RunscriptErrorsForSpecificSpid=== DROP EVENT SESSION #EventSessionName# ON SERVER; -- DropSession ===DropEventSession_S#_RunscriptErrorsForSpecificSpid===*/ /*===CreateEventSession_S#_RunscriptErrorsForSpecificSpid=== CREATE EVENT SESSION #EventSessionName# ON SERVER -- CreateCurrent ADD EVENT sqlserver.error_reported -- for standard SQL error ( ACTION(sqlserver.Session_id, sqlserver.sql_text) WHERE [sqlserver].[session_id]=(#CurrentSpid#) And [package0].[not_equal_unicode_string]([message],N'''''') AND [severity]>10 ) ADD TARGET package0.ring_buffer WITH ( --NO_EVENT_LOSS fails on SQL 2014 SP2 --ALLOW_SINGLE_EVENT_LOSS EVENT_RETENTION_MODE=ALLOW_SINGLE_EVENT_LOSS , MAX_MEMORY=4096 KB , MAX_DISPATCH_LATENCY=1 SECONDS , MAX_EVENT_SIZE=0 KB , MEMORY_PARTITION_MODE=NONE , TRACK_CAUSALITY=ON , STARTUP_STATE=OFF ) ===CreateEventSession_S#_RunscriptErrorsForSpecificSpid===*/ /*===StartEventSession_S#_RunscriptErrorsForSpecificSpid=== ALTER EVENT SESSION #EventSessionName# ON SERVER STATE = START -- StartCurrent ===StartEventSession_S#_RunscriptErrorsForSpecificSpid===*/ /* -- bunch of test code to test this function Declare @CreateStart nvarchar(max) = '' Select @CreateStart=@CreateStart+Sql from S#.Enums as En Cross Apply S#.ScriptManageEventSession(En.ExtendedEventSession@Start) As A Print @CreateStart; Exec (@CreateStart) If @CreateStart Not Like '%-- CreateCurrent%-- StartCurrent%' Raiserror ('Session not started properly-test1',11,1) Declare @AlreadyStarted nvarchar(max) = '' Select @AlreadyStarted=@AlreadyStarted+Sql from S#.Enums as En Cross Apply S#.ScriptManageEventSession(En.ExtendedEventSession@Start) As A If @@rowcount<>0 Raiserror ('Session already started, nothing to do-test2',11,1) Declare @StopAStartedOne nvarchar(max) = '' Select @StopAStartedOne=@StopAStartedOne+Sql from S#.Enums as En Cross Apply S#.ScriptManageEventSession(En.ExtendedEventSession@Stop) As A If @@rowcount>0 And @StopAStartedOne >'' And @StopAStartedOne Not Like '%-- StopSession%-- DropSession%' Raiserror ('Session not stopped properly--test3',11,1) Print @StopAStartedOne; Exec (@StopAStartedOne) Declare @StopAlreadyDone nvarchar(max) = '' Select @StopAlreadyDone=@StopAlreadyDone+ Sql from S#.Enums as En Cross Apply S#.ScriptManageEventSession(En.ExtendedEventSession@Stop) As A If @@rowcount<>0 Raiserror ('Session already stopped, nothing to do, should not be generated--test4',11,1) Print @StopAlreadyDone; Exec (@StopAlreadyDone) Declare @StartExisting nvarchar(max) = '' Select @StartExisting=@StartExisting+ Sql from S#.Enums as En Cross Apply S#.ScriptManageEventSession(En.ExtendedEventSession@Start) As A Print @StartExisting; Exec (@StartExisting) If @StartExisting Not Like '%-- CreateCurrent%-- StartCurrent%' Raiserror ('Session not start properly--test5',11,1) declare @ClearEv nvarchar(max)='' Select @ClearEv=@ClearEv+ Sql from S#.Enums as En Cross Apply S#.ScriptManageEventSession(En.ExtendedEventSession@Clear) As A If @@rowcount>0 And @ClearEv Not Like '%-- StopSession%-- DropSession%' Raiserror ('Session not stopped properly--test6',11,1) Print @ClearEv; Exec (@ClearEv) Declare @CreateStart2 nvarchar(max) = '' Select @CreateStart2=@CreateStart2+ Sql from S#.Enums as En Cross Apply S#.ScriptManageEventSession(En.ExtendedEventSession@Start) As A Print @CreateStart2; Exec (@CreateStart2) If @CreateStart2 Not Like '%-- CreateCurrent%-- StartCurrent%' Raiserror ('Session not started properly--test7',11,1) Declare @StopAStartedOne2 nvarchar(max) = '' Select @StopAStartedOne2=@StopAStartedOne2+ Sql from S#.Enums as En Cross Apply S#.ScriptManageEventSession(En.ExtendedEventSession@Stop) As A If @StopAStartedOne2 Not Like '%-- StopSession%-- DropSession%' Raiserror ('Session not stopped properly--test8',11,1) Print @StopAStartedOne2; Exec (@StopAStartedOne2) declare @ClearEv2 nvarchar(max)='' Select @ClearEv2=@ClearEv2+ Sql from S#.Enums as En Cross Apply S#.ScriptManageEventSession(En.ExtendedEventSession@Clear) As A If @@rowcount>0 And @ClearEv2 Not Like '%-- DropSession%' Raiserror ('Session not stopped properly--test9',11,1) Print @ClearEv2; Exec (@ClearEv2) declare @ClearEv3 nvarchar(max)='' Select @ClearEv3=@ClearEv3+ Sql from S#.Enums as En Cross Apply S#.ScriptManageEventSession(En.ExtendedEventSession@Clear) As A If @@rowcount<>0 Raiserror ('Session already cleared, nothing to do--test10',11,1) Print @ClearEv3; Exec (@ClearEv3) */ /*===KeyWords=== Logging ===KeyWords===*/ ) GO ---------------------------------------------------------------------------------------------------------- -- With inline table programming, the optimizer sometimes tries too much to nest expressions -- into expressions instead of puting intermediary result of expressions into a bucket and reuse them. -- These functions (by type) makes expressions results more opaque to the optimizer so it uses a pointer -- to their results. This avoids also unecessary re-evaluation of an expression -- that is used in many places. This may be used in last recourse when it cleary appears into the access -- plan that the optimizer redundantly recompute the same expression in different places into the query -- This rewards a lot especially with xml expressions, or with very nested string expression ---------------------------------------------------------------------------------------------------------- Create or Alter Function S#.EaseOptimizerJobByMaterializingXmlExpression (@xml xml) Returns XML as Begin Return (@xml) End GO Create or Alter Function S#.EaseOptimizerJobByMaterializingNvarcharExpression (@str nvarchar(max)) Returns Nvarchar(max) as Begin Return (@str) End GO -- ------------------------------------------------------------------------------------------- -- Shorthand cast workaround to help cleaning up XML concat limited by default at 4000 char -- and many other uses where a string expression has to become nvarchar(max) string -- ------------------------------------------------------------------------------------------- GO -- @@MARK: Running batch of generated code -- Multiple Error message handling from extended event target - Get Error messages and info Create Or Alter Function S#.GetErrorMessagesAndInfo() Returns Table as Return Select * From ( Select -- if an extended event is activated by runScript for this session, it grabs errors not reported by TSQL ERROR_NUMBER() -- but if it reports an error with the same error_number, this error is reported in more details by TSQL MostSignificantMessages.* , DecideBetweenTSqlOrExtended = ROW_NUMBER() Over (Partition By ErrNumber Order By isAnExtendedError) , TotalNbOfMessages = Count(*) Over (Partition by NULL) From ( -- error message from TSQL if any (the most significant) SELECT Fmt.ErrMsg -- formatted msg , Fmt.ErrMessage -- data for standard error messages , Fmt.ErrNumber , Fmt.ErrSeverity , Fmt.ErrState , Fmt.ErrLine -- this information sometimes apply in case of error reported from standard error function in catch , Fmt.ErrProcedure -- this information sometimes apply in case of error reported from standard error function in catch , EventTime=GETUTCDATE() , isAnExtendedError , TotalEventsProcessed , EventSessionName , eventName=CAST(null as sysname) --, XEdata=CAST(null as xml) FROM -- unapplicable columns for this error source, but necessary for union of the two sources (Select isAnExtendedError=0, TotalEventsProcessed=Cast(NULL as Int), EventSessionName=Cast(null as sysname)) As PlaceHolderColsForUnion CROSS JOIN S#.Enums As E CROSS APPLY (Select errNo=ERROR_NUMBER (), errSev=ERROR_SEVERITY(), errState=ERROR_STATE(), ErrLine=ERROR_LINE(), ErrPrc=ERROR_PROCEDURE (), errMsg=ERROR_MESSAGE ()) as er CROSS APPLY S#.FormatRunTimeMsg (E.RunScript@ErrMsgTemplate, er.errNo, er.errSev, er.errState, er.ErrLine, er.ErrPrc, er.errMsg) as Fmt -- drop the message if already formatted and threw from a nested call OUTER Apply (Select FmtErrMsg=Replace(Fmt.ErrMsg, '#atPos#', AtPos) Where Fmt.ErrMessage Not Like '----%Error from S#.RunScript%' ) as vErrMsg Where FmtErrMsg IS NOT NULL UNION -- perform some distinct operation especially on the next query for which I do not know there is some message reported twice -- extended events if it exists, while have some of the messages from T-SQL to eliminate Select ISNULL(Fmt.ErrMsg, 'Error from SQL ErrorLog: ' + X.ErrMessage) -- formatted msg or message from SQL error log , X.ErrMessage -- data for standard error messages , X.ErrNumber , X.ErrSeverity , X.ErrState , PlaceHolderColsForUnion.ErrLine , PlaceHolderColsForUnion.ErrProcedure , X.EventTime , isAnExtendedError , X.TotalEventsProcessed -- let know if events details are there yet (delay between catch and event session reporting) , EventSessionName , X.EventName --, XEdata FROM ( Select EventSessionName, XE.*, Xet.target_data From (Select EventSessionName='YoourSqlDbaQueryMsgs'+CAST(@@spid as Nvarchar)) as EventSessionName JOIN sys.server_event_sessions as Se ON Se.name = EventSessionName JOIN sys.dm_xe_sessions AS xe On Xe.name = Se.name JOIN sys.dm_xe_session_targets AS xet ON (xet.event_session_address=xe.address) ) as ExEventInfo CROSS APPLY (Select XEdata=S#.EaseOptimizerJobByMaterializingXmlExpression(CAST(ExEventInfo.target_data AS XML)) ) as vXEdata CROSS APPLY XEData.nodes('//RingBufferTarget/event') AS xnode(c) CROSS APPLY ( Select * From (Select TotalEventsProcessed = xnode.c.value(N'(/RingBufferTarget/@totalEventsProcessed)[1]', 'INT') ) as TotalEventsProcessed CROSS APPLY (Select SessionId = xnode.c.value(N'(action[@name="session_id"]/value)[1]', N'SMALLINT') ) AS SessionId CROSS APPLY (Select EventName = xnode.c.value(N'(@name)[1]', N'NVARCHAR(MAX)') ) AS EventName CROSS APPLY (Select EventTime = xnode.c.value(N'(@timestamp)[1]', N'datetime') ) AS EventTime CROSS APPLY (Select ErrMessage = xnode.c.value(N'(data[@name="message"]/value)[1]', N'NVARCHAR(MAX)') ) AS ErrMessage CROSS APPLY (Select ErrNumber = xnode.c.value(N'(data[@name="error_number"]/value)[1]', N'INT') ) AS ErrNumber CROSS APPLY (Select ErrSeverity = xnode.c.value(N'(data[@name="severity"]/value)[1]', N'INT') ) AS ErrSeverity CROSS APPLY (Select ErrState = xnode.c.value(N'(data[@name="state"]/value)[1]', N'INT') ) AS ErrState CROSS APPLY (Select IsIntercepted = xnode.c.value(N'(data[@name="is_intercepted"]/value)[1]', N'NVARCHAR(MAX)') ) AS IsIntercepted -- this info in never reported in event session error reporting, but must match the next select in union CROSS APPLY (Select ErrLine=Cast(Null as Int) ) AS ErrLine -- this info in never reported in event session error reporting, but must match the next select in union CROSS APPLY (Select ErrProcedure=Cast(Null as sysname) ) As ErrProcedure ) as X CROSS JOIN S#.Enums As E OUTER APPLY S#.FormatRunTimeMsg (E.RunScript@ErrMsgTemplate, ErrNumber, ErrSeverity, ErrState, X.ErrLine, X.ErrProcedure, X.ErrMessage) as Fmt -- unapplicable columns for this error source, but necessary for union of the two sources CROSS APPLY (Select isAnExtendedError=1, ErrLine=Cast(Null as Int), ErrProcedure=Cast(Null as sysname) ) as PlaceHolderColsForUnion -- Get event from "errorlog_written" event only for DBCC for which error_reported event do not report some interesting info CROSS APPLY (Select LikeInfoFmtDBCCMsg='[0-9][0-9][0-9][0-9]% spid'+CONVERT(nvarchar, @@spid)+' %DBCC CHECKDB%') as LikeInfoFmtDBCCMsg OUTER APPLY (Select GetErrorFromErrLog=1 where X.ErrMessage Like LikeInfoFmtDBCCMsg) as GetErrorFromErrLog Where (X.EventName = 'error_reported' Or GetErrorFromErrLog = 1) -- we get stuff from errorLog only for DBCC which do not report some error through normal SQL ERROR ) as MostSignificantMessages ) as SortedMsg Where DecideBetweenTSqlOrExtended=1 /* -- get the stuff to create Declare @Start1 nvarchar(max) = '' Select @Start1=@Start1+ProperActionSequence from S#.Enums as En Cross Apply S#.ScriptManageEventSession(En.ExtendedEventSession@Start) As A Print @start1;Exec (@start1) Drop table if exists #Tmp Select top 0 * into #tmp From S#.GetErrorMessagesAndInfo () as Err Begin try Exec('select * from NonExistingtable') Insert into #tmp Select * From S#.GetErrorMessagesAndInfo () as Err If @@rowcount=0 Begin Insert into #tmp Select * from S#.GetErrorMessagesAndInfo () End Select * from #tmp End try begin catch Insert into #tmp Select * From S#.GetErrorMessagesAndInfo () If @@rowcount=0 Begin WaitFor Delay '00:00:02' Insert into #tmp Select * from S#.GetErrorMessagesAndInfo () End end catch Dbcc checkdb ('bible') Begin try Declare @divparZero int = 1/0 Insert into #tmp Select * From S#.GetErrorMessagesAndInfo () as Err If @@rowcount=0 Begin Insert into #tmp Select * from S#.GetErrorMessagesAndInfo () End Select * from #tmp End try begin catch Insert into #tmp Select * From S#.GetErrorMessagesAndInfo () If @@rowcount=0 Begin WaitFor Delay '00:00:02' Insert into #tmp Select * from S#.GetErrorMessagesAndInfo () End end catch Select * from #tmp Declare @stop1 nvarchar(max) = '' Select @stop1=@Stop1+ProperActionSequence from S#.Enums as En Cross Apply S#.ScriptManageEventSession(En.ExtendedEventSession@Stop) As A Print @stop1;Exec (@stop1) Declare @Start2 nvarchar(max) = '' Select @Start2=@Start2+ProperActionSequence from S#.Enums as En Cross Apply S#.ScriptManageEventSession(En.ExtendedEventSession@Start) As A Print @start2;Exec (@start2) Drop table if exists #Tmp Select top 0 * into #tmp From S#.GetErrorMessagesAndInfo () as Err Begin try Backup database Msdb to disk='C:\Existepas\Msdb.bak' End try begin catch Insert into #tmp Select * From S#.GetErrorMessagesAndInfo () as Err If @@rowcount=0 Begin WaitFor Delay '00:00:02' select 'du catch' Select * from S#.GetErrorMessagesAndInfo () End end catch Select * from #tmp Declare @clear nvarchar(max) = '' Select @clear=@clear+ProperActionSequence from S#.Enums as En Cross Apply S#.ScriptManageEventSession(En.ExtendedEventSession@Start) As A Print @clear;Exec (@clear) */ /*===KeyWords=== Logging ===KeyWords===*/ GO -- ------------------------------------------------------------------------------------------ -- Table used to store commands to run (segregated by connection) -- ------------------------------------------------------------------------------------------ -- @@MARK: Code generation - table to store generated code to be ran Drop View If Exists S#.ScriptToRun Drop View If Exists S#.AppendToScriptToRun Drop Table If Exists S#.RealScriptToRun Create Table S#.RealScriptToRun ( spid int constraint DF_RealScriptToRun_Spid default @@spid -- segregated by current user connection , nestLevel Int constraint DF_RealScriptToRun_nestLevel default @@NESTLEVEL -- allow reentrancy when working with this S#.ScriptToRun and 77Script , seq int , eventTime datetime2 constraint DF_RealScriptToRun_eventime default SYSDATETIME() , Sql Nvarchar(max) , label nvarchar(max) , db Sysname NULL , constraint Pk_RealScriptToRun Primary Key Clustered (spid, nestLevel, seq) -- extra columns for support passing of paramters to yExecNLog.LogAndOrExecPrm from S#.RunScript , Context nvarchar(4000) NULL , info nvarchar(max) NULL , raiseErrorFlag int NULL , forDiagOnly int NULL ) GO -- @@MARK: Code generation - view to simplify insert into to table (S#.RealScriptToRun) of generated code Create View S#.ScriptToRun as Select Sql, db, label, Seq, nestLevel, Context, info, raiseErrorFlag, forDiagOnly From S#.RealScriptToRun Where spid = @@spid -- filtered by the current user connection GO -- ------------------------------------------------------------------------------------------ -- When inserting through this view, previous rows inserted are automatically cleanuped -- ------------------------------------------------------------------------------------------ -- @@MARK: Code generation - Instead of insert trigger on S#.ScriptToRun to clean it before inserting generated code Drop Trigger If Exists S#.ScriptToRunInsertTrigger GO Create Trigger S#.ScriptToRunInsertTrigger ON S#.ScriptToRun Instead Of Insert as Begin Set nocount on Delete From S#.ScriptToRun Where nestLevel >= @@NESTLEVEL-- this view is filtered by current @@spid Insert into S#.ScriptToRun (seq, Sql, db, label, Context, info, raiseErrorFlag, forDiagOnly) Select seq, Sql, db, label, Context, info, raiseErrorFlag, forDiagOnly From Inserted End GO -- @@MARK: Code generation - with a different view trigger that appends code generated until ran. Create View S#.AppendToScriptToRun as Select Sql, db, label, Seq, nestLevel, Context, info, raiseErrorFlag, forDiagOnly From S#.RealScriptToRun Where spid = @@spid -- filtered by the current user connection GO -- ------------------------------------------------------------------------------------------ -- When inserting through this view, previous rows inserted are not automatically cleanuped -- It allows to add script lines in many inserts before running them -- ------------------------------------------------------------------------------------------ -- @@MARK: Code generation - Instead of insert trigger on S#.AppendToScriptToRun to set starting seq for append Drop Trigger If Exists S#.AppendToScriptToRunInsertTrigger GO Create Trigger S#.AppendToScriptToRunInsertTrigger ON S#.AppendToScriptToRun Instead Of Insert as Begin Set nocount on Insert into S#.AppendToScriptToRun (seq, Sql, db, label, Context, info, raiseErrorFlag, forDiagOnly) Select seq+(Select ISNULL(Max(Seq),0) From S#.ScriptToRun) , Sql, db, label, Context, info, raiseErrorFlag, forDiagOnly From Inserted End GO ------------------------------------------------------------------------------ -- This procedure is altered at every query run by RunScript -- It conveniently displays last running query attempted to be run ------------------------------------------------------------------------------ -- @@MARK: Running batch of generated code -- logging of last query run in the batch Create Or Alter Procedure S#.ShowLastRunScriptQry as /*===QueryToDisplay=== --actually nothing ===QueryToDisplay===*/ Select S.Line From S#.GetCmtBetweenDelim ('===QueryToDisplay===', 'S#.ShowLastRunScriptQry') as Sql cross Apply S#.SplitSqlCodeLinesIntoRows(Sql.TxtInCmt) S Order by LineNum /*===KeyWords=== Logging ===KeyWords===*/ Go ------------------------------------------------------------------------------ -- This procedure is altered at every query run by RunScript -- It conveniently displays last running query attempted to be run ------------------------------------------------------------------------------ Create Or Alter Procedure S#.ModifyShowLastRunScriptQry @Sql Nvarchar(max) as Begin -- The success of this operation is not essential to S#.RunScript -- We just trap error if it fails Declare @SqlThatChangeShowLastRunScriptQry Nvarchar(max) /*===AlterProc S#.ShowLastRunScriptQry=== Alter Procedure S#.ShowLastRunScriptQry as /*===QueryToDisplay=== #SQL# ===QueryToDisplay===*/ Select S.Line From S#.GetCmtBetweenDelimInBatch ('===QueryToDisplay===') as Sql cross Apply S#.SplitSqlCodeLinesIntoRows(Sql.TxtInCmt) S Order by LineNum ===AlterProc S#.ShowLastRunScriptQry===*/ Select @SqlThatChangeShowLastRunScriptQry = SqlThatChangeShowLastRunScriptQry From S#.GetCmtBetweenDelim ('===AlterProc S#.ShowLastRunScriptQry===', 'S#.ModifyShowLastRunScriptQry') as C -- replace has its limits, search value to replace needs to be less than page size Cross Apply (Select SqlThatChangeShowLastRunScriptQry=Replace (C.TxtInCmt, '#Sql#', @Sql)) as SqlThatChangeShowLastRunScriptQry Begin try Exec (@SqlThatChangeShowLastRunScriptQry) End try Begin catch End Catch /*===KeyWords=== Logging ===KeyWords===*/ End -- S#.ModifyShowLastRunScriptQry GO -- ------------------------------------------------------------------------------------------ -- Run commands stored in scriptTable (segregated by connection) -- Useful for big scripting -- ------------------------------------------------------------------------------------------ -- @@MARK: Running batch of generated code -- S#.RunScript - Main procedure to run code generated and log Drop Proc If Exists S#.RunScript GO create Proc S#.RunScript @PrintOnly Int = 0 -- don't execute just print what SQL to execute looks like , @Silent Int = 0 -- doesn't echo execution , @NestLevelOffset int = 0 -- special trick not often used to get SQL generated to compare for unit testing , @RunOnThisDb sysname = NULL -- remote database execution if database is specified , @LogExtendedErr int = 0 -- use event session to get extra exceptions from backup commands and DBCC Checkdb , @CatchAndThrow Int = 1 -- stop on error, otherwise go on for next query as Begin Set Nocount on; Declare @Sql Nvarchar(max) = '' Declare @ManageEvent Nvarchar(max) = '' Declare @Label Nvarchar(max) Declare @seq Int Declare @d datetime Declare @SqlThatChangeShowLastRunScriptQry nvarchar(max) Declare @SeqErr Int = 0, @Msg Nvarchar(max) Declare @db Sysname --Drop table if exists #Tmp Select top 0 * into #tmp From S#.GetErrorMessagesAndInfo () as Err -- Create temporary table to hold current query to be processed Select Top 0 Seq=Convert(int, Null) , Sql=Convert(Nvarchar(Max), NULL) , db=Convert(Sysname, NULL) , Label=Convert(sysname, NULL) Into #Sql Select @seq = Min(Seq)-1 From S#.ScriptToRun -- this view is filtered by current @@spid Where nestLevel = (@@NESTLEVEL + @NestLevelOffset) While (1=1) Begin Begin Try Insert into #Sql Select Top 1 seq, sql, db, label From S#.ScriptToRun -- this view is filtered by current @@spid Where nestLevel = (@@NESTLEVEL + @NestLevelOffset) And seq > @Seq Order by Seq If @@rowcount = 0 break Select @seq = Seq, @Sql=Sql, @Db=Db, @Label=Label From #Sql Truncate Table #Sql -- The following feature is not a essential part of the library, just a convenience Exec S#.ModifyShowLastRunScriptQry @Sql -- Special handling of '#SilentModeParameterFromRunScriptToRun#'. When this proc is duplicated we don't want -- this parameter to be replaced so we break this string in two parts to avoid '#SilentModeParameterFromRunScriptToRun#' -- to be replaced Set @Sql=Replace(@Sql, '#SilentModeParameter'+'FromRunScriptToRun#', convert(nvarchar, @silent)) -- carrie on Silent mode if #Silent# tag is specified in queries to run set @d = getdate() If @Silent = 0 Begin If @RunOnThisDb IS NOT NULL Or @Db IS NOT NULL Print '-------------Dynamic database context switch to '+Coalesce(@Db,@RunOnThisDb)+' is done by '+S#.FullObjName (@@procId) Exec S#.PrintSplittedSqlCode @Sql, @Label End If @PrintOnly = 0 Begin Declare @nbRangees Int Declare @StatsInfo nvarchar(max) -- If extended error logging is activated, we need to start the event session -- Here @ManageEvent is used to store the proper sequence of actions (script actions to start the event session) -- as S#.ScriptManageEventSession is a function that returns the proper sequence of actions to start the event session -- Start the event session, if not already start because of a previous run Set @ManageEvent ='' Select @ManageEvent = @ManageEvent+ProperActionSequence From S#.Enums as Consts -- Consts.ExtendedEventSession@Start is a constant understood by S#.ScriptManageEventSession -- to generate script to start the event session Cross Apply S#.ScriptManageEventSession(Consts.ExtendedEventSession@Start) As A Where @LogExtendedErr=1 -- if extended error logging is activated If @@rowcount>0 Exec (@ManageEvent) If Coalesce(@Db,@RunOnThisDb) IS NULL Exec (@Sql) Else Begin -- Since Use is dynamically executed first, and then the exec statement is executed dynamically under this context -- create (view/function/procedure) works because there is no executable statement in the @sql Declare @IndirectUse as nvarchar(max) Set @IndirectUse = ' Use ['+Coalesce(@Db,@RunOnThisDb)+']; Exec (@Sql)' Exec sp_executeSql @IndirectUse, N'@Sql nvarchar(max)', @Sql End Select @StatsInfo = StatsInfo From (Select nbOfRows=convert(nvarchar, @@Rowcount) ) as nbOfRows CROSS APPLY (Select Start=convert(nvarchar, @d, 121) ) as Start CROSS APPLY (Select EndTimeWithDate=convert(nvarchar, getdate(), 121) ) as EndTimeWithDate CROSS APPLY (Select EndTime=IIF(Left(Start,10) = Left(EndTimeWithDate,10), Stuff(EndTimeWithDate , 1, 10, ''), EndTimeWithDate )) as EndTime CROSS APPLY (Select ElapsedSecs=datediff(ss, @d, getdate()) ) as ElapsedSecs CROSS APPLY (Select ElapsedHrs=ElapsedSecs/3600 ) as ElapsedHrs CROSS APPLY (Select ElapsedMins=ElapsedSecs/60 ) as ElapsedMins CROSS APPLY (Select Mins = ElapsedMins - (ElapsedHrs * 60) ) as Mins CROSS APPLY (Select Secs = ElapsedSecs - (ElapsedHrs*3600 - (ElapsedMins * 60)) ) as Secs CROSS APPLY (Select HrMiSecDuration=replace(Str(ElapsedHrs, 2)+':'+Str(Mins, 2)+':'+Str(Secs, 2), ' ', '0') ) as HrMiSecDuration CROSS APPLY (Select MsgFmt = '-------------rows #nbOfRows# duration: #HrMiSecDuration# start/end: #Start# / #EndTime#' + S#.Nl() + S#.Nl() ) as MsgFmt CROSS APPLY S#.MultipleReplaces (MsgFmt, (select nbOfRows, HrMiSecDuration , Start, EndTime For Json Path)) as MsgStatsInfo CROSS APPLY (select StatsInfo = Replace(MsgStatsInfo.replacedTxt, '--Rows 0 ', '--') ) as StatsInfo If @Silent = 0 Exec S#.PrintAndLog @StatsInfo, 0 Set @ManageEvent ='' Select @ManageEvent = @ManageEvent +ProperActionSequence From S#.Enums as En Cross Apply S#.ScriptManageEventSession(En.ExtendedEventSession@Stop) As A Where @LogExtendedErr=1 If @@rowcount>0 Exec (@ManageEvent) End End Try -- cleanup of instructions that are made through the insert trigger on the view S#.ScriptToRun Begin Catch truncate table #tmp Insert Into #Tmp Select * From S#.GetErrorMessagesAndInfo () as Err Set @ManageEvent ='' Select @ManageEvent = @ManageEvent +ProperActionSequence From S#.Enums as En Cross Apply S#.ScriptManageEventSession(En.ExtendedEventSession@Stop) As A Where @LogExtendedErr=1 If @@rowcount>0 Exec (@ManageEvent) -- If error reporting through event session, this must be taken into account: -- There is a latency in event session reporting which is set in our case to the minimum -- of 1 second. So if we are here in the catch, something has to be found. -- Standard error report one error, and extended error should also report the same message -- We keep the sandard error because over extended error when both exists because it give more info like line number, module name -- But we need to know is extend If Exists(Select * From #Tmp Where TotalNbOfMessages = 1 And @LogExtendedErr = 1) Begin WaitFor Delay '00:00:02' Insert into #Tmp Select * from S#.GetErrorMessagesAndInfo () End -- out errors Declare LogCursor Cursor local fast_forward For Select ErrMsg From #tmp Open LogCursor While(1=1) Begin Fetch Next From LogCursor Into @Msg If @@FETCH_STATUS <> 0 Break Exec S#.PrintSplittedSqlCode @Sql = @Msg End Close LogCursor Deallocate LogCursor Truncate Table #Tmp If @CatchAndThrow = 1 --And exists(select * from #Tmp Where ErrMsg Not Like '%FmtErrMsg%') Begin; THROW 51000, 'RunScript caught an error when executing last query, see verbose output or log table', 16; End End Catch End -- While /* set nocount on -- test show all three queries Exec S#.RegisterLogTable 'S#.LogTest' Insert Into S#.ScriptToRun(sql, seq) Select top 3 r0.s, row_Number() Over (Order by Name) as seq From sys.tables Cross Apply S#.iQReplace('Select "#Tb#" as Tb, count(*) from #Tb#', '#tb#', OBJECT_SCHEMA_NAME(object_id)+'.'+name) r0 Exec S#.RunScript @PrintOnly=0 Select * from S#.LogTest -- test show all three queries and log them, display last query in log Exec S#.RegisterLogTable 'S#.LogTest' Insert Into S#.ScriptToRun(sql, seq) Select top 3 r0.s, row_Number() Over (Order by Name) as seq From sys.tables Cross Apply S#.iQReplace('Select "#Tb#" as Tb, count(*)', '#tb#', OBJECT_SCHEMA_NAME(object_id)+'.'+name) r0 Exec S#.RunScript @PrintOnly=0 Select * from S#.LogTest exec S#.ShowLastQuerybatchInLog -- test error trapping without log table Exec S#.RegisterLogTable '' Insert Into S#.ScriptToRun(sql, seq) Select 'select * from MakeAnErrorThisTableDoesntExist', 1 Exec S#.RunScript @PrintOnly=0 -- test nested call error trapping without log table Exec S#.RegisterLogTable '' Insert Into S#.ScriptToRun(sql, seq) Select ' Insert Into S#.ScriptToRun(sql, seq) Select ''select * from MakeAnErrorThisTableDoesntExist'', 1 Exec S#.RunScript @PrintOnly=0 ', 1 Exec S#.RunScript @PrintOnly=0 -- test error trapping with log table, without extended error trapping Exec S#.RegisterLogTable 'S#.LogTest' Insert Into S#.ScriptToRun(sql, seq) Select 'backup database msdb to disk=''z:\nowhere\msdb.bak'' ', 1 Exec S#.RunScript @PrintOnly=0 Select * from S#.LogTest -- error must also be recorded into table -- test error trapping with log table, with extended error trapping, and no stop on error Exec S#.RegisterLogTable 'S#.LogTest' Insert Into S#.ScriptToRun(sql, seq) Select 'backup database msdb to disk=''z:\msdb\a.bak'' ', 1 UNION ALL Select 'backup database master to disk=''R:\master\b.bak'' ', 2 Exec S#.RunScript @PrintOnly=0, @LogExtendedErr=1, @CatchAndThrow=0 Select * from S#.LogTest -- error must also be recorded into table -- test error trapping with log table, with extended error trapping on DBCC special case -- trapping error with DBCC needs a special procedure Exec S#.RegisterLogTable 'S#.LogTest' Insert Into S#.ScriptToRun(sql, seq) Select 'Exec S#.PerformCheckDbAndReportErrorsIfAny ''DemoCorruption'' ', 1 Exec S#.RunScript @PrintOnly=0, @LogExtendedErr=1 Select * from S#.LogTest -- error must also be recorded into table Exec S#.RegisterLogTable 'S#.LogTest' Insert Into S#.ScriptToRun(sql, seq) values ('select * from MakeAnErrorThisTableDoesntExist', 1) Exec S#.RunScript @PrintOnly=0, @LogExtendedErr=1 Select * from S#.LogTest -- error must also be recorded into table -- test nested call error trapping with log table -- dédoublement message, à cause du throw? Exec S#.RegisterLogTable 'S#.LogTest' Insert Into S#.ScriptToRun(sql, seq) Select ' Insert Into S#.ScriptToRun(sql, seq) Values(''select * from MakeAnErrorThisTableDoesntExist'', 1) Exec S#.RunScript @PrintOnly=0 ', 1 Exec S#.RunScript @PrintOnly=0, @LogExtendedErr=1 Select * from S#.LogTest -- error must also be recorded into table */ /*===KeyWords=== Scripting,Logging ===KeyWords===*/ End GO ------------------------------------------------------------------------------------------------ -- Two functions that returns CLR Code and SQL that match the CLR Code -- they are used in an automated way to do deployement of CLR Code from C# source code -- embedded within. -- all functions defined to feed the SQL CLR compiler Sp in this script -- must start with S#.ReturnClrDefFor_ -- and must be defined on this model, so read the comments inside to take this one as a model ------------------------------------------------------------------------------------------------ -- @@MARK: Dynamic SQL -- CLR and TSQL proc for an assembly who run SQL and get all messages for a given exec. Create Or Alter Function S#.ReturnClrDefFor_ClrExecSqlWithMsgs (@pathAssemblyDll nvarchar(512)) -- ========================================================================================== -- The function prefix must be S#.ReturnClrDefFor_ follow by the assembly name (for clarity) -- ClrExecSqlWithMsgs is an SQLCLR assembly that runs queries, but does something more: -- It catch all messages and their severity, and return them. -- ========================================================================================== Returns Table As Return ( Select code.*, Prm.* From (Select -- Some set of constants that can be used in the view and when querying the view -- to improve self explaing code CSharp='C#', SQL='SQL', ThisFunction='S#.ReturnClrDefFor_ClrExecSqlWithMsgs' , AssemblyName='ClrExecSqlWithMsgs' ,Namespace='CLR_ExecSqlWithMsg' , Class='ExecuteYourSqlDbaCmdsThroughCLR' , pathAssemblyDll=@PathAssemblyDll ) as Prm CROSS APPLY ( Select Language=Prm.Sql, Code=SqlForAssembly.Sql, Seq From ( Select Sql, Seq -- given with each template From (values (1,'Assembly', Prm.AssemblyName) /*===ClrExecSqlWithMsgs=== CREATE ASSEMBLY #AssemblyName# AUTHORIZATION [dbo] FROM #Bits# WITH PERMISSION_SET = EXTERNAL_ACCESS ===ClrExecSqlWithMsgs===*/ , (2,'Procedure', 'S#.Clr_ExecAndLogAllMsgs') /*===S#.Clr_ExecAndLogAllMsgs=== Create Or Alter Procedure S#.Clr_ExecAndLogAllMsgs @SqlCmd nvarchar(max), @MaxSeverity Int Output, @Msgs nvarchar(max) OUTPUT AS EXTERNAL NAME [#AssemblyName#].[#NameSpace#.#Class#].[Clr_ExecAndLogAllMsgs] ===S#.Clr_ExecAndLogAllMsgs===*/ ) as F(Seq, Type, ObjName) CROSS APPLY ( Select Sql From -- Compute comment delimiter for each object name (Select TemplateTag='==='+ObjName+'===') as TemplateTag -- put elements to replace in each template CROSS APPLY (Select J=(Select AssemblyName, pathAssemblyDll, NameSpace, Class for Json Path, INCLUDE_NULL_VALUES)) as j -- each different template is delimited by each computed templateTag, in this view source code CROSS APPLY (Select Sql=Code From S#.GetTemplateFromCmtAndReplaceTags (TemplateTag, ThisFunction, j)) as Sql ) as Creates ) As SqlForAssembly UNION ALL Select Language=Prm.CSharp, code=Line, Seq=L.LineNum From (select code=TxtInCmt From s#.GetCmtBetweenDelim('===C#===', ThisFunction)) as C CROSS APPLY S#.SplitSqlCodeLinesIntoRows (c.Code) as L ) as Code /*===C#=== // about using yield in C# https://www.infoworld.com/article/3122592/my-two-cents-on-the-yield-keyword-in-c.html // about using yield in SqlClr TVF https://www.c-sharpcorner.com/UploadFile/5ef30d/understanding-yield-return-in-C-Sharp/ using System; using System.Data; using System.Security; using System.Data.SqlClient; using System.Data.SqlTypes; using Microsoft.SqlServer.Server; using System.Text; using System.Xml; using System.IO; using System.Reflection; // Set version number for the assembly. [assembly:AssemblyVersionAttribute("1.0.0.0")] [assembly:AssemblyCulture("en")] [assembly:AssemblyTitle("ExecuteYourSqlDbaCmdsThroughCLR")] [assembly:AssemblyDescription("SQL CLR Assembly to run queries and return all messages even including prints")] [assembly:AssemblyCompany("S#")] [assembly:AssemblyProduct("S#")] namespace CLR_ExecSqlWithMsg { public class ExecuteYourSqlDbaCmdsThroughCLR { // implements a suitable way to trap ALL SQL messages which is not easy to do in T-SQL // especially when there are errors with backups that implies an OS problem // such as inxesitant directory, or lack of disk space or IO error. [SqlProcedure(Name = "Clr_ExecAndLogAllMsgs")] public static void Clr_ExecAndLogAllMsgs(SqlChars SqlCmd, out SqlInt32 MaxSeverity, out SqlChars Msgs) { SqlCommand cmd; string LocalMsgs; // must be a local variable to be manipulated before returned by output param Int32 LocalMaxSeverity; // must be a local variable to be manipulated before returned by output param // Using implies an automatic dispose of the connexion object // context connection=true means that the same session as the caller is used using (SqlConnection conn = new SqlConnection("context connection=true;")) { try { // Assumes that "conn" represents a SqlConnection object. conn.Open(); conn.FireInfoMessageEventOnUserErrors = true; // don't stop on first error message, want info message to catch them all // here is the inline delegate trick. This block of code is called back // when SQL Info messages are raised (informational or error) // it then adds messages to an string builder so all messages can be returned as a single string conn.InfoMessage += delegate(object sender, SqlInfoMessageEventArgs args) { LocalMaxSeverity = 0; String message; // use a string builder to cumulate messages StringBuilder SbMsgs = new StringBuilder(""); foreach (SqlError err in args.Errors) { if (!(err.Message == null)) { message = err.Message; if (err.Class == 0) { // don't put special info for messages produced by the print statement // SbMsgs.AppendFormat("{0} ", message); SbMsgs.AppendLine(message); } else { if (err.Class > 10) // error messages { string s; if (err.LineNumber > 0) { s = String.Format(" at line {0} in proc {1} ", err.LineNumber, err.Procedure); } else { s = ""; } SbMsgs.AppendFormat("Error {0}, Severity {1}, level {2} : {3}{4}", err.Number, err.Class, err.State, message, s); SbMsgs.AppendLine(); } else // informational messages { SbMsgs.AppendFormat("Warning Severity {0}, level {1} : {2}", err.Class, err.State, message); SbMsgs.AppendLine(); } if (err.Class > LocalMaxSeverity) { LocalMaxSeverity = err.Class; // this allows to know if a real error occured (severity > 10) } } } } LocalMsgs = SbMsgs.ToString(); }; // end of inline delegate code to trap and stored informational and error messages // execute SP using (cmd = new SqlCommand("sp_executeSql", conn)) { string sql; LocalMsgs = ""; // avoid compile warnings that says that the variable is not initialized LocalMaxSeverity = 0; // avoid compile warnings that says that the variable is not initialized sql = new string(SqlCmd.Buffer); cmd.CommandType = CommandType.StoredProcedure; cmd.Parameters.Add(new SqlParameter("@statement", @SqlCmd)); cmd.ExecuteNonQuery(); Msgs = new SqlChars(LocalMsgs.ToCharArray()); // return local value into output parameter MaxSeverity = LocalMaxSeverity; // return local value into output parameter } } catch (SqlException ex) { throw new ApplicationException(ex.Message); } } } // Clr_ExecAndLogAllMsgs } // class ExecuteYourSqlDbaCmdsThroughCLR } // CLR_ExecSqlWithMsg ===C#===*/ ) -- S#.ReturnClrDefFor_YourSqlDba_ClrExec GO -- @@MARK: File operations -- CLR and TSQL code parts for an assembly that does some file operations Create Or Alter Function S#.ReturnClrDefFor_FileOpCS (@pathAssemblyDll nvarchar(512)) -- ========================================================================================== -- This function returns CSharp code for an SQLCLR assembly that allows some file operations -- It must start with S#.ReturnClrDefFor_ -- It must be defined on this model to returns parts of assembly definition i.e. -- 1) CSharp Code 2) SQL to add assembly to the database and T-SQL that refers to -- modules entry points in the CSharp code. -- ========================================================================================== Returns Table As Return ( Select code.*, Prm.* From (Select -- Some set of constants that can be used in the view and when querying the view -- to improve self explaing code CSharp='C#', SQL='SQL', ThisFunction='S#.ReturnClrDefFor_FileOpCS' , AssemblyName='SomeFileOps',Namespace='CLR_SomeFileOps', Class='FileOpCS' , pathAssemblyDll=@PathAssemblyDll ) as Prm CROSS APPLY ( Select Language=Prm.Sql, Code=SqlForAssembly.Sql, Seq From ( Select Sql, Seq -- given with each template From (values (1,'Assembly', 'SomeFileOps') /*===SomeFileOps=== CREATE ASSEMBLY #AssemblyName# AUTHORIZATION [dbo] FROM #Bits# WITH PERMISSION_SET = EXTERNAL_ACCESS ===SomeFileOps===*/ , (2,'Function', 'S#.clr_FileExists') /*===S#.clr_FileExists=== Create Or Alter Function S#.clr_FileExists (@FilePath nvarchar(4000)) RETURNS TABLE ([FilePath] nvarchar(512), [ExistsFlag] TinyInt) AS EXTERNAL NAME [#AssemblyName#].[#NameSpace#.#Class#].[Clr_FileExists]; ===S#.clr_FileExists===*/ , (3,'Function', 'S#.clr_GetFolderListDetailed') /*===S#.clr_GetFolderListDetailed=== Create Or Alter Function S#.clr_GetFolderListDetailed (@FolderPath nvarchar(4000), @SearchPattern nvarchar(4000)) RETURNS TABLE ([FileName] nvarchar(255), [FileExtension] nvarchar(255), [Size] bigint, [ModifiedDate] datetime, [CreatedDate] datetime, ErrMessage Nvarchar(4000)) AS EXTERNAL NAME [#AssemblyName#].[#NameSpace#.#Class#].[Clr_GetFolderListDetailed]; ===S#.clr_GetFolderListDetailed===*/ , (4,'Proc' , 'S#.clr_DeleteFile') /*===S#.clr_DeleteFile=== CREATE PROC S#.clr_DeleteFile (@FolderPath nvarchar(4000), @ErrorMessage nvarchar(4000) OUTPUT) AS EXTERNAL NAME [#AssemblyName#].[#NameSpace#.#Class#].[Clr_DeleteFile]; ===S#.clr_DeleteFile===*/ , (5,'Proc' , 'S#.clr_DeleteFiles') /*===S#.clr_DeleteFiles=== CREATE PROC S#.clr_DeleteFiles (@FolderPath nvarchar(4000), @SearchPattern nvarchar(4000), @ErrorMessage nvarchar(4000) OUTPUT) AS EXTERNAL NAME [#AssemblyName#].[#NameSpace#.#Class#].[Clr_DeleteFiles]; ===S#.clr_DeleteFiles===*/ ) as F(Seq, Type, ObjName) CROSS APPLY ( Select Sql From -- Compute comment delimiter for each object name (Select TemplateTag='==='+ObjName+'===') as TemplateTag -- put elements to replace in each template CROSS APPLY (Select J=(Select AssemblyName, pathAssemblyDll, NameSpace, Class for Json Path, INCLUDE_NULL_VALUES)) as j -- each different template is delimited by each computed templateTag, in this view source code CROSS APPLY (Select Sql=Code From S#.GetTemplateFromCmtAndReplaceTags (TemplateTag, ThisFunction, j)) as Sql ) as Creates ) As SqlForAssembly UNION ALL Select Language=Prm.CSharp, code=Line, Seq=L.LineNum From (select code=TxtInCmt From s#.GetCmtBetweenDelim('===C#===', ThisFunction)) as C CROSS APPLY S#.SplitSqlCodeLinesIntoRows (c.Code) as L ) as Code /*===C#=== using System; using System.IO; using Microsoft.SqlServer.Server; using System.Data.SqlTypes; using System.Collections; using System.Reflection; // Set version number for the assembly. [assembly:AssemblyVersionAttribute("1.0.0.0")] [assembly:AssemblyCulture("en")] [assembly:AssemblyTitle("FileOpCS")] [assembly:AssemblyDescription("SQL CLR Assembly to allows minor file operations from SQL")] [assembly:AssemblyCompany("S#")] [assembly:AssemblyProduct("S#")] struct FileDetails { public string FileName; public string FileExtension; public long FileSizeByte; public DateTime ModifiedDate; public DateTime CreatedDate; public string ErrMessage; } struct FileExists { public string FilePath; public byte ExistsFlag; } namespace CLR_SomeFileOps { // implements reduced set of file operations that are and could be useful to YourSqlDba (to reduce surface area): // GetFolderListDetailed, Deletefile, Deletefiles, and AppendStringTofile // stored procedure and function names are self explanatory public class FileOpCS { [SqlFunction(Name = "Clr_GetFolderListDetailed", TableDefinition = "FileName nvarchar(255), FileExtension nvarchar(255), FileSizeByte bigint, ModifiedDate datetime, CreatedDate datetime, ErrMsg nvarchar(4000)", FillRowMethodName = "Clr_GetFolderListDetailedFillRow")] public static IEnumerable Clr_GetFolderListDetailed(string FolderPath, string SearchPattern) { string[] FilesIn; ArrayList FilesOut = new ArrayList(); FileDetails fd; FileInfo fi; char[] charsToTrim = {'\\'}; if (FolderPath.EndsWith("\\")) FolderPath = FolderPath.TrimEnd(charsToTrim); try { FilesIn = System.IO.Directory.GetFiles(FolderPath, SearchPattern); foreach (string f in FilesIn) { fi = new FileInfo(f); fd = new FileDetails(); fd.FileName = fi.Name; fd.FileExtension = fi.Extension; fd.FileSizeByte = fi.Length; fd.ModifiedDate = fi.LastWriteTime; fd.CreatedDate = fi.CreationTime; FilesOut.Add(fd); } } catch (Exception ex) { FilesOut.Clear(); fd = new FileDetails(); fd.FileName = ""; fd.FileExtension = "ERR"; fd.FileSizeByte = 0; fd.ModifiedDate = Convert.ToDateTime("1900-01-01"); fd.CreatedDate = Convert.ToDateTime("1900-01-01"); fd.ErrMessage = ex.Message; FilesOut.Add(fd); } return FilesOut; } // method defined for the above function public static void Clr_GetFolderListDetailedFillRow(Object obj, out SqlChars FileName, out SqlChars FileExtension, out SqlInt64 FileSizeByte, out SqlDateTime ModifiedDate, out SqlDateTime CreatedDate, out SqlChars ErrMsg) { FileDetails fd = (FileDetails)obj; FileName = new SqlChars(fd.FileName); FileExtension = new SqlChars(fd.FileExtension); FileSizeByte = new SqlInt64(fd.FileSizeByte); ModifiedDate = new SqlDateTime(fd.ModifiedDate); CreatedDate = new SqlDateTime(fd.CreatedDate); ErrMsg = new SqlChars(fd.ErrMessage); } [SqlFunction(Name = "Clr_FileExists", TableDefinition = "FilePath nvarchar(512), ExistsFlag TinyInt", FillRowMethodName = "Clr_FileExistsFillRow")] public static IEnumerable Clr_FileExists(string FilePath) { FileExists fe; fe.FilePath = FilePath; fe.ExistsFlag = (Byte)(System.IO.File.Exists(FilePath) ? 1 : 0); // this function never raise error, even for access denied, so false is returned in case of error yield return fe; } // // method defined for the above function // public static void Clr_FileExistsFillRow(Object obj, out SqlChars FileName, out SqlByte ExistsFlag) { FileExists fe = (FileExists)obj; FileName = new SqlChars (fe.FilePath); ExistsFlag = new SqlByte (fe.ExistsFlag); } [SqlProcedure(Name = "Clr_DeleteFile")] public static void Clr_DeleteFile(string FilePath, out string ErrorMessage) { try { System.IO.File.Delete(FilePath); ErrorMessage = ""; } catch (Exception ex) { ErrorMessage = ex.Message; } } [SqlProcedure(Name = "Clr_DeleteFiles")] public static void Clr_DeleteFiles(string FolderPath, string SearchPattern, out string ErrorMessage) { string[] Files; char[] charsToTrim = {'\\'}; // if (FolderPath.EndsWith("\\")) FolderPath = FolderPath.TrimEnd(charsToTrim); try { Files = System.IO.Directory.GetFiles(FolderPath, SearchPattern); foreach(string f in Files) System.IO.File.Delete(f); ErrorMessage = ""; } catch (Exception ex) { ErrorMessage = ex.Message; } } } } ===C#===*/ -- both queries below test code output. The one that returns C# and the one that link SQL to C#. -- select * from S#.ReturnClrDefFor_FileOpCS (null) where language=CSharp Order by seq -- select * from S#.ReturnClrDefFor_FileOpCS (null) where language=Sql Order by seq ) -- S#.ReturnClrDefFor_FileOpCS GO -- -------------------------------------------------------------------- -- start of section and tools about deploying native c# code into CLR -- -------------------------------------------------------------------- -- -- Import the assembly bits into a table, to be able to use it in the function that generate -- the code to create the assembly in SQL Server -- DROP TABLE IF EXISTS S#.AssemblyBits; CREATE TABLE S#.AssemblyBits ( AssemblyDllPath NVARCHAR(4000) NOT NULL -- ex: MonAssembly.dll , Bits VARBINARY(MAX) NOT NULL -- le contenu du .dll ); GO -- -- This function generate code to perform all aspects of CLR code deployment -- When invoked, is generates all those steps, but it is possible to filter output on "action" column -- made equal to one of those columns. -- -- -- DropAssembly -- CompileAssembly -- CreateAssembly -- AuthorizeAssembly -- -- To get the code to compile one special cratfed function must be supplied in paramter @AssembleSourceCodeView -- See as exemple S#.ReturnClrDefFor_FileOpCS -- -- @@MARK: Assembly management -- Generated all code to compile, create, Authorize, Drop assembly Create Or Alter Function S#.ScriptAssemblyMgmt (@assemblyName Sysname, @Db sysname, @AssemblySourceCodeView sysname, @Silent int = 1) Returns Table as Return ------------------------------------------------------------------------------------- -- Build code parts for assembly management (drop, compile, create, authorize) -- Thank you very much Solomon Rutzky for your expertise -- and helping me about setting security over assemblies -- see ===AuthorizeAssembly=== template. ------------------------------------------------------------------------------------- Select Prm.*, CodeLines.* --,ToReplaceInName, CertName, CertNameM, LoginName, AssemblyDefInfo, CodeLines.* From (Select db=@Db, aName=@assemblyName, Silent=ISNULL(@Silent, '1') , assemblySourceCodeView = @AssemblySourceCodeView , DropAssembly='DropAssembly' , CompileAssembly='CompileAssembly' , CreateAssembly='CreateAssembly' , AuthorizeAssembly='AuthorizeAssembly' , ThisFunction='S#.ScriptAssemblyMgmt' ) as Prm CROSS APPLY (Select ToReplaceInName=(select Db, aName for Json path)) as ToReplaceInName -- this certificate is local to the DB so db name is not useful in the name -- it is used to sign the assembly CROSS APPLY (Select CertName= replacedTxt from S#.MultipleReplaces('CertToSign_#aName#', ToReplaceInName)) as CertName -- this certificate in master is created from public key of the certificate used to sign the assembly CROSS APPLY (Select CertNameM=replacedTxt from S#.MultipleReplaces('#Db#CertFor_#aName#', ToReplaceInName)) as CertNameM -- this credential (login) is created from the the certificate created in master CROSS APPLY (Select LoginName=replacedTxt from S#.MultipleReplaces('#Db#CredFor_#aName#', ToReplaceInName)) as LoginName -- compute files name and appropriate CSC compiler (that match SQL.Net for this server) CROSS APPLY ( Select * From (Select prm=CAST(value_data as nvarchar(100)) From sys.dm_server_registry Where value_name = 'SQLArg0') as prm Cross Apply (Select NoNulChar=Left(prm,Len(prm)-1)) as NoNulChar Cross Apply (Select NoFile=Replace(NoNulChar, '\master.mdf','\')) as NoFile Cross Apply (Select PathMaster=Stuff(NoFile, 1,2,'')) as PathMaster CROSS APPLY (Select pathAssemblySourceCode=pathMaster+aName+'.cs') as pathAssemblySourceCode CROSS APPLY (Select pathAssemblyDll=pathMaster+aName+'.Dll') as pathAssemblyDll CROSS APPLY (Select pathAssemblyDirAll=pathMaster+aName+'.*') as pathAssemblyDirAll ) as Path CROSS APPLY (Select ServerName =CONVERT(sysname, ServerProperty ('Servername'))) as ServerName CROSS APPLY (Select SqlDotNetDirValue=Value from sys.dm_clr_properties Where name = 'directory') as SqlDotNetDirValue -- remove nul char at the end of the string, problematic in SQL replaces CROSS APPLY (Select SqlDotNetDirBs=IIF(unicode(right(SqlDotNetDirValue,1))=0, Left(SqlDotNetDirValue, len(SqlDotNetDirValue )-1), SqlDotNetDirValue )) as SqlDotNetDirBs -- ensure path ends with '\' CROSS APPLY (Select SqlDotNetDir=SqlDotNetDirBs+IIF(RIGHT(SqlDotNetDirBs,1)<>'\', '\', '')) as SqlDotNetDir CROSS APPLY (Select AssemblyDefInfo= ( Select Prm.*, AssemblyName=AName, pathAssemblySourceCode, pathAssemblyDll, pathAssemblyDirAll , ServerName, AssemblySourceCodeView, SqlDotNetDir, CurrentDb=DB_NAME() , CertName, CertNameM, LoginName , Csharp='C#', Sql='Sql' for Json Path, INCLUDE_NULL_VALUES ) ) as AssemblyDefInfo CROSS APPLY S#.GetTemplateFromCmtAndReplaceTags ('===CompileAssembly===', ThisFunction, AssemblyDefInfo) as CCompileAssembly /*===CompileAssembly=== -- ----------------------------------------------------------------------------------------------------- -- Code that compile an assembly using xp_cmdshell to invoke proper CSC (csharp) compiler -- that match .NET clr level of SQL -- Source code parts of assembly for both C# and SQL are supplied by a callback function -- See S#.ReturnClrDefFor_FileOpCS. It can be taked as an example of a generic mean to define C# and SQL code source parts -- from a function that returns everything necessary to define an assembly from pure SQL code. -- See S#.CompileAssemblyAndCreateSql to see the mean to compile an assembly from any source provided -- a similar S#.ReturnClrDefFor_... function is defined -- See S#.ScriptDeployAllClrDef as an exemple to have many assembly defined in this script -- and to be compiled as a batch. Their names must start with "S#.ReturnClrDefFor_" -- S#.ScriptDeployAllClrDef attempts to call S#.CompileAssemblyAndCreateSql -- For every function starting with this name -- ------------------------------------------------------------------------------------------------------ Set Nocount on If Not Exists(Select * from Sys.configurations Where name = 'show advanced options' And value = '1') Begin exec sp_configure 'show advanced options', '1' RECONFIGURE End If Object_id('S#.XpCmdShellWasOn') IS NULL And Object_id('S#.XpCmdShellWasOff') IS NULL Begin If Exists(Select * From sys.Configurations Where name = 'xp_cmdshell' And value_in_Use=1) Create Table S#.XpCmdShellWasOn (i Int) Else Create Table S#.XpCmdShellWasOff (i Int) End If Object_id('S#.XpCmdShellWasOff') IS NOT NULL Begin Exec sp_configure 'Xp_CmdShell', '1' Reconfigure End Drop table if exists #xp_cmdShellOutput create table #xp_cmdShellOutput (l nvarchar(max)) delete #xp_cmdShellOutput insert into #xp_cmdShellOutput exec xp_cmdShell 'Del /q /f "#pathAssemblyDirAll#" 1> NUL 2>&1' -- -- Just compile if Dll file isn't there to prevent hacking techniques -- delete #xp_cmdShellOutput insert into #xp_cmdShellOutput exec xp_cmdShell ' dir /b "#pathAssemblyDirAll#"' If Exists(Select * From #xp_cmdShellOutput Where l like '#AssemblyName#.%') Begin RAISERROR ('See why "#pathAssemblyDirAll#" cannot be deleted, remove it and try again',11,1) Return End Else Begin Delete #xp_cmdShellOutput insert into #xp_cmdShellOutput -- produce .Cs file from SQLCMD that reads the view with proper params to avoir line header, line truncation Exec xp_cmdshell 'Sqlcmd -E -S #ServerName# -d #CurrentDb# -y 0 -Q "set nocount on;select code from #AssemblySourceCodeView# (null) where language=CSharp Order by seq" -o "#pathAssemblySourceCode#" ' If exists (select top 1 * from #xp_cmdShellOutput Where l is not null) Begin Select [error msg from SqlCmd while writing #AssemblyName# C# .cs file to disk]=l from #xp_cmdShellOutput Raiserror ('#AssemblyName# .cs file failed to be overwritten, check with command: attrib "#pathAssemblyDirAll#" if there is not an abnormal attribute read-only, hidden or system .cs file ',11,1) Return End Delete #xp_cmdShellOutput insert into #xp_cmdShellOutput -- compile the assembly Exec xp_cmdshell 'Call "#SqlDotNetDir#\csc" /target:library /out:"#pathAssemblyDll#" "#pathAssemblySourceCode#"' If Exists(Select * From #xp_cmdShellOutput Where l like '%.cs(%,%): error CS%:%') Begin -- in case of error printout what the compiler said -- and the file name so it is easy to spot as : error CS(line:col) the error' Insert into S#.ScriptToRun (Sql, seq) Select C.Sql, 1 From (Select Lf From S#.Enums) as E CROSS APPLY (Select compilerOutputXml= (Select '-- '+l+E.lf as [text()] From #xp_cmdShellOutput Where l is not null For XML Path(''),TYPE) ) as compilerOutputXml CROSS APPLY (Select compilerOutput=compilerOutputXml.value('.','NVARCHAR(MAX)')) as compilerOutput -- convert xml back to nvarchar(max) making escapes back to original chars. CROSS APPLY ( Select Sql = '-- '+replicate('=',80)+E.lf+ + '-- Compiler output for compile of #AssemblyName#'+E.Lf + '-- see generated source at "#pathAssemblySourceCode#"'+E.lf + '-- '+replicate('=',80)+E.Lf + compilerOutput + '-- '+replicate('=',80)+E.Lf ) as C -- Run this script to report the error in the same session that do the deployment, so it is easier to correlate error and source code and fix it. Exec S#.RunScript @printOnly=0, @Silent=#Silent# End End Delete #xp_cmdShellOutput -- check that the DLL is here, otherwise it means that it was not compiled and there is a problem with the compiler command or the source code that make the compiler fail without producing a DLL insert into #xp_cmdShellOutput exec xp_cmdShell 'dir /b "#pathAssemblyDll#"' -- the DLL could not be created If Not Exists(Select * From #xp_cmdShellOutput Where l like '#AssemblyName#.%') Begin Raiserror ('#AssemblyName# failed to compile, check with with: dir -a-d "#pathAssemblyDirAll#" if there is not a matching hidden or system .cs or .dll file ',11,1) Return End -- Load assembly bits into SQL table to be able to create assembly from bits in SQL, which doesn't require any right on the folder for the service account as long as the file can be read by xp_cmdshell command (which is the case if it is created by xp_cmdshell itself) Delete S#.AssemblyBits -- clean previous bits if any Declare @cmdShellForPS varchar(8000) = '' Select @cmdShellForPS = CmdCallForPs From ( Select PsPrefix = 'Powershell -NoProfile -NoLogo -NonInteractive -OutputFormat Text -EncodedCommand ' , PsCmd = N' $bytes = [IO.File]::ReadAllBytes("#pathAssemblyDll#"); $cn = New-Object System.Data.SqlClient.SqlConnection "Server=#ServerName#;Database=#CurrentDb#;Integrated Security=SSPI"; $cn.Open(); $cmd = $cn.CreateCommand(); $cmd.CommandText = "INSERT INTO S#.AssemblyBits (AssemblyDllPath, Bits) VALUES (@n, @b)"; $cmd.Parameters.Add("@n", [System.Data.SqlDbType]::VarChar, 200).Value = "#pathAssemblyDll#"; $cmd.Parameters.Add("@b", [System.Data.SqlDbType]::VarBinary, -1).Value = $bytes; $cmd.ExecuteNonQuery(); $cn.Close(); ' ) as Ml CROSS APPLY (Select PsCmdBase64=(Select cast(PsCmd as varbinary(max)) for xml path(''), binary base64)) as PsCmdBase64 CROSS APPLY (Select CmdCallForPs=PsPrefix+PsCmdBase64) as CmdCallForPs Exec Xp_cmdShell @cmdShellForPS, NO_OUTPUT If Not Exists(Select * From S#.AssemblyBits) Begin Raiserror ('PS failed to load #AssemblyName# bits from "#pathAssemblyDirAll#" into S#.AssemblyBits ',11,1) Return End -- CREATE ASSEMBLY is going to be done as next step, but with gMSA account, by default -- it doesn't have right to read the DLL and I do not want to introduce a new requirement of rights setup -- for the service account using gMSA on the folder where the DLL is, so I will get the bits using xp_cmdshell + powershell and put them -- in a SQL table, and then create the assembly from the bits in SQL, which doesn't require any external right on the folder for the service account as long as the file can be read by xp_cmdshell command (which is the case if it is created by xp_cmdshell itself) -- and then create the assembly from the bits in SQL, which doesn't require any right on the folder for the service account as long as the file can be read by xp_cmdshell command (which is the case if it is created by xp_cmdshell itself) -- -- If here job is done, erase memory of the process If Object_id('S#.XpCmdShellWasOn') IS NOT NULL And Exists(Select * From sys.Configurations Where name = 'xp_cmdshell' And value_in_Use=1) Return -- was 'on' and leave it as it is If Object_id('S#.XpCmdShellWasOff') IS NOT NULL And Exists(Select * From sys.Configurations Where name = 'xp_cmdshell' And value_in_Use=1) Begin Exec sp_configure 'Xp_Cmdshell', 0 -- put if back off Reconfigure End Drop Table if Exists S#.XpCmdShellWasOn Drop Table if Exists S#.XpCmdShellWasOff ===CompileAssembly===*/ CROSS APPLY S#.GetTemplateFromCmtAndReplaceTags('===DropAssembly===', ThisFunction, AssemblyDefInfo) as CDropAssembly /*===DropAssembly=== Use [#Db#]; -- get objects using the assembly Insert into S#.ScriptToRun (Sql, seq) Select Sql, Seq=ROW_NUMBER() Over (Order by SuperSeq, ModuleName) From (Select AssemblyName = '#assemblyName#') as Prm CROSS APPLY ( select Sql, superSeq=1, ModuleName from sys.assembly_modules M join sys.assemblies A On A.assembly_id = M.assembly_id And A.name = assemblyName JOIN sys.objects as Obj ON Obj.object_id = M.object_id CROSS APPLY (Select ModuleName=S#.FullObjName(M.OBJECT_ID)) as ModuleName CROSS APPLY ( Select DT.DropType From ( Values ('CLR%Function', 'Function') , ('Clr%PROC%', 'Proc') , ('AGGREGATE_FUNCTION', 'AGGREGATE') , ('Clr%Trigger', 'TRIGGER') ) as DT(type_desc_like, DropType) Where Obj.type_desc Like DT.type_desc_like ) as DropType CROSS APPLY (Select Sql='Drop '+DropType+' IF EXISTS '+ModuleName) as Sql UNION ALL Select Distinct SQL='Drop Assembly IF EXISTS '+AssemblyName, SuperSeq=2, '' from sys.assemblies A Where A.Name = AssemblyName ) as Sql Exec S#.RunScript @printOnly=0, @Silent=#Silent#, @RunOnThisDb='#Db#' -- Assembly security objects cleanup If Exists (Select * From Sys.server_Principals Where Name='#LoginName#') Drop Login [#LoginName#] If Exists (Select * From Sys.certificates Where Name='#CertName#') Drop Certificate [#CertName#] Use master; If Exists (Select * From Sys.certificates Where Name='#CertNameM#') Drop Certificate [#CertNameM#] Use [#Db#]; ===DropAssembly===*/ CROSS APPLY S#.GetTemplateFromCmtAndReplaceTags('===CreateAssembly===', ThisFunction, AssemblyDefInfo) as CCreateAssembly /*===CreateAssembly=== -- Create Sql modules from function that returns their DDL Declare @OriginalTrustWorthyState NVARCHAR(3) Select @OriginalTrustWorthyState=IIF(is_trustworthy_on=1,'ON', 'OFF') from sys.databases Where name = IIF('#Db#'='', Db_name(), '#Db#') -- since the assembly has unsafe attribute, the DB needs to be set TRUSTWORTHY before creating it. -- It can be turned off as we will sign the assembly later Alter database [#Db#] Set TRUSTWORTHY On; Insert into S#.ScriptToRun (Sql, Seq) Select Sql=CodeAndHex, Seq From #AssemblySourceCodeView#('#pathAssemblyDll#') as SourceCode CROSS APPLY ( Select CodeAndHex=Replace(SourceCode.Code, '#Bits#', CONVERT(nvarchar(max), Bits, 1) ) From S#.AssemblyBits Where AssemblyDllPath='#pathAssemblyDll#' ) as Code Where Language=Sql -- note the param @RunOnThisDb='#Db#' which make commands redirected to the destination database which may be the current or not Exec S#.RunScript @printOnly=0, @Silent=#Silent#, @RunOnThisDb='#Db#' Exec ('Alter database [#Db#] Set TRUSTWORTHY '+@OriginalTrustWorthyState); ===CreateAssembly===*/ CROSS APPLY S#.GetTemplateFromCmtAndReplaceTags ('===AuthorizeAssembly===', ThisFunction, AssemblyDefInfo) as CAuthorizeAssembly /*===AuthorizeAssembly=== Use [#Db#]; Declare @CertPassword nvarchar(64) = replace(replace(convert(nvarchar(100), newid()), 'D', 'A'), '2','8') DECLARE @SQL NVARCHAR(MAX); Set @sql = N' CREATE CERTIFICATE [#CertName#] ENCRYPTION BY PASSWORD = "'+@CertPassword+'" WITH SUBJECT = "Mean to sign and protect #Db# Assemblies from unauthorized modification", EXPIRY_DATE = "20991231"; ADD SIGNATURE TO Assembly::[#aName#] BY CERTIFICATE [#CertName#] WITH PASSWORD = "'+@CertPassword+'"; ' Set @Sql=replace(@Sql,'"', '''') Exec sp_executeSql @sql DECLARE @PublicKey VARBINARY(MAX) SET @PublicKey = CERTENCODED(CERT_ID(N'#CertName#')); SET @SQL = N'Use master; CREATE CERTIFICATE [#CertNameM#] FROM BINARY = ' + CONVERT(NVARCHAR(MAX), @PublicKey, 1) + N';'; EXEC [master].[sys].[sp_executesql] @SQL; EXEC [master].[sys].[sp_executesql] N'Create Login [#LoginName#] From CERTIFICATE [#CertNameM#]' EXEC [master].[sys].[sp_executesql] N'GRANT UNSAFE ASSEMBLY TO [#LoginName#];' -- REQUIRED!!!! ===AuthorizeAssembly===*/ Cross Apply ( Select Action=CompileAssembly, seq=1, Code='Raiserror(''AssemblySourceCodeView param '+assemblySourceCodeView+' must valid when CompileAssembly is required'',11,1)' Where Object_id(assemblySourceCodeView) IS NULL UNION ALL Select Action=CreateAssembly, seq=2, Code='Raiserror(''AssemblySourceCodeView param '+assemblySourceCodeView+' must valid when CreateAssembly action is required'',11,1)' Where Object_id(assemblySourceCodeView) IS NULL UNION ALL Select Action=DropAssembly, Seq=3, CDropAssembly.Code UNION ALL Select Action=CompileAssembly, Seq=4, CCompileAssembly.Code Where Object_id(assemblySourceCodeView) IS NOT NULL UNION ALL Select Action=CreateAssembly, seq=5, CCreateAssembly.Code Where Object_id(assemblySourceCodeView) IS NOT NULL UNION ALL Select Action=AuthorizeAssembly, seq=6, CAuthorizeAssembly.Code ) as CodeLines /* -- -- displays output of the function that supplies assembly C# code and assembly SQL code -- Select * From (select top 1 AssemblyName From S#.ReturnClrDefFor_FileOpCS(null)) as A CROSS APPLY S#.ScriptAssemblyMgmt(A.AssemblyName, 'Regard', NULL, null) Where action = DropAssembly union all Select * From (select top 1 AssemblyName From S#.ReturnClrDefFor_FileOpCS(null)) as A CROSS APPLY S#.ScriptAssemblyMgmt(A.AssemblyName, 'Regard', 'S#.ReturnClrDefFor_FileOpCS', null) Where action = CompileAssembly union all Select * From (select top 1 AssemblyName From S#.ReturnClrDefFor_FileOpCS(null)) as A CROSS APPLY S#.ScriptAssemblyMgmt(A.AssemblyName, 'Regard', 'S#.ReturnClrDefFor_FileOpCS', null) Where action = CreateAssembly union all Select * From (select top 1 AssemblyName From S#.ReturnClrDefFor_FileOpCS(null)) as A CROSS APPLY S#.ScriptAssemblyMgmt(A.AssemblyName, 'Regard', NULL, null) Where action = AuthorizeAssembly union all -- -- displays output function that supplies assembly C# code and assembly SQL code -- are misnamed -- Select * From (select top 1 AssemblyName From S#.ReturnClrDefFor_FileOpCS(null)) as A CROSS APPLY S#.ScriptAssemblyMgmt(A.AssemblyName, 'Regard', 'S#.NotGoodReturnClrDefFor_FileOpCS', null) Where action = CreateAssembly union all Select * From (select top 1 AssemblyName From S#.ReturnClrDefFor_FileOpCS(null)) as A CROSS APPLY S#.ScriptAssemblyMgmt(A.AssemblyName, 'Regard', 'S#.NotGoodReturnClrDefFor_FileOpCS', null) Where action = CompileAssembly */ Go -- @@MARK: Assembly management -- make all code to Drop assembly Create Or Alter Function S#.ScriptDropAssembly (@assemblyName sysname) Returns Table -- ---------------------------------------------------------------------------- -- Automate droppring any assembly by dropping its SQL objects references first -- ---------------------------------------------------------------------------- as Return ( Select Sql, Seq=ROW_NUMBER() Over (Order by SuperSeq, ModuleName) From -- useful constant to generate code (Select ThisFunction='S#.ScriptDropAssembly') as ThisFunction CROSS JOIN (Select AssemblyName = @assemblyName) as Prm CROSS APPLY ( select Sql, superSeq=1, ModuleName from sys.assembly_modules M join sys.assemblies A On A.assembly_id = M.assembly_id And A.name = @assemblyName JOIN sys.objects as Obj ON Obj.object_id = M.object_id CROSS APPLY (Select ModuleName=S#.FullObjName(M.OBJECT_ID)) as ModuleName CROSS APPLY ( Select DT.DropType From ( Values ('CLR%Function', 'Function') , ('Clr%PROC%', 'Proc') , ('AGGREGATE_FUNCTION', 'AGGREGATE') , ('Clr%Trigger', 'TRIGGER') ) as DT(type_desc_like, DropType) Where Obj.type_desc Like DT.type_desc_like ) as DropType CROSS APPLY (Select Sql='Drop '+DropType+' IF EXISTS '+ModuleName) as Sql UNION ALL Select Distinct SQL='Drop Assembly IF EXISTS '+AssemblyName, SuperSeq=2, '' from sys.assemblies A Where A.Name = AssemblyName ) as Sql -- Select * from S#.ScriptDropAssembly ('SomeFileOps') ) -- S#.ScriptDropAssembly GO -- @@MARK: Assembly management -- make all code to compile assembly Create Or Alter Function S#.ScriptCompileAssemblyAndCreateSql (@AssemblySourceCodeView sysname, @AssemblyName sysname, @Silent Int, @Db Sysname = NULL) Returns table as Return ( Select Sql=Sql.Code, Seq=1 /* , db, ServerName,pathErrLogBefore, pathErrorLog, pathAssemblyDirAll, pathAssemblyDll, pathAssemblySourceCode , SourceCodeView */ From ( Select AssemblySourceCodeView=@AssemblySourceCodeView, AssemblyName=@AssemblyName, Db=ISNULL(@db, Db_name()), Silent=ISNULL(@Silent,1) , ThisFunction='S#.ScriptCompileAssemblyAndCreateSql' ) as Prm CROSS APPLY (Select J=(Select Prm.* for Json Path, INCLUDE_NULL_VALUES)) as j CROSS APPLY S#.GetTemplateFromCmtAndReplaceTags ('===GetAssemblyMgmtcodeParts===', ThisFunction, j) as Sql /*===GetAssemblyMgmtcodeParts=== Insert into S#.ScriptToRun (Sql, Seq) Select code, seq From S#.ScriptAssemblyMgmt('#AssemblyName#', '#Db#', '#AssemblySourceCodeView#', #Silent#) Where action = DropAssembly union all Select code, seq From S#.ScriptAssemblyMgmt('#AssemblyName#', '#Db#', '#AssemblySourceCodeView#', #Silent#) Where action = CompileAssembly union all Select code, seq From S#.ScriptAssemblyMgmt('#AssemblyName#', '#Db#', '#AssemblySourceCodeView#', #Silent#) Where action = CreateAssembly union all Select code, seq From S#.ScriptAssemblyMgmt('#AssemblyName#', '#Db#', '#AssemblySourceCodeView#', #Silent#) Where action = AuthorizeAssembly Exec S#.RunScript @printOnly=0, @Silent=#Silent# ===GetAssemblyMgmtcodeParts===*/ /* Select * From (select top 1 AssemblyName From S#.ReturnClrDefFor_FileOpCS(null)) as A CROSS APPLY S#.ScriptCompileAssemblyAndCreateSql('S#.ReturnClrDefFor_FileOpCS', A.AssemblyName, 0, 'regard') */ ) -- S#.ScriptCompileAssemblyAndCreateSql go -- @@MARK: Assembly management -- make all code needed to compile one assembly and create its SQL Create Or Alter Proc S#.CompileAssemblyAndCreateSql (@SourceCodeView sysname, @AssemblyName sysname, @Silent Int, @destDb Sysname) as Insert Into S#.ScriptToRun(Sql, Seq) Select Sql, seq From S#.ScriptCompileAssemblyAndCreateSql (@SourceCodeView, @AssemblyName, @Silent, @destDb) Select * from S#.scripttorun Exec S#.RunScript @printOnly=0, @silent=@silent -- Exec S#.CompileAssemblyAndCreateSql @SourceCodeView = 'S#.ReturnClrDefFor_FileOpCS', @AssemblyName ='ClrFileOp_DirAndDel', @Silent=0 GO -- @@MARK: Assembly management -- make all code needed to compile assemblies and create their SQL -- Find assemblies and create them Create Or Alter Function S#.ScriptDeployAllClrDef(@Silent Int, @DestDb Sysname = NULL) Returns table as return ( Select Sql, Seq=ROW_NUMBER() Over (Order by ClrDefFct), j From ( Select ClrDefFct=S#.FullObjName(object_id), Silent=@Silent, ThisFunction='S#.ScriptDeployAllClrDef', DestDb=ISNULL(@destDb, Db_Name()) From sys.objects Where S#.FullObjName(object_id) Like '\[S#\].\[ReturnClrDefFor_%' Escape '\' ) as ClrDef CROSS APPLY (Select j=(Select ClrDef.* for Json Path, INCLUDE_NULL_VALUES)) as j CROSS APPLY (Select Sql=Code From S#.GetTemplateFromCmtAndReplaceTags ('===DeployAnAssembly===', ThisFunction, j)) as Sql /*===DeployAnAssembly=== declare @SourceCodeView sysname = '#ClrDefFct#'; Declare @AssemblyName sysname Select Top 1 @AssemblyName = AssemblyName From #ClrDefFct#(null) Insert into S#.ScriptToRun (Sql, Seq) Select Sql, seq From S#.ScriptCompileAssemblyAndCreateSql (@SourceCodeView, @AssemblyName, #Silent#, '#DestDb#') Exec S#.RunScript @printOnly=0, @Silent=#Silent# ===DeployAnAssembly===*/ ) GO -- ========================================================================= -- Deploy CSharp code defined in this script -- ========================================================================= -- @@MARK: Assembly management -- Deploy all assemblies defined in this script by finding them with a filter on their name and by calling the function that compile and create them -- Find assemblies and create them Declare @silent Int = 1 -- change to 1 to suppress verbose output Print '' Print '**************************************************************************************************************************************' Print '-------------------------------- Compiling and signing YourSqlDba assemblies from C# source code in this script ----------------------' Print '-------------------------------- Compilation process can be verbose (seek Declare @Silent Int = 1) and change it to 0 ----------------------' Print '**************************************************************************************************************************************' Print '' Insert into S#.ScriptToRun (Sql, Seq) Select Sql, seq From S#.ScriptDeployAllClrDef(@Silent, DB_NAME()) -- can change param to 0 to verbose generated code Exec S#.RunScript @printOnly=0, @silent=@silent -- can change silent to 0 to verbose generated code Print '-------------------------------- Compile, signing, securing and deployment done ----------------------' GO -- end of section about deploying native c# code into CLR --------------------------------------------------------------------------------------------------------- -- This function is useful to deduplicate duplicated sequence of char into a string --------------------------------------------------------------------------------------------------------- -- @@MARK: String function S#.DedupSeqOfChar Create Or Alter Function S#.DedupSeqOfChar (@Dup nvarchar(5), @Str Nvarchar(max)) Returns table As Return ( Select s = NoMoreRepeatingChar, * -- return both values with different names for compatibility purposes From (Select StrWithCharToDedup=@Str, Dup=@Dup) as vPrm Cross Apply (Select StartEndPair=NChar(0x25BA)+NChar(0x25C4)) As vStartEndPair Cross Apply (Select DupCharReplacedByStartEndPairs=Replace(StrWithCharToDedup, Dup, StartEndPair)) as vDupCharToStartEndPair Cross Apply (Select EndStartPair=NChar(0x25C4)+NChar(0x25BA)) as vEndStartPair Cross Apply (Select EndStartPairsRemoved=replace(DupCharReplacedByStartEndPairs, EndStartPair, '')) as vEndStartPairsRemoved Cross Apply (Select NoMoreRepeatingChar=replace(EndStartPairsRemoved, StartEndPair, Dup)) as vNoRepeatingChar /* Select * From ( Values ('test1', ',', 'a,,,b,c') , ('test2', 'zz', 'zzazzzzbzzc') , ('test3', ' ', '| a b c |') ) as test(name, dup, toDedup) cross apply S#.DedupSeqOfChars(Dup, ToDeDup) */ /*===KeyWords=== String ===KeyWords===*/ ) GO -- @@MARK: Pedigree of the caller for improving logging or maintenance queries Create Or Alter View dbo.WhoCalledWhat As -- --------------------------------------------------------------------------------------------- -- this View returns how the whole thing was called and at topmost level -- plus From Where, what and Who -- --------------------------------------------------------------------------------------------- Select Host , Prog , MainSqlCmd=Srctxt , Who , JobId , Stepid , SqlAgentJobName=SJ.NAme , PrefixStep , PosJobId , PosFinJobId , StartStepId from S#.GetCmtBetweenDelim (null, '') CROSS APPLY (Select Who=Quotename(ORIGINAL_LOGIN())+ IIF(SUSER_SNAME() <> ORIGINAL_LOGIN(), ' Executing as: '+Quotename(SUSER_SNAME()), '')) as Who -- If calling program is SQLAgent, parse it to get JobId and StepId CROSS APPLY (Select Prog=PROGRAM_NAME(), Host=HOST_NAME (), SqlAgentSignature='SQLAgent%0x% : Step %)', PrefixStep=' : Step ') as SearchDom OUTER APPLY (Select PosJobId=Charindex('0x', Prog) Where Prog like SqlAgentSignature) as PosJobId OUTER APPLY (Select PosFinJobId=CharIndex(PrefixStep, Prog)) as PosFinJobId OUTER APPLY (Select JobIdStr=Substring(Prog, PosJobId, PosFinJobId-PosJobId) Where PosJobId >0 And PosFinJobId >0) as JobIdStr OUTER APPLY (Select JobId=convert (uniqueIdentifier, convert(varbinary(200),JobIdStr,1))) as JobId OUTER APPLY (Select StartStepId=PosFinJobId+Len(PrefixStep)+1 Where PosJobId >0 And PosFinJobId >0) as StartStepId OUTER APPLY (Select StepId=Substring(Prog, StartStepId, LEN(Prog)-StartStepId) Where StartStepId > 0) as StepId Left JOIN Msdb.dbo.sysjobs as SJ ON SJ.job_id = JobId /* Create Or Alter Proc A as Exec('B') go Create Or Alter Proc B as Select * from dbo.WhoCalledWhat go Exec A */ GO -- @@MARK: Generate code to set a session context that make accessible global param of maintenance to the callees Create Or Alter Function dbo.ScriptSetGlobalAccessToPrm (@PrmSetInJson as Nvarchar(max)) returns table -- ============================================================================================================= -- -- This function generates code to save a row into Maint.JobHistory. Among the saved info in the row are -- the main query that performs the call (from dbo.WhoCalledWhat), some contextual info about who does the call, -- and in a json string passed from the caller which is the set of parameters that needs to be access through the call -- stack especially when YourSqlDba_DoMaint does a job. -- In some cases, it is used just to set a context which help ExecAndLog use to do reporting, i.e. report the command and who run it. -- -- The key to match info between caller and callees is JobNo, which is made accessible through -- a session context for which the creation is embedded into the script generated by this function -- -- As YourSqlDba was growing it appeared that passing this bunch of parameters around was a pain and bloat the code. -- The call stack may be deep so repeating this is annoying. -- -- For example : The Maint.Backup, is not meant to be called directly, but it needs a lot of param that come either -- from Maint.YourSqlDba_DoMaint of Maint.SaveDbOnNewFileSet. With this mecanism, Maint.Backup may have access to -- all parameters from the caller Maint.YourSqlDba_DoMaint or Maint.SaveDbOnNewFileSet. -- -- Maint.JobHistory contains a row for which jobNo is the key, and with parameters in a form of a json string. -- This row also contains the main SQL query, which is useful for reporting, auditing and debugging. -- -- Related modules: MaincontextInfo -- MainContextInfo ia a function that returns parameters as a single row, for which columns reflect paramaters name. -- It also returns the main Sql query of the procedure that called this functions. -- It finds the info from maint.JobHistory using the session context set for the jobNo. -- -- ============================================================================================================= as return -- See MainContextInfo for complementary info. Select * From (Select ThisFunction='dbo.ScriptSetGlobalAccessToPrm', PrmSetInJson=@PrmSetInJson) as Prm CROSS APPLY (Select templ=TxtInCmt From S#.GetCmtBetweenDelim ('===SetCtx===', ThisFunction)) as templ CROSS APPLY (Select Sql=replace(templ, '#PrmSetInJson#', PrmSetInJson)) as Sql /*===SetCtx=== -- ---------------------------------------------------------------- -- Set global states that can be retrieved in the call stack of -- sub procedure and fonction of the maintenance -- and in a permanent manner for databases across maintenance -- ---------------------------------------------------------------- -- I store all associated relevant params in json format into maint.JobHistory, -- which generate a new jobNo (identity) on insert. This JobNo is also set in a -- sessionContext under name JobNoInSessCtx. -- A Session context allow to memorize this jobNo and make it available -- from any level in the call stack of the session -- Declare @jobNo Int Declare @MainSqlCmd Nvarchar(max) Insert into Maint.JobHistory (JSonPrms, MainSqlCmd, Who, Host, Prog, SqlAgentJobName, JobId, StepId) Select JSonPrms = G.TxtInCmt -- PrmSetInJSon is pre-replaced by main statement of this function and extracted using S#.GetCmtBetweenDelim , W.MainSqlCmd , W.Who , W.Host , W.Prog , W.SqlAgentJobName , W.jobId , W.StepId From Dbo.WhoCalledWhat as W -- get pedigree of the caller -- a useful trick to avoid syntax issues over the json expression is to place it in comments -- and comment content is extracted through S#.GetCmtBetweenDelim. CROSS APPLY S#.GetCmtBetweenDelim ('===PrmSetInJson===', NULL) as G /*===PrmSetInJson=== #PrmSetInJson# ===PrmSetInJson===*/ -- If Session_context is not set this adds the row, otherwise it stops creating another session context. -- This means that every function inthe call stack may ask to set a context without having worry if there is -- already one. If there is one, the one already there is just fine. Where Cast(SESSION_CONTEXT (N'JobNoInSessCtx') as Int) IS NULL Select @jobNo = SCOPE_IDENTITY() -- get newly JobNo identity on insert into Maint.JobHistory Exec SP_Set_Session_context -- memorize jobNo in a Session_Context with key JobNoInSessCtx @key='JobNoInSessCtx' , @value=@JobNo , @read_only=0 ===SetCtx===*/ GO -- ------------------------------------------------------------------------------------------------------------- -- Function that infer many ways to name objects, choose the one that match your in resulting columns -- from partial of full object names OR object_id + optional dabatase Id -- The function accepts an SQL_VARIANT and depending of its type walks different ways to get the names parts -- and from then, returns many naming combinations, quoted or unquoted, partially qualified of schema qualified -- or database+schema+name -- ------------------------------------------------------------------------------------------------------------- -- @@MARK: Object naming vs Object Id Create Or Alter Function Dbo.InferObjectNamings(@objRef SQL_Variant, @DbId Int) Returns Table as Return ( Select Db, Sch, name, SN, DSN, QDb, QSch, QSN, QDSN, hasDbInName, hasSchInName, MinimalQDSN, MinimalDSN , FromHereDebugInfo='Debug inf in next columns' , ObjRef, PrmDbId, Typ, ObjNameFromPrm, ObjIdFromPrm, DbInNameToCheck, DbInName , DbNameFromDbId, DbIdFromNameOrDbId, ObjIdFromPrmObjRefOrPrmId From (Select ObjRef=Convert(Sql_variant, @Objref), PrmDbId=@Dbid) as ObjRef --( -- Test for the function : Must be done from MSDB -- Select ObjRef=Convert(Sql_Variant, 'backupset'), PrmDbId = NULL --union all Select ObjRef=Convert(Sql_Variant, 'MSDB..[backupset]'), PrmDbId = NULL --union all Select ObjRef=Convert(Sql_Variant, '[dbo].[backupset]'), PrmDbId = NULL --union all Select ObjRef=Convert(Sql_Variant, '[managed_backup].[fn_get_parameter]'), PrmDbId = NULL --union all Select ObjRef=Convert(Sql_Variant, '[MSDB].[managed_backup].[fn_get_parameter]'), PrmDbId = NULL --union all Select ObjRef=Convert(Sql_Variant, '[MSDB].[managed_backup].[fn_get_parameter]'), PrmDbId = Db_id('msdb') -- dbid ignored --union all Select ObjRef=Convert(Sql_Variant, object_id('[MSDB].[managed_backup].[fn_get_parameter]')), PrmDbId = Db_id('msdb') -- dbid ok --union all Select ObjRef=Convert(Sql_Variant, object_id('[MSDB].[managed_backup].[fn_get_parameter]')), PrmDbId = NULL -- depend on current db context --) as PrmTst CROSS APPLY (Select Typ=sql_variant_property(ObjRef, 'BaseType')) as Typ OUTER APPLY (Select ObjNameFromPrm=Convert(sysName,ObjRef) Where Typ = 'varchar') as ObjNameFromPrm OUTER APPLY (Select ObjIdFromPrm=Convert(int, ObjRef) Where Typ = 'int') as ObjIdFromPrm CROSS APPLY (Select hasDbInName=IIF(PARSENAME (ObjNameFromPrm, 3) IS NOT NULL,1,0)) as hasDbInName CROSS APPLY (Select hasSchInName=IIF(PARSENAME (ObjNameFromPrm, 2) IS NOT NULL,1,0)) as hasSchInName OUTER APPLY (Select DbInNameToCheck=PARSENAME (ObjNameFromPrm, 3) Where ObjNameFromPrm IS NOT NULL) as DbInNameToCheck OUTER APPLY (Select DbInName=ISNULL(DbInNameToCheck, Db_Name()) Where ObjNameFromPrm IS NOT NULL) as DbInName OUTER APPLY (Select DbNameFromDbId=ISNULL(Db_name(prmDbId), Db_name()) Where ObjIdFromPrm IS Not NULL) as DbNameFromDbId CROSS APPLY (Select Db=CAST(COALESCE(DbInName, DbNameFromDbId) as sysname)) as Db CROSS APPLY (Select DbIdFromNameOrDbId=DB_ID(Db)) as DbIdFromNameOrDbId CROSS APPLY (Select ObjIdFromPrmObjRefOrPrmId=COALESCE(ObjIdFromPrm, OBJECT_ID(ObjNameFromPrm))) as ObjIdFromPrmObjRefOrPrmId CROSS APPLY (Select Sch=OBJECT_SCHEMA_NAME (ObjIdFromPrmObjRefOrPrmId, DbIdFromNameOrDbId)) as Sch CROSS APPLY (Select name=OBJECT_NAME (ObjIdFromPrmObjRefOrPrmId, DbIdFromNameOrDbId)) as Name Cross Apply (Select Dot='.') as Dot CROSS APPLY (Select QSch=QUOTENAME(Sch), QName=QUOTENAME(name), QDb=QUOTENAME(Db)) As QParts CROSS APPLY (Select QSN=Qsch+Dot+Qname) as QSN CROSS APPLY (Select QDSN=QDb+Dot+QSN) as QDSN CROSS APPLY (Select MinimalQDSN=IIF(hasDbInName=1, QDb+Dot,'')+IIF(hasSchInName=1,Qsch+Dot,IIF(hasDbInName=1,Dot,''))+QName) as MinimalQDSN CROSS APPLY (Select SN=Sch+Dot+Name) as Sn CROSS APPLY (Select DSN=Db+Dot+SN) as Dsn CROSS APPLY (Select MinimalDSN=IIF(hasDbInName=1, Db+Dot,'')+IIF(hasSchInName=1,sch+Dot,IIF(hasDbInName=1,Dot,''))+Name) as MinimalDSN /*===KeyWords=== Scripting,Object Management ===KeyWords===*/ ) GO Create Or Alter Function Dbo.DbsFileSizes (@DbNameLike Sysname) -- ---------------------------------------------------------------------------------------------------------- -- This function get database(s) name, logical filename, type of file, physical filename, and size in mb -- ---------------------------------------------------------------------------------------------------------- Returns TABLE as Return SELECT DbName.DbName , Mf.type_desc , FileName.FileName , Mf.physical_name , fSizeInMb FROM sys.databases as Db JOIN sys.master_files as MF WITH(NOWAIT) ON MF.database_id = Db_id(Db.name) CROSS APPLY (Select DbFileSizeInMb = Cast(size * 8. / 1024 AS DECIMAL(8,2))) as DbFileSizeInMb CROSS APPLY (Select DbName=Db.name) as DbName CROSS APPLY (Select FileName=Mf.name) as FileName CROSS APPLY (Select fSizeInMb=CAST(size * 8. / 1024 AS DECIMAL(8,2))) as fSIzeinMb Where Db.name Like @DbNameLike Or @DbNameLike IS NULL /* Select * From Dbo.DbsFileSizes(NULL) -- all file db sizes Select * From Dbo.DbsFileSizes('%GPI%') -- all file db sizes of Db that match the filter */ GO --Drop objects that belongs to the assembly below -- they must be dropped from the assembly before dropping the assembly declare @sql nvarchar(max) ;With InfoModuleAssemblies as ( select OBJECT_SCHEMA_NAME(m.object_id) as NomSchema , OBJECT_NAME(m.object_id) as nomObj , A.name as NomModule from sys.assembly_modules M join sys.assemblies A On A.assembly_id = M.assembly_id ) Select @sql = ( Select convert(nvarchar(max), '') +'Drop Assembly If Exists '+I.NomSchema+'.'+I.nomObj+';'+NCHAR(10) as [text()] from InfoModuleAssemblies I Where NomModule IN ('YourSqlDba_ClrExec', 'YourSqlDba_ClrFileOp') For XML PATH('') ) Set @sql = REPLACE(@sql, '"', '''') --print @sql exec (@sql) GO -- The broker is no more needed in YourSqlDba If DATABASEPROPERTYEX('YourSqlDba', 'IsBrokerEnabled')=1 Begin ALTER DATABASE YourSqlDba SET SINGLE_USER WITH ROLLBACK IMMEDIATE; ALTER DATABASE YourSqlDba SET ERROR_BROKER_CONVERSATIONS; ALTER DATABASE YourSqlDba SET DISABLE_BROKER; ALTER DATABASE YourSqlDba SET MULTI_USER WITH ROLLBACK IMMEDIATE; End GO -- old code removed from version 7.0.0.4 IF EXISTS (SELECT * FROM sys.assemblies asms WHERE asms.name = N'YourSqlDba_ClrExec') DROP ASSEMBLY [YourSqlDba_ClrExec] GO IF EXISTS (SELECT * FROM sys.assemblies asms WHERE asms.name = N'YourSqlDba_ClrFileOp') DROP ASSEMBLY [YourSqlDba_ClrFileOp] GO Create Or Alter Function yInstall.SqlVersionNumber () Returns Int as Begin Declare @i int; With VersionBrute (ver) as (Select convert(nvarchar, serverproperty('ProductVersion'))) Select @i = convert (int, Left(ver, charindex('.', ver)-1)) From VersionBrute return @i*10 -- match compatibility level End GO -- this function reduce repeating spaces to a single one in one quick replace -- @@MARK: String - remove repeating spaces Create Or Alter Function yUtl.ReduceRepeatingSpaceToOne(@s nvarchar(max) ) returns nvarchar(max) as Begin return (replace(replace(replace(@s, ' ', ' !'), '! ', ''), '!', '')) End go -- this procedure reports bestPractices to follow -- @@MARK: Best practices reporting Create Or Alter Function PerfMon.GetBestPracticesMsgs () returns Table as Return ( With MemSqlmax (MemIngb) as ( Select top 1 -- for some reason this top 1 clause makes the optimizer choose an access plan that works fine with the loop below. (found at sql2012 sp3) Convert(int, Round((total_physical_memory_kb/(1024.0)) / 1024.0, 0)) From sys.dm_os_sys_memory ) -- rows builder for a loop , L0 AS (select 1 as c union all Select 1 as c ) --2 , L1 as (select 1 as C From L0 as A Cross JOIN L0 as B ) --4 , L2 as (select 1 as C From L1 as A Cross JOIN L1 as B ) -- 16 , L3 as (select 1 as C From L2 as A Cross JOIN L2 as B ) -- 256 , L4 as (select 1 as C From L3 as A Cross JOIN L3 as B ) -- 65536 -- row_number() limits the number of row built , nums as (Select ROW_NUMBER() OVER (Order by c) as i from L4) -- condition goes over previous row_number() indirectly which limits it , Loop as (Select * From MemSqlMax Join Nums ON i <= MemInGb) , FreeSpaceByGb as ( Select memInGb , Loop.i , Case When Loop.i <= 4 then 0 -- below 4Gb, there is already 1Gb that will be added later When Loop.i > 4.1 and Loop.i <= 16 Then 0.25 -- between 4Gb et 16Gb add 0.25Gb of free space by Gb for the OS When Loop.i > 16.1 Then 0.125 -- Above 16gb add 0.125 Gb of free space by Gb for the OS End as FreeMem From Loop ) --Select * from FreeSpaceByGb Order by i , TbMinFreeSpaceInMb as ( Select MemInGb, 1+SUM(FreeMem) as ToFree, Convert(Int, ((MemInGb - (1+SUM(FreeMem))) * 1024)) as MaxSpaceToUseInMb From FreeSpaceByGb Group By MemInGb ) --Select * from TbMinFreeSpaceInMb , optionVal (opt, val) as ( Select 'Max server memory (MB)', convert(int, MaxSpaceToUseInMb) From sys.configurations CROSS JOIN TbMinFreeSpaceInMb Where (1=0) And name = 'max server memory (MB)' And (value_in_use = 0 Or value_in_use > CONVERT(Int, MaxSpaceToUseInMb)) UNION ALL Select 'max degree of parallelism', 3 From sys.configurations Where name = 'max degree of parallelism' And value = 0 UNION ALL Select 'cost threshold for parallelism', 50 From sys.configurations C1 JOIN sys.configurations C2 ON C2.name = 'max degree of parallelism' And C2.Value <> 1 JOIN sys.dm_os_sys_info DMOS ON DMOS.cpu_count > 1 And DMOS.affinity_type_desc = 'AUTO' Where C1.name = 'cost threshold for parallelism' And C1.value < 50 UNION ALL Select 'backup compression default', 1 From sys.configurations Where name = 'backup compression default' And value <> 1 UNION ALL Select 'nested triggers', 1 From sys.configurations Where name = 'nested triggers' And value <> 1 ) Select Case When @@LANGUAGE <> 'français' Then 'Adjust following server settings by executing following commands:' Else 'Ajuster les propriétés suivantes du serveur en exécutant les commandes suivantes' End + '
' + r3.s + '
Reconfigure
GO
' as MsgLines From ( Select (Select CONVERT(nvarchar(max), '
exec Sp_Configure '''+opt+''', '+convert(nvarchar, val) + '
GO') From optionVal for Xml path('')) as Msgs ) as r0 CROSS APPLY (Select REPLACE (r0.Msgs, '>', '>') ) as r1(s) CROSS APPLY (Select REPLACE (r1.s, '≪', '<') ) as r2(s) CROSS APPLY (Select REPLACE (r2.s, ' ', '
')) as r3(s) ) GO -- @@MARK: Report unapplied best practices Create Or Alter Proc PerfMon.ReportIgnoredBestPractices @email_Address sysname As Begin Declare @Msg Nvarchar(max) Select @Msg = GM.MsgLines From PerfMon.GetBestPracticesMsgs() GM If @Msg IS NOT NULL EXEC Msdb.dbo.sp_send_dbmail @profile_name = 'YourSQLDba_EmailProfile' , @recipients = @email_Address , @importance = 'High' , @subject = 'YourSqlDba : Apply following good practices to your SQL Server configuration' , @body = @Msg , @body_format = 'HTML' -- Exec PerfMon.ReportIgnoredBestPractices '' End GO Create Or Alter Function yUtl.ColumnInfo (@tbName sysname, @colName sysname, @typeName sysname = NULL) returns table as return ( Select OBJECT_SCHEMA_NAME (c.object_id) as schName, object_name(object_id) as TbName, TYPE_NAME (c.user_type_id) as TypeName, c.* From Sys.columns C Where c.object_id = object_id(@tbName) And c.name = @colName And (@typeName is NULL Or @typeName = TYPE_NAME (c.user_type_id) ) ) GO -- yUtl.ColumnInfo -- ------------------------------------------------------------------------------ -- This function unindent TSQL code so that the leftmost code is in column one -- It helps log log dynamic T-SQL that is originally generated indented relative -- to it the code where it is defined. It is to ease nice dynamic code formatting -- at code level, and avoid extra indentation of gnererated code in logs -- ------------------------------------------------------------------------------ -- @@MARK: Logging SQL UnIndent Create Or Alter Function yExecNLog.Unindent_TSQL ( @sql nvarchar(max) ) returns nvarchar(max) as Begin Declare @NbOfLn Int Declare @NextSql nvarchar(max) -- Unindent T-SQL to have leftmost code to start in column on Set @NbOfLn = len(@sql) - len(replace(@sql, nchar(10)+' ', nchar(10)+'')) If @NbOfLn = 0 Return (@sql) -- otherwise endless loop (happen with empty @sql string or @sql string without CRLF While (1 = 1) Begin set @NextSql = replace (@sql, nchar(10)+' ', nchar(10)+'') If len(@sql) - len(@NextSql) = @NbOfLn Set @sql = @NextSql Else Break End -- while Return (@sql) End -- yExecNLog.Unindent_TSQL GO -- @@MARK: TODO : check if this function is still needed Create Or Alter Function yInstall.DoubleLastSpaceInFirst78Colums ( @msg nvarchar(max) ) returns nvarchar(max) as Begin If len(@msg) > 78 -- Note that a bug exist in sp_send_dbmail. -- The @subject parameter is wrap from the 78 th column. -- Then the previous space is replaced by a line feed which is then ignore by sp_send_dbmail. -- A solution is to replace the last space in the first 78 columns of the "@msg" variable -- by two spaces. Begin Declare @First78 nvarchar(max) Declare @reverse78 nvarchar(max) Declare @spacePos int Set @First78 = left(@msg, 78) Set @reverse78 = REVERSE(@First78) Set @spacePos = PATINDEX('% %', @reverse78) -- position of the first space Set @spacePos = 79 - @spacePos -- position of the last space in the first 78 characters Set @msg = STUFF(@msg, @spacePos, 1, ' ') -- Replace the space by 2 spaces End Return @msg End -- yInstall.DoubleLastSpaceInFirst78Colums GO -- @@MARK: TODO : check if this function is still needed Create Or Alter Function yInstall.DoubleLastSpaceInFirst150Colums ( @msg nvarchar(max) ) returns nvarchar(max) as Begin If len(@msg) > 150 -- Note that a bug exist in sp_send_dbmail. -- The @subject parameter is wrap from the 78 th column and the 150 th. -- Then the previous space is replaced by a line feed which is then ignore by sp_send_dbmail. -- A solution is to replace the last space in the first 78 colomns of the "@msg" variable -- by two spaces. And also the 150 th column. Begin Declare @First150 nvarchar(max) Declare @reverse150 nvarchar(max) Declare @spacePos int Set @First150 = left(@msg, 150) Set @reverse150 = REVERSE(@First150) Set @spacePos = PATINDEX('% %', @reverse150) -- position of the first space Set @spacePos = 151 - @spacePos -- position of the last space in the first 150 characters Set @msg = STUFF(@msg, @spacePos, 1, ' ') -- Replace the space by 2 spaces End Return @msg End -- yInstall.DoubleLastSpaceInFirst150Colums GO CREATE Or Alter view PerfMon.LockChainView as With LockChainCTE as ( select A.program_name as BlockingApp , A.hostname as BlockingHost , A.cmd as BlockingCmd , A.spid as BlockindSpid , DB_NAME(A.dbid) as BlockingDbSite , A.blocked as BlockedSPid from master.sys.sysprocesses As A where A.spid >= 51 And A.blocked > 0 UNION ALL Select B.program_name , B.hostname , B.cmd , b.spid , DB_NAME(B.dbid) , B.blocked From LockChainCTE as A join master.sys.sysprocesses as B ON B.spid = A.BlockindSpid ) select A.* , B.program_name as BlockedApp , B.hostname as BlockedHost , B.cmd as BlockedCmd , DB_NAME(b.dbid) as BlockedDbSite from LockChainCte A join master.sys.sysprocesses as B On B.spid = A.BlockedSPid -- End of view PerfMon.LockChainView GO ------------------------------------------------------------------------------------------------------------ -- This procedure is derived from code obtained from Paul Randal blog's site -- @@MARK: Performance : analyse waitstats -- reset Create Or Alter Procedure PerfMon.ResetAnalyzeWaitStats as DBCC SQLPERF ('sys.dm_os_wait_stats', CLEAR); go -- This function is derived from code obtained from Paul Randal blog's site Create Or Alter Function PerfMon.AnalyzeWaitStats () returns table as return ( -- reset wait stats with this : DBCC SQLPERF ('sys.dm_os_wait_stats', CLEAR); with unwanted_wait_types (wait_type) as ( --select --'select '''+wait_type+''' union all ' --from sys.dm_os_wait_stats order by wait_type select 'BROKER_EVENTHANDLER' union all select 'BROKER_RECEIVE_WAITFOR' union all select 'BROKER_TASK_STOP' union all select 'BROKER_TO_FLUSH' union all select 'BROKER_TRANSMITTER' union all select 'CHECKPOINT_QUEUE' union all select 'CHKPT' union all select 'CLR_AUTO_EVENT' union all select 'CLR_MANUAL_EVENT' union all select 'CLR_SEMAPHORE' union all select 'DBMIRROR_DBM_MUTEX' union all select 'DBMIRROR_EVENTS_QUEUE' union all select 'DBMIRRORING_CMD' union all select 'DIRTY_PAGE_POLL' union all -- sql2012 select 'DISPATCHER_QUEUE_SEMAPHORE' union all select 'FFT_RECOVERY' union all select 'FT_IFTS_SCHEDULER_IDLE_WAIT' union all select 'FT_IFTSHC_MUTEX' union all select 'HADR_FILESTREAM_IOMGR_IOCOMPLETION' union all select 'LOGMGR_QUEUE' union all select 'LAZYWRITER_SLEEP' union all select 'ONDEMAND_TASK_QUEUE' union all select 'PWAIT_ALL_COMPONENTS_INITIALIZED' union all Select 'QDS_CLEANUP_STALE_QUERIES_TASK_MAIN_LOOP_SLEEP' union all Select 'QDS_PERSIST_TASK_MAIN_LOOP_SLEEP' union all select 'REQUEST_FOR_DEADLOCK_SEARCH' union all select 'RESOURCE_QUEUE' union all select 'SERVER_IDLE_CHECK' union all select 'SLEEP' union all select 'SLEEP_BPOOL_FLUSH' union all select 'SLEEP_DBSTARTUP' union all select 'SLEEP_DCOMSTARTUP' union all select 'SLEEP_MASTERDBREADY' union all select 'SLEEP_MSDBSTARTUP' union all select 'SLEEP_SYSTEMTASK' union all select 'SLEEP_TASK' union all select 'SLEEP_TEMPDBSTARTUP' union all select 'SNI_HTTP_ACCEPT' union all select 'SP_SERVER_DIAGNOSTICS_SLEEP' union all select 'SQLTRACE_FILE_BUFFER_FLUSH' union all select 'SQLTRACE_INCREMENTAL_FLUSH_SLEEP' union all select 'Total' union all select 'TRACEWRITE' union all select 'WAITFOR' union all select 'WAITFOR_TASKSHUTDOWN' union all select 'XE_DISPATCHER_JOIN' union all select 'XE_DISPATCHER_WAIT' union all select 'XE_TIMER_EVENT' union all select '' where 1=2 ) , Waits_Sum_Wait_time_ms AS ( SELECT wait_type, signal_wait_time_ms, wait_time_ms / 1000.0 AS WaitS, (wait_time_ms - signal_wait_time_ms) / 1000.0 AS ResourceS, signal_wait_time_ms / 1000.0 AS SignalS, waiting_tasks_count AS WaitCount, wait_time_ms, Sum(wait_time_ms) OVER() AS TotalTimeMs, ROW_NUMBER() OVER(ORDER BY wait_time_ms DESC) AS RowNum FROM sys.dm_os_wait_stats WHERE wait_type NOT IN (Select wait_type from unwanted_wait_types) And wait_type not like 'PREEMPTIVE[_]%' ) , Waits AS ( SELECT wait_type, wait_time_ms / 1000.0 AS WaitS, (wait_time_ms - signal_wait_time_ms) / 1000.0 AS ResourceS, signal_wait_time_ms / 1000.0 AS SignalS, Waitcount, 100.0 * wait_time_ms / (case when TotalTimeMs > 0.0 Then TotalTimeMs Else 1 End) AS Percentage, RowNum FROM Waits_Sum_Wait_time_ms Where WaitCount > 0 ) SELECT W1.wait_type AS WaitType, CAST (W1.WaitS AS DECIMAL(14, 2)) AS Wait_S, CAST (W1.ResourceS AS DECIMAL(14, 2)) AS Resource_S, CAST (W1.SignalS AS DECIMAL(14, 2)) AS Signal_S, W1.WaitCount AS WaitCount, CAST (W1.Percentage AS DECIMAL(5, 2)) AS Percentage, CAST ((W1.WaitS / W1.WaitCount) AS DECIMAL (14, 4)) AS AvgWait_S, CAST ((W1.ResourceS / W1.WaitCount) AS DECIMAL (14, 4)) AS AvgRes_S, CAST ((W1.SignalS / W1.WaitCount) AS DECIMAL (14, 4)) AS AvgSig_S FROM Waits AS W1 INNER JOIN Waits AS W2 ON W2.RowNum <= W1.RowNum GROUP BY W1.RowNum, W1.wait_type, W1.WaitS, W1.ResourceS, W1.SignalS, W1.WaitCount, W1.Percentage HAVING SUM (W2.Percentage) - W1.Percentage < 95 -- percentage threshold ) go Create Or Alter Function yPerfMon.ActiveQueryInBatch(@batch nvarchar(max), @start int, @end int) returns table return ( With CalcStartEnd as (Select (@start/2)+1 as start, (CASE @end When -1 Then DATALENGTH(@batch) Else @End End)/2+1 as Stringlen) Select SUBSTRING (@batch, start, Stringlen) as RunningQuery from CalcStartEnd ); go -- @@MARK: Performance : see current queries create view perfmon.SessionInfo as select s.session_id , q.RunningQuery , T.Text as QueryBatch , db_name(r.database_id) as dbName , r.blocking_session_id as BlockedBy , S.host_name , S.program_name , S.status , S.cpu_time , S.memory_usage , S.row_count , S.total_scheduled_time , S.total_elapsed_time , S.reads , S.writes , S.logical_reads , r.start_time , r.percent_complete , s.last_request_start_time , S.last_request_end_time , S.login_name , S.client_interface_name , S.client_version , S.nt_domain , S.nt_user_name , S.context_info , S.endpoint_id , S.is_user_process , S.language , S.date_format , S.date_first , S.quoted_identifier , S.arithabort , S.ansi_null_dflt_on , S.ansi_defaults , S.ansi_warnings , S.ansi_padding , S.ansi_nulls , S.concat_null_yields_null , S.transaction_isolation_level , S.lock_timeout , S.deadlock_priority , S.prev_error , S.original_security_id , S.original_login_name , S.last_successful_logon , S.last_unsuccessful_logon , S.unsuccessful_logons , S.login_time , S.host_process_id , C.protocol_version , C.net_transport , p.query_plan from sys.dm_exec_sessions S left join sys.dm_exec_connections C On C.session_id = S.session_id left join sys.dm_exec_requests R on R.session_id = S.session_id outer apply sys.dm_exec_sql_text (r.sql_handle) as T outer apply yPerfMon.ActiveQueryInBatch(T.Text, r.statement_start_offset, r.statement_end_offset) as q outer apply sys.dm_exec_query_plan(r.plan_handle) p where s.program_name is not null go /*===CreatePerfMon.DetailQueriesStats=== Create Or Alter Function PerfMon.DetailQueriesStats() Returns Table as Return ( With QueryStats as ( SELECT P.type_desc , DB_NAME(database_id) as DbName , object_name(object_id, database_id) as ObjName , CONVERT(nvarchar(max), '') as RunningQryInBatch , T.query_plan , cached_time , last_execution_time , execution_count , total_worker_time , last_worker_time , min_worker_time , max_worker_time , total_physical_reads , last_physical_reads , min_physical_reads , max_physical_reads , total_logical_writes , last_logical_writes , min_logical_writes , max_logical_writes , total_logical_reads , last_logical_reads , min_logical_reads , max_logical_reads , total_elapsed_time , last_elapsed_time , min_elapsed_time , max_elapsed_time FROM master.sys.dm_exec_procedure_stats as P cross apply sys.dm_exec_query_plan(plan_handle) as T UNION ALL SELECT P.type_desc , DB_NAME(database_id) , object_name(object_id, database_id) , CONVERT(nvarchar(max), '') as RunningQryInBatch , T.query_plan , cached_time , last_execution_time , execution_count , total_worker_time , last_worker_time , min_worker_time , max_worker_time , total_physical_reads , last_physical_reads , min_physical_reads , max_physical_reads , total_logical_writes , last_logical_writes , min_logical_writes , max_logical_writes , total_logical_reads , last_logical_reads , min_logical_reads , max_logical_reads , total_elapsed_time , last_elapsed_time , min_elapsed_time , max_elapsed_time FROM master.sys.dm_exec_Trigger_stats as P cross apply sys.dm_exec_query_plan(plan_handle) as T UNION ALL SELECT P.type_desc , DB_NAME(database_id) , object_name(object_id, database_id) , CONVERT(nvarchar(max), '') as RunningQryInBatch , T.query_plan , cached_time , last_execution_time , execution_count , total_worker_time , last_worker_time , min_worker_time , max_worker_time , total_physical_reads , last_physical_reads , min_physical_reads , max_physical_reads , total_logical_writes , last_logical_writes , min_logical_writes , max_logical_writes , total_logical_reads , last_logical_reads , min_logical_reads , max_logical_reads , total_elapsed_time , last_elapsed_time , min_elapsed_time , max_elapsed_time FROM master.sys.dm_exec_Function_stats as P cross apply sys.dm_exec_query_plan(plan_handle) as T UNION ALL SELECT 'Query' , '' , '' , RunningQryInBatch , query_plan , NULL , last_execution_time , execution_count , total_worker_time , last_worker_time , min_worker_time , max_worker_time , total_physical_reads , last_physical_reads , min_physical_reads , max_physical_reads , total_logical_writes , last_logical_writes , min_logical_writes , max_logical_writes , total_logical_reads , last_logical_reads , min_logical_reads , max_logical_reads , total_elapsed_time , last_elapsed_time , min_elapsed_time , max_elapsed_time FROM master.sys.dm_exec_query_stats as Q cross apply sys.dm_exec_query_plan(Q.plan_handle) as P cross apply sys.dm_exec_sql_text(Q.Sql_handle) as T cross apply (Select StartOfQryInBatch = 1+(Q.statement_start_offset/2)) as vStartOfQryInBatch cross apply (Select BatchLen = DATALENGTH(T.text)) as vBatchLen cross apply (Select QryStrLen = 1+(Case When Q.statement_end_offset=-1 Then BatchLen Else Q.statement_end_offset End)/2) as vQueryLen Cross Apply (Select RunningQryInBatch = Substring(T.text, StartOfQryInBatch, QryStrLen)) as vRunningQryInBatch ) Select * From QueryStats Where total_logical_reads/execution_count > 100 ) ===CreatePerfMon.DetailQueriesStats===*/ declare @Sql nvarchar(max) Select @Sql = CreateStmt From (Select FullVersion=cast(ServerProperty('Productversion') as nvarchar(20))) as vFullVersion CROSS APPLY (Select MajorVersion=Left(FullVersion, charindex('.', FullVersion)-1) ) as Majorversion CROSS APPLY (Select MyOwnSqlHandle=sql_handle From sys.dm_exec_requests Where session_id = @@SPID) as vMyOwnSqlHandle -- Select SQL text from this running batch (work in or out of this procedure, and makes easy to select and run Cross Apply (Select batchTxt=Text From sys.dm_exec_sql_text(MyOwnSqlHandle)) as vSelfSpText -- locate and extract code between /*===DynamicCodeTemplateToBuildDropStatement=== and ===DynamicCodeTemplateToBuildDropStatement===*/ above CROSS APPLY (Select CreateStmtDelim='===CreatePerfMon.DetailQueriesStats===') as vCreateStmt CROSS APPLY (Select AfterCmtStart=charindex('/*'+CreateStmtDelim, BatchTxt)+LEN(CreateStmtDelim)+2 ) As vAfterCmtStart CROSS APPLY (Select PosStartCmtEnd=charindex(CreateStmtDelim+'*/', BatchTxt) ) as vPosStartCmtEnd CROSS APPLY (Select CreateStmt=Substring(BatchTxt, AfterCmtStart, PosStartCmtEnd-AfterCmtStart) ) as vCommentContent Where Majorversion >= 13 Exec (@Sql) GO -- ------------------------------------------------------------------------------ -- Création des tables d'historique -- ------------------------------------------------------------------------------ -- @@MARK: TODO : check if this function is still needed If object_id('Maint.DbMaintPolicies') is not null And Not Exists (select * from yUtl.ColumnInfo ('Maint.DbMaintPolicies', 'FullBkExt', NULL)) Drop Table if Exists Maint.DbMaintPolicies go Drop Table If Exists Maint.XpCmdShellSavedState go CREATE TABLE Maint.XpCmdShellSavedState ( EnforceSingleRowTable int default 1 primary key , value_In_Use int ) go -- @@MARK: Install : Reimport data for jobHistory -- create the latest version by keep most recent data from previous version -- that match by columns names Declare @sql nvarchar(max) Set @sql = ' Create table Maint.JobHistory ( JobNo int identity(1,1) , JobStart DateTime Default (getdate()) , JobEnd DateTime Default (getdate()) , spid Int Default @@spid , JSonPrms Nvarchar(max) , MainSqlCmd Nvarchar(max) , Who sysname NULL , Host sysname NULL , Prog sysname NULL , SqlAgentJobName Sysname NULL , JobId UniqueIdentifier NULL , StepId Int NULL , constraint Pk_HistMaintTrav primary key clustered (JobNo) ) ' Exec (@sql) If object_id('tempdb..##JobHistory') is not null -- successfully created Begin Select @Sql = Sql From ( Select * From ( -- comma separated list of matching column name between previous version of the table and this one Select MatchingColsList= ( Select convert(nvarchar(max), ','+name) as [text()] -- concat matching cols From YourSqlDba.Sys.Columns Y Where object_id=Object_id('YourSqlDba.Maint.JobHistory') And Exists -- a matching col of original table in TempDb..##JobHistory ( Select * FROM tempdb.Sys.Columns Tmp Where object_id=Object_id('TempDb..##JobHistory') And Y.Name = Tmp.Name Collate Database_default ) Order by column_id For xml path('') ) ) as MatchingColsBeforeAndAfter -- remove first comma in the cols list CROSS APPLY (Select MatchingCols=Stuff(MatchingColsList, 1, 1, '')) as MatchingCols ) As MatchingCols CROSS JOIN ( Select Template = ' Set Identity_insert YourSqlDba.Maint.JobHistory ON Delete YourSqlDba.Maint.JobHistory Insert into YourSqlDba.Maint.JobHistory («Cols») Select «Cols» From ##JobHistory Drop Table If Exists ##JobHistory Set Identity_insert YourSqlDba.Maint.JobHistory OFF ' ) as Template CROSS APPLY (Select Sql=REPLACE(template, '«Cols»', MatchingCols)) as Sql Exec (@sql) End GO -- @@MARK: Maintenance : Return context info of the maintenance Create Or Alter Function dbo.MainContextInfo (@jobNo Int) returns table as return -- --------------------------------------------------------------------------------------------- -- -- Main maintenance sp have numerous parameters and many of them are used also -- in stored procedures and functions in the underlying call stack. -- -- Some underlying procedures may also be call directly by the user, and when it is the case, -- this is also a problem. -- -- These values are kind of global. This is a context of the maintenance process. -- Passing them (they are numerous) across the call stack is a coding pain and bloat the code. -- -- To workaround this, this function make them available, indirectly, through -- Maint.JobHistory in which the are pre-stored. See function dbo.ScriptSetGlobalAccessToPrm to see how it is done. -- -- dbo.ScriptSetGlobalAccessToPrm generates a script that insert theses values, and also from the jobNo value obtained -- on insert (identity column), it set a session context containing this value. If packs parameters into a jSonPrms col -- and stored also the topmost calling SQL into mainSqlCmd -- -- This function gets the JobNo from the session_context which is the key to Maint.JobHisotry, and then gets parameters -- packed in a jSon format (JsonPrms). It shreds them into differents colums into a single row, each column reflecting the -- column name. It also get from Maint.JobHistory the main SQL query. This last value is very valuable for reporting, auditing and debugging -- -- --------------------------------------------------------------------------------------------- Select -- this select returns a bunch of columns tant can relate partially or completely to parameters of either -- Maint.YourSqlDba_DoMaint -- Maint.DeleteOldBackup -- Maint.SaveDbOnNewFileSet, -- Maint.ExportDb... JobNo , JobStart , JobEnd , Oper=JSON_VALUE(JsonPrms, '$.oper') , MaintJobName = JSON_VALUE(JsonPrms, '$.MaintJobName') , IncDb=JSON_VALUE(JsonPrms, '$.IncDb') , ExcDb=JSON_VALUE(JsonPrms, '$.ExcDb') , DoInteg = JSON_VALUE(JsonPrms, '$.DoInteg') , DoUpdStats = JSON_VALUE(JsonPrms, '$.DoUpdStats') , DoReorg = JSON_VALUE(JsonPrms, '$.DoReorg') , DoBackup = JSON_VALUE(JsonPrms, '$.DoBackup') , DoFullBkp = JSON_VALUE(JsonPrms, '$.DoFullBkp') , DoDiffBkp = JSON_VALUE(JsonPrms, '$.DoDiffBkp') , DoLogBkp = JSON_VALUE(JsonPrms, '$.DoLogBkp') , MigrationTestMode = JSON_VALUE(JsonPrms, '$.MigrationTestMode') , FullBackupPath = JSON_VALUE(JsonPrms, '$.FullBackupPath') , LogBackupPath = JSON_VALUE(JsonPrms, '$.LogBackupPath') , TimeStampNamingForBackups = JSON_VALUE(JsonPrms, '$.TimeStampNamingForBackups') , FullBkExt = JSON_VALUE(JsonPrms, '$.FullBkExt') , LogBkExt = JSON_VALUE(JsonPrms, '$.LogBkExt') , FullBkpRetDays = Cast(JSON_VALUE(JsonPrms, '$.FullBkpRetDays') as int) , LogBkpRetDays = Cast(JSON_VALUE(JsonPrms, '$.LogBkpRetDays') as int) , NotifyMandatoryFullDbBkpBeforeLogBkp = JSON_VALUE(JsonPrms, '$.NotifyMandatoryFullDbBkpBeforeLogBkp') , BkpLogsOnSameFile = JSON_VALUE(JsonPrms, '$.BkpLogsOnSameFile') , SpreadUpdStatRun = Cast(JSON_VALUE(JsonPrms, '$.SpreadUpdStatRun') as int) , SpreadCheckDb = Cast(JSON_VALUE(JsonPrms, '$.SpreadCheckDb') as int) , ConsecutiveDaysOfFailedBackupsToPutDbOffline = Cast(JSON_VALUE(JsonPrms, '$.ConsecutiveDaysOfFailedBackupsToPutDbOffline') as int) , MirrorServer = JSON_VALUE(JsonPrms, '$.MirrorServer') , ReplaceSrcBkpPathToMatchingMirrorPath = JSON_VALUE(JsonPrms, '$.ReplaceSrcBkpPathToMatchingMirrorPath') , ReplacePathsInDbFilenames = JSON_VALUE(JsonPrms, '$.ReplacePathsInDbFilenames') , ExcDbFromPolicy_CheckFullRecoveryModel = JSON_VALUE(JsonPrms, '$.ExcDbFromPolicy_CheckFullRecoveryModel') , EncryptionAlgorithm = JSON_VALUE(JsonPrms, '$.EncryptionAlgorithm') , EncryptionCertificate = JSON_VALUE(JsonPrms, '$.EncryptionCertificate') , MainSqlCmd , Who , Host , Prog , SqlAgentJobName , JobId , StepId , JSonPrms From ( -- most of the time, JobNo is taken from Session_context, because dbo.MainContextInfo is called with a NULL param. Select JobSelected = ISNULL(@JobNo, Cast(SESSION_CONTEXT (N'JobNoInSessCtx') as Int)) ) as JobSelected CROSS APPLY (Select JobNo, JobStart, JobEnd, JSonPrms, MainSqlCmd, Who, host, prog, SqlAgentJobName, JobId, StepId From Maint.JobHistory Where jobNo = JobSelected) as AllCtx GO -- @@MARK: Install : Reimport data for other table -- create the latest version by keep most recent data -- this IF is just useful when testing some parts of the code oterwise it is always true when running the whole script If object_id('Maint.JobSeqCheckDb') is null Begin Declare @sql nvarchar(max) Set @sql = ' Create table Maint.JobSeqUpdStat ( seq int ) Insert into Maint.JobSeqUpdStat values(0) ' Exec (@sql) If Object_Id('tempdb..##JobSeqUpdStat') IS NOT NULL -- try keep previous value Exec ( ' If Exists(Select * from Maint.JobSeqUpdStat) Truncate Table Maint.JobSeqUpdStat Insert Into Maint.JobSeqUpdStat (seq) Select Seq From ##JobSeqUpdStat If @@rowcount = 0 Insert into Maint.JobSeqUpdStat values(0) Drop Table If Exists ##JobSeqUpdStat ' ) End GO -- this IF is just useful when testing some parts of the code oterwise it is always true when running the whole script If object_id('Maint.JobSeqCheckDb') is null Begin Declare @sql nvarchar(max) Set @sql = ' Create table Maint.JobSeqCheckDb ( seq int ) -- values are updated at start Insert into Maint.JobSeqCheckDb values(0) ' Exec (@sql) End GO -- Dbcc ShrinkLog state -- no need to upgrade If object_id('Maint.DbccShrinkLogState') is null Begin Declare @sql nvarchar(max) Set @sql = ' Create table Maint.DbccShrinkLogState ( dbName Sysname , FailedShrinkTime Datetime NULL , constraint Pk_DbccShrinkLogState primary key (dbName) ) ' Exec (@sql) End GO -- this IF is just useful when testing some parts of the code oterwise it is always true when running the whole script If object_id('Maint.JobLastBkpLocations') is null Begin Declare @sql nvarchar(max) Set @sql = ' Create table Maint.JobLastBkpLocations ( dbName Sysname , lastLogBkpFile nvarchar(512) NULL , FailedBkpCnt Int Default 0 , lastFullBkpFile nvarchar(512) NULL , lastDiffBkpFile nvarchar(512) NULL , keepTrace bit default 0 NOT NULL , MirrorServer Sysname NULL , MigrationTestMode Int Default 0 , lastFullBkpDate Datetime , ReplaceSrcBkpPathToMatchingMirrorPath nvarchar(max) NULL , ReplacePathsInDbFilenames nvarchar(max) NULL , EncryptionAlgorithm nvarchar(10) NULL , EncryptionCertificate nvarchar(100) NULL , constraint Pk_HistMaintDernBkpPart primary key clustered (dbName) ) ' Exec (@sql) If Object_Id('tempdb..##JobLastBkpLocations') IS NOT NULL Begin ;With MatchingColsBeforeAndAfter as ( Select ( Select convert(nvarchar(max), ','+name) as [text()] From YourSqlDba.Sys.Columns Y Where object_id=Object_id('YourSqlDba.Maint.JobLastBkpLocations') And Exists ( Select * FROM tempdb.Sys.Columns Tmp Where object_id=Object_id('TempDb..##JobLastBkpLocations') And Y.Name = Tmp.Name Collate Database_default ) Order by column_id For xml path('') ) as Cols -- comma separated list of matching column name between previous version of the table and this one ) , Template as ( Select ' Insert into YourSqlDba.Maint.JobLastBkpLocations («Cols») Select «Cols» From ##JobLastBkpLocations Drop Table If Exists ##JobLastBkpLocations ' as Sql , Stuff(Cols, 1, 1, '') as Cols -- remove first comma in the cols list From MatchingColsBeforeAndAfter ) Select @Sql = r0.s From Template CROSS APPLY (Select REPLACE(Sql, '«Cols»', Cols)) as r0(s) Exec(@Sql) --select @sql End End GO -- this IF is just useful when testing some parts of the code oterwise it is always true when running the whole script If object_id('Mirroring.RestoreQueue') is null Begin Declare @sql nvarchar(max) Set @sql = ' Create table Mirroring.RestoreQueue ( RestoreSeq Int Identity (1,1) , QueuedAt Datetime Default Getdate() , StartedAt Datetime Default NULL , EndedAt Datetime Default NULL , CallingJobNo Int Not Null -- to be able to have a common context to log restore events under the same job as the calling one , JSonPrms Nvarchar(max) , ErrorN Int Default 0 ) Create Unique Index iRestoreQueueNextJob On Mirroring.RestoreQueue (RestoreSeq) Where ErrorN=0 ' Exec (@sql) End GO -- this IF is just useful when testing some parts of the code oterwise it is always true when running the whole script If object_id('Mirroring.TargetServer') is null Begin Declare @sql nvarchar(max) Set @sql = ' create table Mirroring.TargetServer ( MirrorServerName sysname Not Null default "" , constraint PK_TargetServer Primary Key (MirrorServerName) ) ' Set @Sql = Replace(@Sql, '"', '''') Exec (@sql) If Object_Id('tempdb..##TargetServer') IS NOT NULL Begin Set @sql = ' Insert Into Mirroring.TargetServer (MirrorServerName) Select ISNULL(T.MirrorServerName, "") From ##TargetServer T Where Exists(Select * From Sys.Servers as S Where S.name = T.MirrorServerName Collate Database_Default And S.is_linked = 1) -- cleanup missing mirrorServer if no matching linked server exists Drop Table If Exists ##TargetServer ' Set @Sql = Replace(@Sql, '"', '''') Exec (@sql) End -- MIGRATE OLD SQLNCLI% not supported provider to new MSOLEDBSQL (compatible with SQL Server 2012 and later) Insert into S#.ScriptToRun (sql, seq) Select sql, seq=ROW_NUMBER() over (Order by name) From (Select MirrorServerName=MirrorServerName From Mirroring.TargetServer) as M CROSS APPLY ( SELECT name, data_source, provider FROM Sys.Servers Where name Collate Database_Default = M.MirrorServerName And product = 'SQL Server' And provider like 'SQLNCLI%') as Prm CROSS APPLY (Select j=(Select Prm.* for Json Path)) as j CROSS APPLY (Select Sql=Code From S#.GetTemplateFromCmtAndReplaceTags ('===UpgradeSQlClient===', NULL, j)) as Sql /*===UpgradeSQlClient=== -- -- Here is the time to your linked server with SQLNCLI% provider (deprecated) to MSOLEDBSQL (fully supported) -- EXEC YourSqlDba.Mirroring.DropServer '#Name#', 'dropLogins' Print '--recreer le serveur lié #Name# avec Mirroring.AddServer. See https://onedrive.live.com/personal/12c385255443c4ed/_layouts/15/Doc.aspx?sourcedoc=%7B5443c4ed-8525-20c3-8012-a81b00000000%7D&action=view&redeem=aHR0cHM6Ly8xZHJ2Lm1zL28vYy8xMmMzODUyNTU0NDNjNGVkL0V1M0VRMVFsaGNNZ2dCS29Hd0FBQUFBQlJ2b290QVJmaE5LQjJaenNPU09yZkE_ZT11c0h6Vms&wd=target%28REFERENCE.one%7Cc7b30aeb-6ae2-4bd6-a550-14feb11d776d%2FMirroring.AddServer%7Ca71c4787-8076-4ed3-a6be-d6c5c3c8b6b3%2F%29&wdorigin=703&wdpartid=%7B2da72b12-728f-4f44-ba3b-477df906c323%7D%7B80%7D&wdsectionfileid=%7B12c385255443c4ed%21sfb02454d2d084363a169b209686c280b%7D ' Print '--SAMPLE:' Print 'Exec YourSQLDba.Mirroring.AddServer' Print ' @MirrorServer = ''#Name#''' Print ', @RemoteLogin = ''sa'' ' -- or equivalent login on the mirror server with enough right to create linked server and access to msdb for backup restore history and to the user databases for backup restore and integrity check, and also with right to view server state for monitoring session during restore and checkdb Print ', @RemotePassword = ''choose your Password For Your @RemoteLogin'' ' ===UpgradeSQlClient===*/ Exec S#.RunScript END GO -- this IF is just useful when testing some parts of the code oterwise it is always true when running the whole script If object_id('Maint.JobHistoryDetails') is NULL Begin Declare @sql nvarchar(max) Set @sql = ' Create table Maint.JobHistoryDetails ( JobNo Int Not NULL , seq Int identity(1,1) , cmdStartTime datetime default getdate() , Secs Int , ForDiagOnly Bit NULL , constraint PK_HistMaintSql primary key clustered (JobNo, seq) , constraint FK_JobMaintHistoryDetails_TO_JobMaintHistory foreign key (JobNo) references Maint.JobHistory (JobNo) On delete cascade ) Create Index iCmdStartTime On Maint.JobHistoryDetails(CmdStartTime) ' Exec (@sql) End GO -- this IF is just useful when testing some parts of the code oterwise it is always true when running the whole script If object_id('Maint.JobHistoryLineDetails') is NULL Begin Declare @sql nvarchar(max) Set @sql = ' Create table Maint.JobHistoryLineDetails ( JobNo Int Not NULL , seq Int Not NULL , TypSeq Int Not NULL , Typ nvarchar(6) Not NULL , line int Not NULL , Txt Nvarchar(max) NULL , constraint PK_JobHistoryLineDetails primary key clustered (JobNo, seq, TypSeq, line) , constraint FK_JobMaintHistoryLineDetails_TO_JobMaintHistoryDetails foreign key (JobNo, seq) references Maint.JobHistoryDetails (JobNo, seq) On delete cascade ) ' Exec (@sql) End GO -- -------------------------------------------------------------------------------------------- -- Return SQL code or XML text string in multi-rows, 1 row per code line -- Workround of the 8000 char limit when printing SQL code. -- -------------------------------------------------------------------------------------------- -- @@MARK: TODO : check if this function is still needed and see the alternate S#.Sqlcode... Create Or Alter Function yExecNLog.SqlCodeLinesInResultSet ( @sql nvarchar(max) ) RETURNS @TxtSql TABLE (i int identity (1, 1), txt nvarchar(max)) AS Begin declare @i int, @d Datetime If @i > 0 Insert into @txtSql (txt) values ('-- Seq:'+ltrim(str(@i))+ ' Time:'+convert(nvarchar(20), @d, 120) + ' ' + replicate('-', 10) ) If @sql is null Or @sql = '' Begin Insert into @txtSql (txt) values ('') return End declare @Start int, @End Int, @line nvarchar(max), @EOLChars int Set @Start = 1 Set @End=0 -- Normalize end-of-line -- Sql server interpret first #13#10 as a valid end-of-line otherwise #10 -- If #10#13 is found it is shown a two end-of-line Set @sql = REPLACE(@sql, nchar(13)+nchar(10), nchar(10)) -- normalize #13#10 -> #10 Set @sql = REPLACE(@sql, nchar(13), nchar(10)) -- normalize #10#13 -> #10#10 -- shown like this in normal SSMS output If charindex(Nchar(10), @Sql)=0 Set @sql=@sql+nchar(10) While(1=1) Begin Set @end = charindex(nchar(10), @Sql, @Start) If @End = 0 Begin Set @line = Substring(@sql, @Start, len(@sql)-@Start+1) Break End Else Set @line = Substring(@sql, @Start, @End-@Start+1) Set @Start = @End+1 Insert into @txtSql (txt) values (replace (replace (@line, nchar(10), ''), nchar(13), '')) End RETURN End -- yExecNLog.SqlCodeLinesInResultSet GO -- -- If object_id('tempdb..##JobHistoryLineDetails') IS NULL -- mock up an empty table for conversion so the query below will work Begin Exec( ' Select top 0 JobNo=1, seq=1, TypSeq=1, typ=convert(nvarchar(6),null), Line=1, Action=convert(nvarchar(max),null) Into ##JobHistoryLineDetails ') End -- this IF is just useful when testing some parts of the code oterwise it is always true when running the whole script If object_id('tempdb..##JobHistoryDetails') IS NOT NULL Begin Set Identity_insert YourSqlDba.Maint.JobHistoryDetails ON Exec (' -- With version 6.8.0.0 a complete overhaul of YourSqlDba logging system is done -- Everything meaningful (and a lot more readable) is directly inserted from yExecNLog.LogAndOrExec -- JobHistoryDetails only keeps track of entry sequence in the job Insert into YourSqlDba.Maint.JobHistoryDetails (JobNo, Seq, cmdStartTime, Secs, ForDiagOnly) Select JobNo, Seq, cmdStartTime, Secs, ForDiagOnly From ##JobHistoryDetails ') Set Identity_insert YourSqlDba.Maint.JobHistoryDetails OFF End GO If object_id('tempdb..##JobHistoryLineDetails') IS NOT NULL Begin Exec (' Insert into YourSqlDba.Maint.JobHistoryLineDetails (JobNo, Seq, TypSeq, Typ, line, Txt) Select * From ##JobHistoryLineDetails ') End GO -- this IF is just useful when testing some parts of the code oterwise it is always true when running the whole script If object_id('Maint.TemporaryBackupHeaderInfo') is NULL Begin Declare @sql nvarchar(max) Set @sql = ' Create table Maint.TemporaryBackupHeaderInfo ( spid int default @@spid -- columns needed by YourSqlDba , BackupType smallint , Position smallint , DeviceType tinyint , DatabaseName nvarchar(128) , LastLSN numeric(25,0) , constraint PK_TemporaryBackupHeaderInfo primary key (spid, backupType, position, deviceType, DatabaseName) ) ' Exec (@sql) End go -- this IF is just useful when testing some parts of the code oterwise it is always true when running the whole script If object_id('Maint.TemporaryBackupFileListInfo') is NULL Begin Declare @sql nvarchar(max) Set @sql = ' Create table Maint.TemporaryBackupFileListInfo ( spid int default @@spid -- columns needed by YourSqlDba , LogicalName nvarchar(128) -- Logical name of the file. , PhysicalName nvarchar(260) -- Physical or operating-system name of the file. , Type NCHAR(1) -- The type of file, one of: L = Microsoft SQL Server log file D = SQL Server data file F = Full Text Catalog , FileID bigint -- File identifier, unique within the database. , constraint PK_TemporaryBackupFileListInfo primary key (spid, FileId, Type) ) ' Exec (@sql) End go declare @sql nvarchar(max) Set @sql = ' Drop Function If Exists Install.VersionInfo Exec sp_ExecuteSql @SqlCreateFunction ' Set @sql = REPLACE(@sql, '"', '''') declare @createFct nvarchar(max) Set @createFct = ' Create Or Alter Function Install.VersionInfo () returns table as return ( Select "" As VersionNumber , "" as VersionDate , "" as UpdateReminderDate , Replicate ("=", 40) + nchar(10)+ "YourSQLDba version: " + nchar(10)+ Replicate ("=", 40) as Msg ) -- Install.VersionInfo ' Set @createFct = REPLACE(@createFct, '', (select version from #Version)) Set @createFct = REPLACE(@createFct, '', (select convert(nvarchar(10), versionDate, 120) from #Version)) Set @createFct = REPLACE(@createFct, '', (select convert(nvarchar(10), getdate()+365, 120))) Set @createFct = REPLACE(@createFct, '"', '''') Exec Sp_ExecuteSql @sql, N'@SqlCreateFunction nvarchar(max)', @SqlCreateFunction=@createFct go Create Or Alter Proc Install.PrintVersionInfo as Begin declare @versionInfo as nvarchar(500) Select @versionInfo = msg from Install.VersionInfo () Print @versionInfo End go Create Or Alter Function yInstall.NextUpdateTime ( ) returns datetime as Begin return(Select max(convert(datetime, UpdateReminderDate, 120)) from Install.VersionInfo() as f) End -- yInstall.NextUpdateTime GO -- @@MARK: TODO : check if this function is still needed Create Or Alter Function yUtl.UnicodeCrLf ( ) RETURNS nchar(2) AS BEGIN Return (N' ') END -- yUtl.UnicodeCrLf GO -- ----------------- fonction yUtl.SplitList ------------------------------------------------------------ -- Function that split a string into a set of rows based on a @sep list -- ----------------------------------------------------------------------------------------------------------- -- @@MARK: TODO : check if this function is still needed If OBJECT_ID('yUtl.SplitList') is not null drop function yUtl.SplitList go Create Or Alter Function yUtl.SplitList (@Sep nvarchar(max), @list nvarchar(max)) returns @items table (item nvarchar(max), seq int) as Begin declare @start as Int, @Next as Int, @seq as int, @item as nvarchar(max) select @start = 1, @seq = 0, @Next = 1 While (@next > 0) Begin Select @seq = @seq + 1, @Next = CHARINDEX (@Sep, @list, @start) If @Next > 0 Set @item = ltrim(SUBSTRING (@list, @start, @next-@start)) Else Set @item = ltrim(SUBSTRING (@list, @start, len(@list)+1-@start)) Insert into @items values (nullif (@item, ''), @seq) Set @start = @next+1 End return End GO -- @@MARK: TODO : check if this function is still needed Create Or Alter Function yUtl.NormalizeLineEnds -- line ends can be expressed as a pipe char or a regular line end except if it ends by \ ( @prm VARCHAR(max) = '' ) returns VARCHAR(max) as Begin -- remove tabs and finish string by '|', turn line ends chars into '|' and replace multiple consecutives '|' by a single one. -- |||| (|) by (.|) done twice because the last one in done on the first time --> '|.|.|.| --> (.|) by () --> '|' Return ( Select Case When @prm = '' Then '' Else replace(replace(replace(replace(replace (replace(replace(@prm, char(9), '')+'|', nchar(10), '|'), nchar(13), '|'), '||', '|.|'), '||', '|.|'), '.|', ''), '.|', '') End ) End -- yUtl.NormalizeLineEnds --Select yUtl.NormalizeLineEnds(' --one --two --') --Select '!'+yUtl.NormalizeLineEnds('')+'!' GO -- ------------------------------------------------------------------------------ -- Function to select database from @incDb and @excDb parameters -- or replace pairs from @replace... parameters -- ------------------------------------------------------------------------------ -- @@MARK: TODO : check if this function is still needed Create Or Alter Function yUtl.SplitParamInRows ( @prm VARCHAR(max) = '' ) returns @rows table ( No int identity , line nvarchar(max) ) as Begin Declare @line nvarchar(max) -- remove tabs from selection patterns and -- add separator at the end of the parameter list, -- so no exception is required in the processing of the list Set @prm = yUtl.NormalizeLineEnds(rtrim(@prm)) -- Extract rows and add it to @rows table While charindex('|', @Prm) > 0 -- While there is a separator Begin Set @line = ltrim(rtrim(Left (@Prm, charindex('|', @Prm)-1))) -- If it reveals some contents add it If @line <> '' Insert into @rows (line) values (@line) -- remove all up to and including '|' Set @Prm = Stuff(@Prm, 1, charindex('|', @Prm), '') End Return; End -- yUtl.SplitParamInRows GO -- @@MARK: TODO : improve this function to new functional style Create Or Alter Function yUtl.YourSQLDba_ApplyFilterDb ( @IncDb VARCHAR(max) = '' -- @IncDb : See following comments for explanation , @ExcDb VARCHAR(max) = '' -- @ExcDb : See following comments for explanation ) returns @Db table ( DbName sysname , dbOwner sysname NULL -- because actual owner may be invalid after a restore , FullRecoveryMode int , cmptLevel tinyint ) as Begin -- Create table of inclusion and exclusion patterns that apply to database names declare @DbName sysname declare @Pat table ( rech sysname, -- search pattern action char(1) -- 'I' = include if pattern match 'E' = exclude if pattern match ) Insert into @pat Select line, 'I' from yUtl.SplitParamInRows (@IncDb) Insert into @pat Select line, 'E' from yUtl.SplitParamInRows (@ExcDb) -- ===================================================================================== -- Build database list to process -- ===================================================================================== -- Build Db list into temporary table and retain its recovery mode (for possible log backup processing) Insert into @Db (DbName, dbOwner, FullRecoveryMode, cmptLevel) Select name , SUSER_SNAME(owner_sid) , Case When DATABASEPROPERTYEX(name, 'Recovery') = 'Simple' Then 0 -- simple recovery mode, no log backup possible Else 1 -- full recovery mode, log backup possible End as FullRecoveryMode, compatibility_level from master.sys.databases Where name <> 'tempdb' -- If there is at least one inclusion pattern, remove from database list those that -- doesn't match this pattern. Remove from the rest of the list those that match -- with the exclusion pattern -- Yes it can be done in one single query ;) Delete D From @Db D Where ( -- only if there is any inclusion pattern Exists (Select * From @Pat Where Action = 'I') -- delete databases that don't match, otherwise nothing is deleted And Not Exists (Select * From @Pat P Where P.action = 'I' And D.DbName like P.Rech) ) -- Suppress any database from the list that match exclusion pattern Or Exists (Select * From @Pat P Where P.action = 'E' And D.DbName like P.Rech) return End; -- yUtl.YourSQLDba_ApplyFilterDb -- ------------------------------------------------------------------------------ -- Function that normalize path (ensure that a '\' is at the end of the path -- ------------------------------------------------------------------------------ GO Create Or Alter Function yUtl.NormalizePath ( @path nvarchar(512) ) returns nvarchar(512) as Begin If right(@path, 1) <> '\' Set @path = @path + '\' Set @path = left(@path, 2) + replace(substring(@path, 3, 512), '\\', '\') return (@path) -- Some tests -- Select yUtl.NormalizePath('c:\isql2005Backups') -- Select yUtl.NormalizePath('c:\isql2005Backups\\') -- Select yUtl.NormalizePath('c:\isql2005Backups\') -- Select yUtl.NormalizePath('\\aserver\aShare') -- Select yUtl.NormalizePath('\\aserver\\aShare') End -- yUtl.NormalizePath GO ----------------------------- -- @@MARK: TODO : check code use Create Or Alter Function yUtl.SearchWord ( @mot sysname , @str nvarchar(max) , @deb int ) returns int as Begin -- procédure spécifique à la recherche des mot clés SQL suivants -- char, varchar, text, image, declare, datalength, convert, create table -- on ne permet pas que quand on trouve ces chaînes dans le texte -- qu'elles soient précédés ou suivies des caractères suivants -- qui manifestent qu'ils ne s'agit pas de ces mots clés -- les crochets [] on été acceptés de justesse car il y a une colonne -- de Edugroupe qui s'appelle Text et pour ne pas convertir la -- table on a mis le nom de colonne entre crochet dans le code. -- On peut donc distinguer dans une procédure s'il s'agit en fait -- du type en enlevant les crochets s'il y a lieu, sinon on en met. -- .a-z0-9\#/@[]_ Declare @dir Int Declare @c Char(1) If @deb is NULL Set @deb = 0 -- évite l'initalisation nécessaire de la variable passée en paramètre Set @dir = @deb If @dir < 0 -- reverse la chaîne pour simuler rechercher à reculons Begin Set @str = reverse(@str) Set @mot = reverse(@mot) Set @deb = Abs(@deb) -- Si chaine se termine par blancs, les blancs de fin sont pas comptés ! If @deb > len(@str+'.') Set @deb = len(@str+'.') -1 Set @deb = len(@str+'.')-@deb End --print '"'+@mot+'" --> "'+@str+'"' Set @deb = @deb-1 -- pour permettre expression commode @deb+1 en partant While(1=1) Begin set @deb = charindex(@mot, @str, @deb+1) --print @deb If @deb = 0 Break --print substring(@str, @deb-1, 1) If @deb > 1 Begin Set @c = substring(@str, @deb-1, 1) -- parce que like boggue quand on a '[' dedans If @c like '[.a-z0-9\#/]' Or @c in ('@','[', '_') Continue End If @deb + len(@mot) > len(@str) Break --print substring(@str, @deb + len(@mot), 1) set @c = substring(@str, @deb + len(@mot), 1) -- parce que like boggue quand on a ']' dedans If @c like '[.a-z0-9/#\]' Or @c in ('@','[', '_') Continue Break -- si ici on a toutes les conditions ok, donc trouvé End If @dir < 0 And @deb > 0 Set @deb = len(@str+'.')-(@deb+len(@mot)-1) return (@deb) End -- yUtl.SearchWord GO -- Alter Create Or Alter Function yUtl.SearchWords ( @mot sysname , @str nvarchar(max) ) returns @Tags table ( posMotCle int -- pos repere ) as Begin Declare @pos int While (1=1) Begin Set @pos = yUtl.SearchWord(@mot, @str, @pos) If @pos = 0 break insert into @Tags (posMotCle) values(@pos) Set @pos = @pos + 1 End return End -- yUtl.SearchWords GO -- Ensure there is at least a S#.LogTable function If Object_id('S#.LogTable') IS NULL Exec ('Create Or Alter Function S#.LogTable() Returns sysname as Begin Return('''') End') GO -- ------------------------------------------------------------------------------------------- -- Register log table for RunScriptToRun and create a function that returns its name -- ------------------------------------------------------------------------------------------- Create Or Alter Procedure S#.RegisterLogTable @LogTable sysname = NULL As Begin Set Nocount On Declare @Sql nvarchar(max) Select @Sql=B.TxtInCmt From -- instead of S#.GetTxtBtwnTagCmt S#.GetCmtBetweenDelim('===DropLogTableFunction===', @@PROCID) as B /*===DropLogTableFunction=== If Object_Id('S#.LogTable') IS NOT NULL DROP Function S#.LogTable ===DropLogTableFunction===*/ Exec (@sql) Select @Sql=CreateLogTable From S#.GetCmtBetweenDelim('===CreateLogTable===', @@PROCID) as B CROSS APPLY (Select CreateLogTable=Replace(B.TxtInCmt, '#logTable#', @LogTable) ) as vCreateLogTable /*===CreateLogTable=== Drop Function If Exists#logTable#', @silent=1 Create table #logTable# (Line nvarchar(max), seq int identity, batchNo BigInt) Create index [i#logTable#] on #logTable# (batchNo desc) ===CreateLogTable===*/ Where isnull(@logTable,'') <> '' Exec (@Sql) Select @Sql=CreateLogTableFunction From S#.GetCmtBetweenDelim('===CreateLogTableFunction===', @@PROCID) as B CROSS APPLY (Select CreateLogTableFunction=Replace(B.TxtInCmt, '#logTable#', isnull(@logTable,'') ) ) as vCreateLogTableFunction /*===CreateLogTableFunction=== Create Or Alter Function S#.LogTable () Returns sysname as Begin Return('#logTable#') End ===CreateLogTableFunction===*/ Exec (@Sql) --Exec S#.RegisterLogTable 'aLogTable'-- test it /*===KeyWords=== Scripting,Logging ===KeyWords===*/ End GO -- @@MARK: TODO : very code use Create Or Alter Procedure S#.QueryLog @like nvarchar(max) = NULL, @NomLog sysname = NULL as Begin Set Nocount On declare @sql nvarchar(max); Select @sql = Sql From (Select LogTable=ISNULL(@NomLog, S#.LogTable())) as vLogTable OUTER APPLY (Select Qry='Print ''There isn''''t default log. Use S#.RegisterLogTable with a logname''' Where LogTable = '') as vQry OUTER APPLY (Select QryPLain='Select Line from #LogTable# order by batchNo, seq' Where @Like is NULL) as vQryPLain -- locate batch number where there is at least 1 line that match like and print them ordered if all above are false OUTER APPLY (Select QryLike='Select Line From (select distinct batchno from #LogTable# where Line like @like) B Join #LogTable# M ON M.batchNo = B.batchNo order by M.batchNo, seq') as vQryLike CROSS APPLY (Select Sql0 = COALESCE(Qry, QryPlain, QryLike)) as vSql0 CROSS APPLY (Select Sql=Replace(Sql0, '#LogTable#', LogTable)) as vSql Exec sp_executeSql @sql, N'@Like nvarchar(max), @NomLog Sysname', @like, @NomLog End /*===KeyWords=== Scripting,Logging ===KeyWords===*/ GO -- ------------------------------------------------------------------------------------------- -- Print messages and log to a table if necessary -- ------------------------------------------------------------------------------------------- -- @@MARK: TODO : very code use Create Or Alter Procedure S#.PrintAndLog @LogText Nvarchar(Max) -- text to log , @nextBatch Int = 0 -- As Begin Set Nocount On Declare @SqlToPrintAndLog nvarchar(max) Select @SqlToPrintAndLog=Sql From (Select LogTable=S#.LogTable()) as LogTable CROSS APPLY(Select PrintPart='Print @LogText') as PR OUTER APPLY ( Select PrintAndLog=PrintPart+AndLog From S#.GetCmtBetweenDelim('===AndLog===', @@PROCID) as G CROSS APPLY (Select AndLog=Replace(G.TxtInCmt, '#LogTable#', LogTable)) as AndLog /*===AndLog=== Insert into #LogTable# (line, batchNo) Select LogText, BatchNo From (Select LogText=@LogText) as vLogText OUTER APPLY (Select Top 1 BatchNumberInLogTable=BatchNo From #logTable# Order By BatchNo Desc) as BatchNumberInLogTable CROSS APPLY (Select BatchNo=ISNULL(BatchNumberInLogTable, 0) + @NextBatch) as vBatchNo ===AndLog===*/ Where LogTable <> '' ) as AL CROSS APPLY (Select Sql=COALESCE(PrintAndLog, PrintPart)) as Sql Exec Sp_ExecuteSql @SqlToPrintAndLog, N'@LogText Nvarchar(Max), @nextBatch Int', @LogText, @nextBatch /* Exec S#.RegisterLogTable '' Exec S#.PrintAndLog 'test text', 0 Exec S#.RegisterLogTable 'S#.LogTest' Exec S#.PrintAndLog 'test text', 1 Exec('Select * from S#.LogTest') Exec S#.PrintAndLog 'next test text', 0 Exec('Select * from S#.LogTest') Exec S#.PrintAndLog 'next batch test text', 1 Exec('Select * from S#.LogTest') */ End /*===KeyWords=== Scripting,Logging ===KeyWords===*/ GO -- ------------------------------------ yExecNLog.PrintSqlCode ---------------------------------- -- Use yExecNLog.SqlCodeLinesInResultSet for sql code printing purposes -- ---------------------------------------------------------------------------------------------------------- -- @@MARK: TODO : very code use Create Or Alter Procedure yExecNLog.PrintSqlCode @sql nvarchar(max) , @numberingRequired int = 0 AS Begin Set nocount on If @Sql IS NULL Begin Print 'Bug : Query text parameter is null !!' return End declare @codeLines table (i int primary key, txt nvarchar(max)) insert into @codeLines (i, txt) Select i, txt From yExecNLog.SqlCodeLinesInResultSet (@sql) Declare @Seq Int Declare @Line nvarchar(max) Set @seq = -1 While (1=1) Begin Select top 1 @Seq = i, @line = txt from @codeLines Where i > @seq Order by i If @@ROWCOUNT = 0 break If @numberingRequired = 0 Print @line Else Print Str(@seq,5)+' '+@line End --Set @Line = Str(@seq,5)+' line(s) printed' raiserror (@line,10,1) with nowait -- force print output End -- yExecNLog.PrintSqlCode GO -- ------------------------------------------------------------------------------ -- This procedure wraps the call to the clr_exec -- @@MARK: Dynamic SQL for YourSqlDba Create Or Alter Procedure yExecNLog.ExecWithProfilerTrace @sql nvarchar(max), @maxSeverity int output, @Msgs nvarchar(max) = '' output as Begin ---- **** code emprunté de S#.RunScript ---- **** permet à S#.RunScript d'être roulé indépendamment et a ---- yExecNLog.ExecWithProfilerTrace d'être roulé indépendamment ---- ExtendedEvents Session for this spid allows the capture of multiple messages (error or not) ---- on a statement execution (like DBCC ou Backup/Restore) ---- as S#.ScriptManageEventSession is a function that returns the proper sequence of actions to start the event session ---- Start the event session, if not already start because of a previous run --Declare @ManageEvent Nvarchar(Max) --Set @ManageEvent ='' --Select @ManageEvent = @ManageEvent+ProperActionSequence --From -- S#.Enums as Consts -- -- Consts.ExtendedEventSession@Start is a constant understood by S#.ScriptManageEventSession -- -- to generate script to start the event session -- Cross Apply S#.ScriptManageEventSession(Consts.ExtendedEventSession@Start) As A --If @@rowcount>0 Exec (@ManageEvent) Set @MaxSeverity = 0 -- récupérer les messages @msgs ici en cas d'erreur pour les retourner à yExecNLog.LogAndOrExec Exec S#.Clr_ExecAndLogAllMsgs @sqlcmd=@sql, @maxSeverity=@maxSeverity output, @msgs=@msgs output If @sql is null Set @sql = '' If @msgs is null Set @msgs = '' -- produce something that profiler can display of the dynamic query launched, and error messages and warnings if any Exec( 'declare @i int; set @i = 1 /* YourSqlDba profiler trace as a dummy instruction to Show Clr_ExecAndLogAllMsgs @sql param and @msgs output =============================================================================================== '+@sql+' ----------------------------------------------------------------------------------------------- '+@msgs+' =============================================================================================== */' ) End GO Create or Alter proc yExecNLog.LogAndOrExec @YourSqlDbaNo nvarchar(max) = NULL , @context nvarchar(4000) = NULL , @sql nvarchar(max) = '' -- this is a convenient way to know if LogAndOrExec was called without SQL command , @Info nvarchar(max) = NULL , @err nvarchar(max) = NULL -- input : err msg to be written to log; , @errorN Int = 0 Output -- output: error flag (1 or 0) set by an sql execution , @raiseError int = 0 , @forDiagOnly int = 0 as -- @@MARK: Exec Dynamic SQL and Logging for YourSqlDba yExecNLog LogAndOrExec Begin -- ------------------------------------------------------------------------------ -- -- The main reason for this procedure is also to wrap SQL code execution with proper -- error logging and trapping. -- -- But this procedure also plays many roles: -- either logging of different type of messages -- informational, high level error message, error raised on intend, unexpected error conditions -- sometimes trapped error conditions -- -- so both use can happen, exclusively or together -- -- To know what to do depend on a combination of values of parameters -- -- When @err is already specified ignore @sql parameter. -- The intent is to log high level error messages -- When @err is missing, @Info or @context specified but @sql = '' by parameter omission, -- intent is to log pure informational messages -- When @sql parameter is not empty, it must be run dynamically and any error messages -- would be loggued. By precaution, a message is written in case the command should -- not return because of a high severity error. -- @Sql should never set to null on call. This null state is a programming error -- that happens when generating the query where one of the parts -- that is used to build the dynamic query is unexpectedly left null. -- this is loggued as an important debugging aid, because dynamic execution of -- a null Sql command leave otherwise no error message, and isn't trapped by the engine as an error -- ------------------------------------------------------------------------------ Set nocount on Declare @NbOfLn Int Declare @Msgs varchar(max) -- spcified as varchar(max) to auto convert unicode character to convertable char to xml Declare @maxSeverity Int Declare @newJobName nvarchar(128) Declare @xml xml Set @errorN = 0 --********** DON'T EVER DROP THIS TABLE AT RUN-TIME. It is here just for debugging. --Dropping the table cause reentrancy problem, by clearing caller's temporary table with same name --Drop table if Exists #PrmLogAndOrExec --I'm put a kind of unique name for temptable, in case a calling sp would also create a #prm table -- in that case this would create an error here Create table #prmLogAndOrExec ( JobNo INT NULL , YourSqlDbaNo nvarchar(max) Collate database_default , context nvarchar(4000) Collate database_default , sql nvarchar(max) Collate database_default , Info nvarchar(max) Collate database_default , err nvarchar(max) Collate database_default , errorN Int NULL , [raiseError] int NULL , forDiagOnly int NULL , Seq Int NULL ) -- create #prmLogAndOrExec on same model than Maint.JobHistoryLineDetails Select top 0 * Into #trcLogAndOrExec From Maint.JobHistoryLineDetails Begin TRY -- special entries are logged for job start that describe the job and it parameters -- the not exists clause ensure that it is done only once. Declare @seq Int Insert into Maint.JobHistoryDetails (JobNo, forDiagOnly) Select JobNo, @forDiagOnly From dbo.MainContextInfo(null) Set @Seq=SCOPE_IDENTITY() -- memorize parameters into #prmLogAndOrExec, which make queries easier to maintain with intellisence Insert into #prmLogAndOrExec Select M.JobNo, @YourSqlDbaNo, @Context, @sql, @Info, Err.err, @errorN, @raiseError, @forDiagOnly, @seq From dbo.MainContextInfo(null) as M -- S#.FormatCurrentMsg return the message 'No Err Message' if there is no error -- otherwise the current error context CROSS APPLY (Select ErrMsgCtx=ErrMessage From S#.FormatCurrentMsg (null)) as ErrMsgCtx -- if @err is set to '?', it is a signal to take the current message from the err context -- this which avoid to store it at call time into a variable and pass it as a parameter CROSS APPLY (Select Err=IIF(@err='?', errMsgCtx, @err)) as Err -- Required to build output to YourSqlDba log insert into #trcLogAndOrExec Select I.JobNo , I.Seq , ToLog.TypSeq , ToLog.typ , ToLog.Line , ToLog.Txt From -- only happens once at job start (Select JobNo, YourSqlDbaNo, Context, sql, Info, err, errorN, [raiseError], forDiagOnly, seq From #prmLogAndOrExec as I) as I -- shred into columns JSonPrms of JobHistory for info that goes in messages -- parameter NULL could be used again, as before. But jobNo was recorded in yExecNLog.LogAndOrExecPrm table, so we take it there instead CROSS APPLY Dbo.MainContextInfo (I.JobNo) as J JOIN Maint.JobHistory JH ON JH.JobNo = I.jobNo CROSS APPLY ( -- insert job header (multiple entries inserted before the first entry) if it isn't there yet -- which consist of a descriptive of the newly starting job -- Easily found if it the actual JobNo is not yet in the table Maint.JobHistoryLineDetails Select TypSeq, Typ='Job', Line=LineOrd, Txt=Line From (Select TypSeq=0 Where Not Exists (Select * From Maint.JobHistoryLineDetails As D Where D.JobNo = I.JobNo)) as KillIfNotTrue CROSS APPLY ( Select LineOrd=1, Line=Replicate('=', 128) UNION ALL Select LineOrd=2, Line='JobNo: '+Convert(nvarchar, JH.JobNo)+' JobStart: '+convert(nvarchar, J.JobStart, 120)+' JobName= '+J.MaintJobName UNION ALL Select LineOrd=3, Line=Replicate('=', 128) UNION ALL Select LineOrd=4, Line='Job first start seq is '+convert(nvarchar,I.Seq) UNION ALL Select LineOrd=5, Line='Task requested: DBCC Checkdb' Where J.DoInteg = 1 Or J.DoInteg IS NULL UNION ALL Select LineOrd=6, Line='Task requested: Update Statistics' Where J.DoUpdStats = 1 Or J.DoUpdStats IS NULL UNION ALL Select LineOrd=7, Line='Task requested: Reorganize fragmented indexes' Where J.DoReorg = 1 Or J.DoReorg IS NULL UNION ALL Select LineOrd=8, Line='Task requested: Backup Databases to '+J.FullBackupPath Where J.DoFullBkp = 1 Or J.DoFullBkp is null UNION ALL Select LineOrd=9, Line='Task requested: Backup Transaction Logs to '+J.LogBackupPath Where J.DoLogBkp = 1 Or J.DoLogBkp is null Union All Select LineOrd=10, Line='Databases to match: ' + replace(J.IncDb, nchar(10), '') Where ltrim(replace(J.IncDb, nchar(10), ''))<> '' Union All Select LineOrd=11, Line='Databases to exclude: ' + replace(J.ExcDb, nchar(10), '') Where ltrim(replace(J.ExcDb, nchar(10), ''))<> '' ) as Det -- printout query or informational info or error info (typSeq=6, typ=errY) UNION ALL Select TypSeq, Typ, Line=LineOrd, Txt=Line From ( -- many kind of information may be loggued for a single event. -- Each is defined by a condition (the where of each select ... union all) is the condition to find if it is there -- So accordinginly the line column is set to the information source, -- the typSeq is set to the proper display order in sequence -- and the column is set to a readable value that match its source Select TypSeq=1, typ='Time', lineOrd=1, Line='=== '+ convert(nvarchar, Getdate(), 121) + ' ' +Replicate('=', 110) Union all Select TypSeq=2, typ='ctx', lineOrd=1, line=I.Context Where I.context is not null union all Select TypSeq=3, typ='inf', lineOrd=1, line=I.info Where I.info is not null union all Select TypSeq=5, typ='Sql', lineOrd=X.i, line=X.txt From -- Unindent T-SQL to have leftmost code to start in column one (Select SqlU=yExecNLog.Unindent_TSQL(I.sql) Where I.Sql <> '') as SqlU CROSS APPLY yExecNLog.SqlCodeLinesInResultSet( yExecNLog.Unindent_TSQL(sqlu)) as x -- derived table S must return non null err value or null SQL to signal an error -- here this means the YourSqlDbaNo is Set to one of the specific values in the query while err is null -- or generated SQL by the caller was NULL, which is abnormal -- or the caller caught an error, and call yExecNLog only for reporting Union all Select TypSeq=6, typ='errY', lineOrd=X.i, line=X.txt From -- Only one SELECT of conditions below must return a result otherwise it will make a duplicate error into JobHistoryLineDetails ( -- an error text where specified by caller Select Err Where I.Err <>'?' UNION ALL Select Err = 'YourSqlDba error notification '+convert(nvarchar,I.YourSqlDbaNo)+'. Please check ctx, inf, or msgs types' Where charindex(I.YourSqlDbaNo, '005 006 007 008 009 012 013 015 021 999')>0 And I.Err IS NULL -- YourSqlDba tried to build a dynamic query but either concatenated a null in the process or failed to generate SQL UNION ALL Select Err='Unexpected YourSqlDba error : Dynamically generated SQL IS NULL' Where Sql IS NULL And I.Err IS NULL UNION ALL -- When caller catch an unexpected error it can let the task to ExecAndLog -- to get current error state and format it. This request is expressed by Err parameter being '?' Select Err=ErrMsg From S#.FormatCurrentMsg (null) Where I.Err = '?' ) as S -- Split over many rows, lines of the error message if is multi-line Cross Apply yExecNLog.SqlCodeLinesInResultSet(s.err) as x Where s.err IS NOT NULL OR SQL IS NULL -- a return code was specified for a message given by YourSqlDba, it can be an error but is handled somewhere else, at end of job union all Select TypSeq=7, typ='YSDNo', lineOrd=1, line=YourSqlDbaNo Where YourSqlDbaNo is not null ) as linearView ) as ToLog -- if a real @sql command has to be run, try run it otherwise exit the proc -- Select * from #trc -- memorize into Maint.JobHistoryLineDetails, details of the job Insert into Maint.JobHistoryLineDetails (jobNo, Seq, TypSeq, Typ, Line, Txt) Select * from #trcLogAndOrExec -- always update event for log message only Update JH Set JobEnd = Getdate() From #trcLogAndOrExec as t Join Maint.JobHistory as JH ON JH.JobNo = t.JobNo -- the same #trcLogAndOrExec is going to be reused for logging run-time execution error, if there are any Truncate table #trcLogAndOrExec If isnull(@sql, '') = '' Return -- nothing left to do -- execute query and ensure its logging in trace, because of the previous return statement -- there is some SQL to run Exec yExecNLog.ExecWithProfilerTrace @sql, @maxSeverity output, @Msgs Output Select @errorN = IIF(@maxSeverity>10,1,0) -- always update in case of DBCC messages with errors which can't be trapped error -- always update event for log message only -- Since here #trc is empty switch back to dbo.MainContextInfo(null) method to get jobNo Update JH Set JobEnd = Getdate() From dbo.MainContextInfo(NULL) as M Join Maint.JobHistory as JH ON JH.JobNo = M.JobNo -- insert report execution status... -- Logs messages, being informational (severity <=10) or not. -- As examples : prints, or dbcc checkdb output without error are informational messages. -- The last line of the report on a global execution status Insert into #trcLogAndOrExec Select I.JobNo , I.Seq , ToLog.TypSeq , ToLog.typ , Line=ToLog.LineOrd , ToLog.Txt From (Select * From #prmLogAndOrExec as I) as I CROSS APPLY -- log informational messages from SQL severity <=10 ( -- @maxSeverity is a value returned by ExecWithProfilerTrace. Smaller than 10, this is a print. -- higher than this, this let knows that there is an error among the messages -- of different severity, which means some of them has a severity higher than 10. -- This must be reported. Typical cases : DBCC check*, stored procedure multiple messages/errors Select * From ( -- Switch to 'Msgs' line type in log, if @maxSeverity stayed <= 10 Select I.JobNo, I.Seq, TypSeq=8, typ='msgs', lineOrd=X.i, X.txt From ( -- log messages under typ 'msgs', -- but messages lines are generated in reversed order by the clr proc that traps them -- so here is the need to reorder them in reverse order by generating a reverse order using row_number. Select S.txt, I=Row_number() Over (Order by S.i desc) -- get new reversed sequence From yExecNLog.SqlCodeLinesInResultSet(@msgs) as S ) as X Where isnull(@msgs,'') <> '' And @maxSeverity <= 10 -- log messages under 'typ'=err when severity is higher than 10 UNION ALL Select I.JobNo, I.Seq, TypSeq=9, typ='errR', lineOrd=X.i, line=X.txt From yExecNLog.SqlCodeLinesInResultSet(@msgs) as x Where isnull(@msgs,'') <> '' And @maxSeverity > 10 -- Add an extra row status to query output, success when @maxSeverity <=10 otherwis 'fail.. ' -- handle here the case of multiple messages of DBCC UNION ALL Select I.JobNo, I.Seq, TypSeq=10, typ='Status', lineOrd=1, line=Status From (Select Status=IIF(@maxSeverity <= 10, 'Success', 'Fail maintenance context')) as Status Where @maxSeverity <= 10 ) as Ok ) as ToLog -- copy #trc if it has run-time execution error logged into it into Maint.JobHistoryLineDetails Insert into Maint.JobHistoryLineDetails (jobNo, Seq, TypSeq, Typ, Line, Txt) Select * from #trcLogAndOrExec -- Update Maint.JobHistoryDetails for query duration -- otherwise secs remains null as it was when the row was created Update Maint.JobHistoryDetails Set secs = Datediff(ss, cmdStartTime, getdate()) , forDiagOnly = Case When @maxSeverity > 10 Then 0 Else @forDiagOnly End From #prmLogAndOrExec as P JOIN Maint.JobHistoryDetails JD ON JD.JobNo = P.JobNo And JD.seq = P.Seq -- @raiserror = 1 is never used in maintenance related tasks -- For example, it is used in ExportDatabase, which is a large script -- that we want to stop when an error is found. If @maxSeverity > 10 And @raiseError = 1 Raiserror ('Stop on error by request of calling procedure for : %s : %s ',11,1, @YourSqlDbaNo, @context) End TRY Begin CATCH -- Errors caught here are very rare, cause yExecNLog.ExecWithProfilerTrace catch then -- for all dynamic queries -- they must be related with some runtime event in this proc as an duplicate on insert to the log -- or string truncation on insert to log or deadlock on updates to Maint.JobHistoryDetails and -- Maint.JobHistory which are not very likely since they are properly indexed and done by JobNo Set @errorN = 1; -- describe the error to write it in YourSqlDba Job log -- we are here in the context of an error that is not related to the dynamic SQL execution -- but to the logging mecanism itself (this procedure) that is supposed to log the error, -- which is a problem because it means that some error information may not be logged -- but at least we will have some information about it in the log -- which is better than nothing and can help to fix the problem declare @msg nvarchar(4000) Select @Msg=E.Err From (Select Err=ErrMsg From S#.FormatCurrentMsg(null)) as E -- write it into Maint.JobHistoryLineDetails Insert Into Maint.JobHistoryLineDetails (jobNo, Seq, TypSeq, Typ, Line, Txt) Select I.JobNo, I.Seq, TypSeq=99, typ='ErrLog', lineOrd=1, line=@Msg From #prmLogAndOrExec as I -- always update event for log message only Update JH Set JobEnd = Getdate() From dbo.MainContextInfo(NULL) as M Join Maint.JobHistory as JH ON JH.JobNo = M.JobNo; End CATCH End -- yExecNLog.LogAndOrExec GO -- ---------------------------------------------------------------------------- -- This stored procedure serves as a wrapper for yExecNLog.LogAndOrExec. -- It is primarily designed to log diagnostic messages to the Maint.JobHistory table, -- offering valuable insights for debugging within the YourSqlDba system. -- -- Notably, this procedure is fail-safe for the YourSqlDba developer, -- designed to assist in diagnosis without the risk of raising errors. -- Therefore, integrating and executing it within production code is considered safe. -- ----------------------------------------------------------------------------- Create Or Alter Proc dbo.LogForDebugging @DebugMessage Nvarchar(max) as Begin Declare @DebugLine Nvarchar(max) = ':Debug:'+ISNULL(@DebugMessage, ' well... the debug message was found null (concatenation of text with null value? )') ; Exec yExecNLog.LogAndOrExec @Info=@DebugLine End GO -- ------------------------------------------------------------------------------------------ -- Run commands stored in scriptTable (segregated by connection) -- but use the logging mecanism of YourSqlDba -- By calling yExecNLog.LogAndOrExec instead. -- Allow multi-statement generation and processing by inserting multiple statement into view S#.ScriptToRun -- This proc read and execute each generated statements separately. -- ------------------------------------------------------------------------------------------ -- @@MARK: TODO : In-progress Dynamic SQL for YourSqlDba through a SQL Batch - to use and test Create Or Alter Proc S#.RunScriptAndLog @RunOnThisDb sysname = NULL -- remote database execution if database is specified as Begin Set Nocount on; Declare @Sql Nvarchar(max) = '' Declare @Label Nvarchar(max) Declare @seq Int Declare @db Sysname Declare @Context Nvarchar(max) Declare @Info Nvarchar(max) Declare @RaiseErrorFlag Int Declare @forDiagOnly Int Declare @errorN Int --Drop table if exists #Tmp Drop table if exists #tmp Select top 0 * into #tmp From S#.GetErrorMessagesAndInfo () as Err -- Create temporary table to hold current query to be processed Drop table if exists #Sql Select Top 0 Seq=@Seq, Sql=@sql, Db=@Db, Label=@label, Context=@Context, info=@info, raiseErrorFlag=@raiseErrorFlag, forDiagOnly=@forDiagOnly Into #Sql Select @seq = Min(Seq)-1 From S#.ScriptToRun -- this view is filtered by current @@spid Where nestLevel = @@NESTLEVEL While (1=1) Begin Insert into #Sql Select Top 1 seq, sql, db, label, R.Context, R.info, R.raiseErrorFlag, R.forDiagOnly From S#.ScriptToRun AS R -- this view is filtered by current @@spid Where nestLevel = @@NESTLEVEL And seq > @Seq Order by Seq If @@rowcount = 0 break Select @seq = Seq, @Sql=Sql, @Db=Db, @Label=Label, @Context=@Context, @Sql=@Sql, @Info=@Info, @RaiseErrorFlag=@RaiseErrorFlag, @forDiagOnly=@forDiagOnly From #Sql Truncate Table #Sql Declare @nbRangees Int Declare @StatsInfo nvarchar(max) -- Let yExecNLog.LogAndOrExec execute and log stuff the YourSqlDba way -- either directly from YourSqlDba database context If Coalesce(@Db,@RunOnThisDb) IS NULL -- directly from YourSqlDba database context Exec yExecNLog.LogAndOrExec @Context=@Context, @Sql=@Sql, @Info=@Info, @RaiseError=@RaiseErrorFlag, @forDiagOnly=@forDiagOnly, @ErrorN = @ErrorN Output Else -- or perform a database context switch before executing dynamic SQL Begin Declare @IndirectContextSwitch as nvarchar(max) Set @IndirectContextSwitch = N'Use ['+Coalesce(@Db,@RunOnThisDb)+'];'+ N'Exec yExecNLog.LogAndOrExec @Context=@Context, @Sql=@Sql, @Info=@Info, @RaiseError=@RaiseErrorFlag, @forDiagOnly=@forDiagOnly, @ErrorN = @ErrorN Output' -- ***** extraire param pour yExecNLog.LogAndOrExec et l'appeler d'ici Exec sp_executeSql @IndirectContextSwitch , N'@Context Nvarchar(max), @Sql nvarchar(max) ,@Info Nvarchar(max),@RaiseErrorFlag Int, @forDiagOnly Int, @ErrorN Int Output' , @Context, @Sql, @Info, @RaiseErrorFlag, @forDiagOnly, @ErrorN Output End End -- While /*===KeyWords=== Scripting,Logging ===KeyWords===*/ End GO -- @@MARK: Dynamic creation of header info table for RESTORE HEADERONLY with extra columns for each version Create Or Alter Procedure yMaint.CollectBackupHeaderInfoFromBackupFile @bkpFile nvarchar(512) as Begin Declare @sql nvarchar(max) Create Table #Header ( BackupName nvarchar(128), BackupDescription nvarchar(255), BackupType smallint, ExpirationDate datetime, Compressed tinyint, Position smallint, DeviceType tinyint, UserName nvarchar(128), ServerName nvarchar(128), DatabaseName nvarchar(128), DatabaseVersion int, DatabaseCreationDate datetime, BackupSize numeric(20,0), FirstLSN numeric(25,0), LastLSN numeric(25,0), CheckpointLSN numeric(25,0), DatabaseBackupLSN numeric(25,0), BackupStartDate datetime, BackupFinishDate datetime, SortOrder smallint, CodePage smallint, UnicodeLocaleId int, UnicodeComparisonStyle int, CompatibilityLevel tinyint, SoftwareVendorId int, SoftwareVersionMajor int, SoftwareVersionMinor int, SoftwareVersionBuild int, MachineName nvarchar(128), Flags int, BindingID uniqueidentifier, RecoveryForkID uniqueidentifier, Collation nvarchar(128), FamilyGUID uniqueidentifier, HasBulkLoggedData bit, IsSnapshot bit, IsReadOnly bit, IsSingleUser bit, HasBackupChecksums bit, IsDamaged bit, BeginsLogChain bit, HasIncompleteMetaData bit, IsForceOffline bit, IsCopyOnly bit, FirstRecoveryForkID uniqueidentifier, ForkPointLSN numeric(25,0), RecoveryModel nvarchar(60), DifferentialBaseLSN numeric(25,0), DifferentialBaseGUID uniqueidentifier, BackupTypeDescription nvarchar(60), BackupSetGUID uniqueidentifier ) -- adjust table column depending on version -- sql2008 need CompressedBackupSize column If yInstall.SqlVersionNumber () >= 100 -- sql 2008 and above Alter table #Header Add CompressedBackupSize BigInt -- sql2012 need containement column If yInstall.SqlVersionNumber () >= 110 -- sql 2012 and above Alter table #Header Add containment tinyint -- sql2014 need theses If yInstall.SqlVersionNumber () >= 120 -- sql 2014 and above Begin Alter table #Header Add KeyAlgorithm nvarchar(32) Alter table #Header Add EncryptorThumbprint varbinary(20) Alter table #Header Add EncryptorType nvarchar(32) End -- sql2022 need theses If yInstall.SqlVersionNumber () >= 160 -- sql 2022 and above Begin Alter table #Header Add LastValidRestoreTime datetime Alter table #Header Add TimeZone nvarchar(32) Alter table #Header Add CompressionAlgorithm nvarchar(32) End Set @sql = 'Restore HeaderOnly from Disk=""' Set @sql = replace(@sql, '', @bkpFile) Set @Sql = ' insert into #Header exec ("'+replace(@sql, '"', '""')+'") ' Set @Sql = replace (@sql, '"', '''') Set @sql = yExecNLog.Unindent_TSQL(@sql) Declare @maxSeverity int Declare @msgs Nvarchar(max) Exec yExecNLog.ExecWithProfilerTrace @sql, @MaxSeverity output, @Msgs output Delete From Maint.TemporaryBackupHeaderInfo Where spid = @@spid If @maxSeverity > 10 Begin Raiserror (N'CollectBackupHeaderInfoFromBackupFile error %s: %s %s', 11, 1, @@SERVERNAME, @Sql, @Msgs) Return (1) End Insert into Maint.TemporaryBackupHeaderInfo (BackupType, position, deviceType, DatabaseName, lastLsn) Select BackupType, position, deviceType, DatabaseName, lastLsn From #Header Return(0) End Go -- @@MARK: Dynamic creation of flie list table for RESTORE FileListOnly with extra columns for each version Create Or Alter Procedure yMaint.CollectBackupFileListFromBackupFile @bkpFile nvarchar(512) as Begin Declare @sql nvarchar(max) create table #Files -- Database file list obtained from restore filelistonly ( LogicalName nvarchar(128) -- Logical name of the file. ,PhysicalName nvarchar(260) -- Physical or operating-system name of the file. ,Type NCHAR(1) -- The type of file, one of: L = Microsoft SQL Server log file D = SQL Server data file F = Full Text Catalog ,FileGroupName nvarchar(128) -- Name of the filegroup that contains the file. ,Size numeric(20,0) -- Current size in bytes. ,MaxSize numeric(20,0) -- Maximum allowed size in bytes. ,FileID bigint -- File identifier, unique within the database. ,CreateLSN numeric(25,0) -- Log sequence number at which the file was created. ,DropLSN numeric(25,0) NULL -- The log sequence number at which the file was dropped. -- If the file has not been dropped, this value is NULL. ,UniqueID uniqueidentifier -- Globally unique identifier of the file. ,ReadOnlyLSN numeric(25,0) NULL -- Log sequence number at which the filegroup containing the file changed -- from read-write to read-only (the most recent change). ,ReadWriteLSN numeric(25,0) NULL -- Log sequence number at which the filegroup containing the file changed -- from read-only to read-write (the most recent change). ,BackupSizeInBytes bigint -- Size of the backup for this file in bytes. ,SourceBlockSize int -- Block size of the physical device containing the file in bytes (not the backup device). ,FileGroupID int -- ID of the filegroup. ,LogGroupGUID uniqueidentifier NULL -- NULL. ,DifferentialBaseLSN numeric(25,0) NULL -- For differential backups, changes with log sequence numbers greater than or equal -- to DifferentialBaseLSN are included in the differential. ,DifferentialBaseGUID uniqueidentifier -- For differential backups, the unique identifier of the differential base. ,IsReadOnly bit -- 1 = The file is read-only. ,IsPresent bit -- 1 = The file is present in the backup. ) -- sql2008 need TDEThumbprint column If yInstall.SqlVersionNumber () >= 100 -- sql 2008 and above Alter table #Files Add TDEThumbprint varbinary(32) If yInstall.SqlVersionNumber () >= 130 -- Sql2016 and above Alter Table #Files Add SnapshotURL Nvarchar(36) Set @sql = 'Restore filelistonly from Disk=""' Set @sql = replace(@sql, '', @bkpFile ) Set @Sql = ' insert into #Files exec ("'+replace(@sql, '"', '""')+'") ' Set @Sql = replace (@sql, '"', '''') Set @sql = yExecNLog.Unindent_TSQL(@sql) Declare @maxSeverity int Declare @msgs Nvarchar(max) Exec yExecNLog.ExecWithProfilerTrace @sql, @MaxSeverity output, @Msgs output Delete From Maint.TemporaryBackupFileListInfo Where spid = @@spid If @maxSeverity > 10 Begin Raiserror (N'CollectBackupFileListFromBackupFile error %s: %s %s', 11, 1, @@SERVERNAME, @Sql, @Msgs) Return (1) End Insert into Maint.TemporaryBackupFileListInfo (FileId, Type, LogicalName, physicalName) Select FileId, Type, LogicalName, physicalName From #Files Return (0) End Go Create Or Alter Proc yMaint.SaveXpCmdShellStateAndAllowItTemporary as Begin If Exists(Select * from Maint.XpCmdShellSavedState) Begin Exec ( ' With XpCmdShellState as ( Select convert(int,value_In_Use) as Value_in_use from Sys.configurations Where name = ''xp_cmdshell'' ) Update Maint.XpCmdShellSavedState Set value_In_Use = S.Value_in_use From XpCmdShellState S ' ) End Else Exec ( ' With XpCmdShellState as ( Select convert(int,value_In_Use) as Value_in_use from Sys.configurations Where name = ''xp_cmdshell'' ) Insert Into Maint.XpCmdShellSavedState (value_In_Use) Select * from XpCmdShellState ' ) EXEC sp_configure 'xp_cmdshell', 1 Reconfigure End GO Create Or Alter Proc yMaint.RestoreXpCmdShellState as Begin If OBJECT_ID('Maint.XpCmdShellSavedState') IS Not NULL Begin Exec ( ' Declare @state int Select @state=convert(int, value_In_Use) From Maint.XpCmdShellSavedState EXEC sp_configure ''xp_cmdshell'', @state Reconfigure Delete Maint.XpCmdShellSavedState ' ) End End GO -- @@MARK: Maintenance - PutDbOffline Create Or Alter Proc yMaint.PutDbOffline @DbToLockOut nvarchar(128) = '' as Begin Declare @AlterDb nvarchar(512) Declare @Info nvarchar(512) If DatabasepropertyEx(@DbToLockOut, 'Status') <> 'EMERGENCY' And @DbToLockOut Not In ('master', 'model', 'msdb') Begin If DatabasepropertyEx(@DbToLockOut, 'Status') <> N'ONLINE' Return -- version 1.1 don't attempt to put offline a database that is already not online Set @AlterDb = ' Alter database [] Set offline With ROLLBACK immediate ' Set @Info = 'Database []is put offline because the previous error' Set @AlterDb = Replace(@AlterDb, '', @DbToLockOut) Set @Info = Replace(@Info, '', @DbToLockOut) Set @AlterDb = Replace(@AlterDb, '"', '''') Begin try Exec (@alterDb) Exec yExecNLog.LogAndOrExec @context = @Info , @YourSqlDbaNo = '005' End try begin catch Exec yExecNLog.LogAndOrExec @context = 'yMaint.PutDbOffline error' , @err='?' , @YourSqlDbaNo = '005' end catch End End -- yMaint.PutDbOffline GO -- --------------------------------------------------------------------------------------- -- This procedures extract informations required for database mail diagnosis -- --------------------------------------------------------------------------------------- -- @@MARK: Maintenance - Mail diag Create Or Alter Procedure Maint.DiagDbMail as Begin -- Lire les éléments envoyés -- Voir la queue de message SQL EXEC msdb.dbo.sysmail_help_queue_sp @queue_type = 'mail' ; SELECT top 5 S.send_request_date, S.mailItem_id, S.sent_status, S.recipients, s.subject FROM msdb.dbo.sysmail_sentitems S order by S.sent_date desc, S.mailItem_id desc; SELECT top 100 * FROM msdb.dbo.sysmail_event_log order by log_id desc; End -- Maint.DiagDbMail GO --select * --from --yUtl.YourSQLDba_ApplyFilterDb ( --' --', --' --F%' --) ---------------------------------------------------------------------------------------- ------------------------------------------------------------------------------------- -- This highly flexible procedure allows rows comparison from many different sources. -- It could be a table (with an optional filter), or a query. It can reside into the -- same database, or onto different database, on the same server or on -- different servers, provide that the necessary linked server are defined. -- Both source and target are independently configurable -- The only condition is that they returns the same columns. -- Generated query is printed for debugging purposes. -- Column list is mandatoty, and at least one row source for the source and the target. --------------------------------------------------------------------------------------- -- @@MARK: TODO : Check Use Create Or Alter Proc Tools.CompareRows @ColList nvarchar(max) -- list of columns to compare (must include primary key) , @Srctab as sysname = '' -- schema mandatory, could be substituted by @SrcQry , @SrcQry as nvarchar(max) = '' -- if specified override @SrcTab, must include db.schema , @TgtTab as sysname = '' -- schema mandatory, could be substituted by @TgtQry , @TgtQry as nvarchar(max) = '' -- if specified override @TgtTab, must include db.schema , @SrcWhereClause nvarchar(max) = '' -- optional where clause for @srcTab , @TgtWhereClause nvarchar(max) = '' -- optional where clause for @TgtTab , @SrcDB as sysname = '' -- source db (default to current one) , @TgtDB as Sysname = '' -- target db (default to source) , @SrcInstance as sysname = '' -- linked source server, default local , @TgtInstance as sysname = '' -- linked target server, default local as Begin declare @sql nvarchar(max) set @sql = ' With SrcRows as ( ) , tgtRows As ( ) , UnionOfDataSetsToCompare as ( Select "(source) [].[]." as DataSetId , From SrcRows UNION ALL Select "(target) [].[]." as DataSetId , From TgtRows ) select MAX(DataSetId) as DataSetid , from UnionOfDataSetsToCompare group by Having MAX(DataSetId) = MIN(DataSetId) order by ' -- assume some behavior for missing parameters If @SrcDB = '' Set @SrcDB = DB_NAME() -- current db if no @SrcDb If @TgtDB = '' Set @TgtDB = @SrcDB -- Same db if no @tgtDg If @TgtTab = '' Set @TgtTab = @Srctab -- Same table name if no @TgtTab If @TgtWhereClause = '' Set @SrcWhereClause = @TgtWhereClause If @SrcQry <> '' Set @sql = REPLACE(@sql, '', @SrcQry) If @TgtQry = '' And @TgtTab = '' Set @TgtQry = @SrcQry If @TgtQry <> '' Set @sql = REPLACE(@sql, '', @TgtQry) If @Srctab = '' Set @sql = REPLACE(@sql, '(source) [].[].' , '@SrcQry') If @Tgttab = '' Set @sql = REPLACE(@sql, '(target) [].[].' , '@TgtQry') If @Srctab = '' And @SrcQry = '' Begin Print 'Provide either @srcTab or @srcQry parameter' Return End Set @sql = REPLACE(@sql, '', 'Select from [].[]. ') Set @sql = REPLACE(@sql, '', 'Select from [].[]. ') -- replace tags Set @sql = REPLACE (@sql, '', @ColList) Set @sql = REPLACE (@sql, '', @Srctab) Set @sql = REPLACE (@sql, '', @Tgttab) Set @sql = REPLACE (@sql, '', @SrcWhereClause) Set @sql = REPLACE (@sql, '',@TgtWhereClause) Set @sql = REPLACE (@sql, '', @SrcDB ) Set @sql = REPLACE (@sql, '', @TgtDB ) -- remove the linked server syntax part if not specified If @SrcInstance = '' Set @sql = REPLACE (@sql, '[].', '') Set @sql = REPLACE (@sql, '', @SrcInstance ) If @TgtInstance = '' Set @sql = REPLACE (@sql, '[].', '') Set @sql = REPLACE (@sql, '', @TgtInstance ) -- replace double quotes by real single quotes Set @sql = REPLACE (@sql, '"', '''') Exec yExecNLog.PrintSqlCode @sql, @numbering=1 -- show the query for debugging purpose exec (@sql) -- execute it End --exec Tools.CompareRows -- @ColList = 'ContactID, NameStyle, Title, FirstName, MiddleName, LastName, Suffix -- , EmailAddress, EmailPromotion, Phone, PasswordHash, PasswordSalt, rowguid -- , ModifiedDate' --, @Srctab = 'Person.Contact' --, @SrcDb = 'AdventureWorks' --, @TgtDb = 'AdventureWorksCopy' --exec Tools.CompareRows -- @ColList = 'ContactID, NameStyle, Title, FirstName, MiddleName, LastName, Suffix -- , EmailAddress, EmailPromotion, Phone, PasswordHash, PasswordSalt, rowguid -- , ModifiedDate' --, @SrcQry = 'Select * from AdventureWorks.Person.Contact where phone like "440%"' --, @TgtQry = 'Select * from AdventureWorksCopy.Person.Contact where phone like "440%"' --, @SrcInstance = 'ASQL9' --go ---------------------------------------------------------------------------------------- go -- @@MARK: Reporting: HitoryView Create OR Alter Function maint.HistoryView (@StartDateTime Nvarchar(23), @EndDatetime Nvarchar(23), @FilterOption Int) -- ---------------------------------------------------------------------------------------------------------- -- Function to list filter history in a more readeable form, based on a datetime range. -- -- This function performs three type of reporting. -- 1) List all jobs in a time range, with all events that occured in that time range -- 2) List only job entries that belongs a the jobNo of an execution context (available only when called from a job) -- 3) List only job entries that have an error in the time range -- -- The function is intended to be used by YourSqlDba reporting, but can be used by any user with the proper rights. -- YourSqlDba reporting use parameter HV$ShowOnlyErrorOfJobFromSessionContext to get only the errors of the current job -- so yMaint.InstructionsToGetJobHistory uses it to find if an error occured in the job and produces a report -- that is different depening the occurence of an error or not. -- -- When listing jobs in a time range, we may encounter multiple concurrent jobs in the same time range (ex: main mantenance and log backups) -- and we want to show errors for them to. -- However it is visually hard to notice the jobNo change, so columns are added to display the calling task, query, and job name -- and they are displayed only when the jobNo changes. -- -- Since version 7.0, error message for improper dates are added. -- Language setting may affect datetime parameter interpretation, so we use string parameters. -- The official date format supported is 'YYYY-MM-DD hh:mm:dd.mmm' (format 121 of the convert function) -- It can be less precise, but parts must match for those that are present. -- For example: French language setting (either explicitely, or at the login or in conenction parameters) -- make month and day switch places. Not only it may give another date, but even an invalid one. -- So parameters are now of nvarchar type and an explicit TRY_CONVERT to datetime conversion from date format 121 is attempted. -- So the the date will be properly interpreted as year, month, day -- Special handling of invalid date give a singles row output in which the "Line" hold a meaningful error message -- -- ---------------------------------------------------------------------------------------------------------- Returns Table as Return ( Select RegularFunctionOutput.* -- reduced set of cols for normal use --FullSetOfColsForDebug.* -- all intermediary values needed to what leads to final values and final values From ( Select PP.ChkStartDateTime, PP.ChkEndDateTime, PP.FilterOption, PP.TxtErrForInvalidDatePrm , LDet.JobNo, LDet.cmdStartTime, LDet.Secs, LDet.Seq, LDet.Typ, LDet.TypSeq, LDet.Line, TxtM.Txt -- For a given action we may find details of types info, Ctx, Sql, msgs, err% -- and when there is SQL, a Status typ is there (from which isSuccess is derived) -- but YourSqlDba sometimes also just create entries for a given seq, without SQL -- that are informative or operative messages -- (for example a Database that must be found in full recovery mode, but isn't) -- so we can have Errfound to 1 without SQL, even if it comes from a call to -- an internal SQL stored procedure. The SQL called isn't displayed but -- errY type is displayed with error details message/Sql module/error line , isSql, SqlFound=Max(isSql) Over (Partition By JobNo, Seq) , isSuccess, SuccessFound=Max(isSuccess) Over (Partition By JobNo, Seq) , isErr, ErrFound=Max(isErr) Over (Partition By JobNo, Seq) , Ji.MaintJobName, Ji.MainSqlCmd, Ji.Who, Ji.Host, Ji.Prog, Ji.SqlAgentJobName, ji.JobId, ji.StepId, ji.JobStart, ji.JobEnd, ji.JsonPrms , JobNoFromExecutionContext From ( --processedParam Select NonNullStartDateTime , NonNullEndDateTime , ChkStartDateTime , ChkEndDateTime , FilterOption , TxtErrForInvalidDatePrm FROM -- assume default value in replacement of NULL param for startDateTime and EndDateTime -- if date parameters are NULL, they are going to be made valid, otherwise TRY_Convert is going to return NULL (select NonNullStartDateTime=ISNULL(@StartDateTime, '1900-01-01 00:00:00.000')) as NonNullStartDateTime CROSS APPLY (Select NonNullEndDateTime=ISNULL(@EndDateTime, '9999-12-31 23:59:59.997')) as NonNullEndDateTime -- assume default value in replacement of NULL param for FilterOption CROSS JOIN Maint.MaintenanceEnums as E CROSS APPLY (Select FilterOption=ISNULL(@FilterOption, E.HV$ShowAll)) as FilterOption -- check if date parameters are valid CROSS APPLY (Select ChkStartDateTime=TRY_CONVERT(Datetime, NonNullStartDateTime,121)) as StartDateTime CROSS APPLY (Select ChkEndDateTime=TRY_CONVERT(Datetime, NonNullEndDateTime,121)) as EndDateTime -- in either one of the parameter format do not respect expected format, craft an error message otherwise it is left null OUTER APPLY ( Select TxtErrForInvalidDatePrm='Date format must by yyyy-mm-dd hh:mm:ss.nnn' Where ChkStartDateTime IS NULL Or ChkEndDateTime IS NULL ) as TxtErrForInvalidDatePrmShort ) as PP -- pre-processed params -- This column (JobNoFromExecutionContext) allows Maint.historyView to be used by YourSqlDba reporting -- to find out if error where loggued for the currently executing job -- Finding the current jobNo only by a time range is unreliable, because more than one job -- can write entries into the same time range i.e. between startDateTiem and endDateTime -- JobNoFromExecutionContext is only valid from withing calling YourSqlDba procedures that setup -- a session context. -- Dbo.MainContextInfo with NULL param returns info from the current executing job. -- JobNoFromExecutionContext is NULL if the function is called from outside YourSqlDba procedures -- or if the function is called from a YourSqlDba procedure that does not setup a session context -- -- JobNoFromExecutionContext is intended to be used to find out presence of errors only by InstructionsToGetJobHistory -- which is called itself through yMaint.SendExecReports and Maint.YourSqlDba_DoMaint if you go up into the call stack -- -- This jobNo and is valid only for to be used when FilterOption = E.HV$ShowOnlyErrorOfJobFromSessionContext -- as (FilterOption = E.HV$ShowOnlyErrorOfJobFromSessionContext And JobNo = JobNoFromExecutionContext) OUTER APPLY (Select JobNoFromExecutionContext=JobNo From Dbo.MainContextInfo(NULL)) as JobNoFromExecutionContext -- This OUTER APPLY return NULLS columns if an error message about date format is there -- In that case we will put the error message in Line column -- This is also a mean to stop processing when parameters are wrong -- JobNoBefore is used to detect when a jobNo changes in the output (to display job predigree) OUTER APPLY ( Select D.JobNo , D.cmdStartTime , D.Secs , D.Seq , LD.Typ , LD.TypSeq , LD.Line , LD.Txt , JobNoBefore=LAG(D.JobNo, 1, -1) Over (Order By D.cmdStartTime, LD.Seq, LD.TypSeq, LD.Typ, LD.Line) From ( -- If ChkStartDateTime Or ChkEndDateTime are NULL, because date is invalid, the optimizer do not query rows -- because by default expression default to false. Select * From Maint.JobHistoryDetails With (index (iCmdStartTime)) Where cmdStartTime Between PP.ChkStartDateTime And PP.ChkEndDateTime ) As D -- if date format is ok LEFT LOOP JOIN Maint.JobHistoryLineDetails as LD -- match details with time range ON LD.JobNo=D.JobNo And Ld.seq = D.Seq ) as LDet -- global success it a never a false positive, because query is executed by logAndExec and completed successfully -- however some SQL may fail so severely that the connection get away and while the query is not logged -- no return is. So status can be missing CROSS APPLY (Select isSuccess=IIF(typ = 'STATUS' And Txt Like 'Success%', 1, 0)) as isSuccess -- Txt value remains the same except when nothing processed here when PP.TxtErrForInvalidDatePrm is NOT NULL CROSS APPLY (Select Txt=IIF(PP.TxtErrForInvalidDatePrm IS NULL, LDet.Txt, PP.TxtErrForInvalidDatePrm)) as TxtM -- need to know if some SQL was there CROSS APPLY (Select isSql=IIF(typ = 'Sql', 1, 0)) as isSql -- Some actions record an "err" status, in this case, the error is certain, as the SQL is executed by logAndExec -- Some other actions record an "err%" status that comes from YourSqlDba but not for a specific SQL execution -- They differ by characters following Err in the status. CROSS APPLY (Select isErr=IIF(typ Like 'Err%', 1, 0) ) as isErr -- YourSqlDba operational error condition -- This OUTER APPLY could be potentially expensive to run on large output result sets -- but the optimizer does the magic or removing it of the plan, if none of its columns are returned -- since an outer apply that finds nothing does not affect the remaining query results -- I also limits the number of calls as it is useful to set columns only when the job changes, or the first job start OUTER APPLY ( Select MaintJobName, MainSqlCmd, Who, Host, Prog, SqlAgentJobName, JobId, StepId, JobStart, JobEnd, JsonPrms From Dbo.MainContextInfo(LDet.JobNo) as J CROSS JOIN Maint.MaintenanceEnums as E -- set of constants for the where below -- limit the number of calls as it isn't necesseray to repeat this info at every line -- do it just when the job changes Where (LDet.JobNo <> LDet.JobNoBefore) ) as JI -- jobInfo ) as JA -- JobActionSummaryInfo -- -- setup of two potentiel different output with two cross apply. -- one for regular use, and one for debug purposes -- proper output is selected with proper CROSS APPLY alias in the main query -- CROSS APPLY -- Column selection for production output ( Select JA.JobNo, JA.cmdStartTime, JA.Secs, JA.Seq, JA.Typ, JA.TypSeq, JA.Line, JA.Txt , JA.MaintJobName, JA.MainSqlCmd, JA.Who, JA.Host, JA.Prog, JA.SqlAgentJobName, JA.JobId, JA.StepId, JA.JobStart, JA.JobEnd, JA.JsonPrms ) as RegularFunctionOutput CROSS APPLY -- Column selection for debug display purposes ( Select ChkStartDateTime, ChkEndDateTime, FilterOption, TxtErrForInvalidDatePrm , JA.JobNo, JA.cmdStartTime, JA.Secs, JA.Seq, JA.Typ, JA.TypSeq, JA.Line, JA.Txt -- For a given action we may find details of types info, Ctx, Sql, msgs, err% -- and when there is SQL, a Status typ is there (from which isSuccess is derived) -- but YourSqlDba sometimes also just create entries for a given seq, without SQL -- that are informative or operative messages -- (for example a Database that must be found in full recovery mode, but isn't) -- so we can have Errfound to 1 without SQL, even if it comes from a call to -- an internal SQL stored procedure. The SQL called isn't displayed but -- errY type is displayed with error details message/Sql module/error line , isSql, SqlFound, isSuccess, SuccessFound, isErr, ErrFound , JA.MaintJobName, JA.Who, JA.Host, JA.Prog, JA.SqlAgentJobName, JA.JobId, JA.StepId, JA.JobStart, JA.JobEnd, JA.JsonPrms , JobNoFromExecutionContext ) as FullSetOfColsForDebug -- use cross apply as a mean to cancel output of single row with empty data, if condition is false, it cancel output -- why can we have a row with NULL? It is allowed to display an error, but if there is no error and no data, -- (because getting detail is done through a outer apply that could find nothing) -- we do not want to display a row with NULL data CROSS APPLY (Select noRow=1 Where Ja.Txt is Not NULL) as NoRow CROSS JOIN Maint.MaintenanceEnums as E -- set of constants for the where below -- Decides what to output from lineDetails Where -- show all in the time range JA.FilterOption = E.HV$ShowAll -- show only errors in the time range OR ( ( (JA.SuccessFound=0 And JA.SqlFound=1) -- No Success reported for query occuring on very severe untrapped fail (SqlDump and query killed) Or (JA.ErrFound=1) -- Operational issue error explicitely flagged by YourSqlDba (i.e. check on database recovery to full) ) And ( -- 1st situation : Exercice error detection only on job from Execution context. -- E.HV$ShowOnlyErrorOfJobFromSessionContext is only valid when maintenance execution context -- is set on the current session id. (JA.FilterOption = E.HV$ShowOnlyErrorOfJobFromSessionContext And JA.JobNo = JA.JobNoFromExecutionContext) -- 2nd : Display ALL errors in the time range, may it belongs to the current job or not. -- Job may overlap in time, but also interesting to report previous errors in all jobs. Or JA.FilterOption = E.HV$ShowErrOnly ) ) -- show error if date format appears invalid (121 format of convert) OR (JA.TxtErrForInvalidDatePrm IS NOT NULL) -- show error about date format ) -- Maint.HistoryView -- keep this comments as this code is an helper template to test the function -- Select H.* -- From -- Maint.MaintenanceEnums as E -- CROSS APPLY (Select Now=GetDate()) as Now -- CROSS APPLY (Select SomeTimeBeforeNow=DateAdd(dd, -2, Now)) as BeforeNow -- CROSS APPLY -- ( -- Values -- --('2021-04-30 13:26:06.630', '2024-04-30 13:26:06.833', E.HV$ShowErrOnly) -- --('2024-04-29 18:39:06.510', '2024-04-29 18:43:33.017', E.HV$ShowAll) -- --('2024-04-29 18:48:10.900', '2024-04-29 18:48:13.623', E.HV$ShowErrOnly) -- --('2024-04-30 10:27:06.763', '2024-04-30 10:30:11.490', E.HV$ShowErrOnly) -- --('2024-05-01 12:10:34.477', '2024-05-01 12:10:41.297', E.HV$ShowAll) -- (E.HV$FromYesterdayMidnight, E.HV$Now, E.HV$ShowAll) -- ) as t(startDate, EndDate, FilterOption) -- CROSS APPLY Maint.HistoryView (startDate, EndDate, FilterOption) as H -- Order by CmdStartTime, seq, typSeq, Line go -- --------------------------------------------------------------------------------------- -- Function that returns a part of the message to instruct the user -- on how to get the job history for a given job number, or for all jobs -- it also returns some columns of data where the tags match columns names -- and are replaced by the data of the matching columns. -- This replacement is done by the caller. -- --------------------------------------------------------------------------------------- -- @@MARK: Reporting : Prep message to send to User Create Or Alter Function yMaint.InstructionsToGetJobHistory ( @StartOfMaint datetime , @JobNo Int ) Returns Table as Return ( Select InstructionsToGetJobHistory.* From ( Select ToPrm.* , StartEnd.* , JobNameSource From (Select JobNo=@JobNo, StartOfMaintPrm= @StartOfMaint, crlf=yUtl.UnicodeCrLf()) as ToPrm -- jobStart and jobEnd are more precise, if a jobNo is specified. CROSS APPLY ( Select * From (Select DummyForOuterJoin=1) as DummyForOuterJoin OUTER APPLY (Select JobStart, JobEnd From Maint.JobHistory Where JobNo=ToPrm.JobNo) as JH CROSS APPLY (Select StartOfMaint=ISNULL(JH.jobStart,ToPrm.StartOfMaintPrm)) As StartOfMaint -- JobEnd may be missing if no job is created. Review code so that it is always created CROSS APPLY (Select EndOfMaint=ISNULL(JH.jobEnd,Getdate())) as EndOfMaint CROSS APPLY (Select StartOfMaintTxt=CONVERT(NVARCHAR,StartOfMaint,121)) StartOfMaintTxt CROSS APPLY (Select EndOfMaintTxt=CONVERT(NVARCHAR,EndOfMaint,121)) EndOfMaintTxt ) as StartEnd CROSS APPLY ( Select JobNameSource=Coalesce(C.SqlAgentJobName, C.MainSqlCmd, 'Manual Maintenance Job') From Dbo.MainContextInfo(ToPrm.Jobno) as C LEFT JOIN(values ('%SaveDbOnNewFileSet%'),('%DeleteOldBackups%'),('%YourSQLDba_DoMaint%') ) as J(CmdLike) On C.MainSqlCmd Like J.CmdLike ) as JobNameSource ) as Prm CROSS APPLY (Select ServerInstance=Convert(Nvarchar(128),SERVERPROPERTY('ServerName'))) As ServerInstance CROSS APPLY (Select YourSqlDbaVersion=VersionNumber From Install.VersionInfo ()) as YourSqlDbaVersion -- JobSuccess is true when HistoryView reports no error in maintenance time interval, when asked to do so (1 as trd param) -- Maint.HistoryView expect datetime parameters formatted as datetime with format 121, to avoid language setting effect on datetime parameter interpretation CROSS APPLY (Select * From Maint.MaintenanceEnums) as Enum CROSS APPLY (Select JobSuccess=IIF(Not Exists(Select * From MAINT.HistoryView(Prm.StartOfMaintTxt, Prm.EndOfMaintTxt, Enum.HV$ShowOnlyErrorOfJobFromSessionContext)),1,0)) as JobSuccess CROSS JOIN (Select shortResultMessTmp='#shortResultMess#') as shortResultMessTmp Cross Apply (Select shortResultMess=IIF(JobSuccess=1, 'Maintenance succeeded', 'Error detected by maintenance process')) as shortResultMess Cross Apply (Select ErrColor=IIF(JobSuccess=1, 'color="Green"', 'color="Red"')) as ErrColor CROSS JOIN WhoCalledWhat as ctx --extract the full predigree of the call, who, which program, and the query at the top of the stack -- have
for Html crlf Cross Apply (Select MainSqlCmdWithBreak=Replace(Ctx.MainSqlCmd, crlf, '
')) as MainSqlCmdWithBreak -- add crlf to
to improve readability of html code, crlf has no effect on rendering of html Cross Apply (Select MainSqlCmdWithBreakAndCrLf=Replace(MainSqlCmdWithBreak, '
', '
'+crlf)) as MainSqlCmdWithBreakAndCrLf Cross Apply (Select mailPriority = IIF(JobSuccess=1, 'Normal', 'High')) as MailPriority Cross Apply (Select reportSource = IIF(ctx.Prog NOT Like 'SqlAgent%', 'Manual Maintenance Job', 'SQL Server Agent Job:')) as reportSource Cross Apply (Select InstructionBlocTag=IIF(JobSuccess=1, '===TemplateJobOk===', '===TemplateJobErr====')) as InstructionBlocTag Cross Apply (Select kw='') as Kw Cross Apply (Select id='') as id Cross Apply (Select cm='') as cm -- get the instruction bloc, and replace constants to display with values Cross Apply (Select HowToShowHistory=c.TxtInCmt From S#.GetCmtBetweenDelim(InstructionBlocTag, 'yMaint.InstructionsToGetJobHistory') as C ) as ActionToTakeIf /*===TemplateJobOk===
To list ALL maintenance commands ran by the maintenance process, execute the following
command in a query window connected to the SQL Server instance that ran the maintenance:
#kw#Select
#id#  cmdStartTime, JobNo, seq, Typ, line, Txt, MaintJobName, MainSqlCmd, Who, Prog, Host, SqlAgentJobName, JobId, JobStart, JobEnd
#kw#From 
#cm#  -- set of constants for the function below (& precomputed date range constants) 
#id#  YourSQLDba.Maint.MaintenanceEnums #kw#as #id#E #cm#-- HV$Now, HV$FromMidnight, HV$FromYesterdayMidnight, HV$Since12Hours, HV$Since10Min, HV$Since1Hour
#kw#  cross apply#id# YourSQLDba.Maint.HistoryView('#StartOfMaintTxt#', '#EndOfMaintTxt#', E.HV$ShowAll) #cm#-- E.HV$ShowErrOnly=1, E.HV$ShowAll=0
#id#Order By #id#cmdStartTime, Seq, TypSeq, Typ, Line
===TemplateJobOk===*/ /*===TemplateJobErr==== To list only the errors, copy & paste the following command in a query window
connected to the SQL Server instance that ran the maintenance.
#kw#Select #id#cmdStartTime, JobNo, seq, Typ, line, Txt, MaintJobName, MainSqlCmd, Who, Prog, Host, SqlAgentJobName, JobId, JobStart, JobEnd  
#kw#From
#cm#  -- set of constants for the function below (& precomputed date range constants) 
#id#  YourSQLDba.Maint.MaintenanceEnums #kw#AS #id#E #cm# -- HV$Now, HV$FromMidnight, HV$FromYesterdayMidnight, HV$Since12Hours, HV$Since10Min, HV$Since1Hour
#kw#  cross apply#id# YourSQLDba.Maint.HistoryView('#StartOfMaintTxt#', '#EndOfMaintTxt#', E.HV$ShowErrOnly) #cm#-- E.HV$ShowErrOnly=1, E.HV$ShowAll=0
#kw#Where #id#JobNo=#JobNo# 
#kw#Order By #id#cmdStartTime, JobNo, Seq, TypSeq, Typ, Line


To bring back quickly any databases online from offline, run this command:
Exec YourSQLDba.Maint.BringBackOnlineAllOfflineDb

===TemplateJobErr====*/ -- From all the above information, make a set of columns to -- be selectable for returning by InstructionsToGetJobHistory.* in the function -- relative column order is important especially between -- shortResultMessTmp, shortResultMess, ErrColor and StartOfMaintTxt, EndOfMaintTxt CROSS APPLY ( Select mailPriority, JobNameSource, reportSource , JobSuccess, shortResultMessTmp, shortResultMess, ErrColor , HowToShowHistory, YourSqlDbaVersion, StartOfMaintTxt, EndOfMaintTxt, ServerInstance , Ctx.Prog, Ctx.Host, MainSqlCmdWithBreakAndCrLf , Subject=ServerInstance+', '+ReportSource+', '+JobNameSource, kw, id, cm ) as InstructionsToGetJobHistory -- code to test /* -- TODO: Must set an execution context over an existing job to test this function Declare @jobNo Int Select Top 1 @jobNo=jobNo From Maint.JobHistory Select * From yMaint.InstructionsToGetJobHistory ( 'someEmail@outlook.com' -- dummy email , 'nomdejobYourSqlDba' -- , Getdate() , @JobNo , Show ) */ ) -- InstructionsToGetJobHistory go -- ------------------------------------------------------------------------------ -- Procedure which send exec report and errors report -- ------------------------------------------------------------------------------ -- @@MARK: Reporting : Mail message to send to User create or alter proc yMaint.SendExecReports @email_Address nvarchar(200) , @MaintJobName nvarchar(200) , @StartOfMaint datetime , @SendOnErrorOnly int as Begin Declare @msgBody nvarchar(max) Declare @Subject nvarchar(512) Declare @jobSuccess Int Declare @mailPriority nvarchar(6) Select @msgBody = msgBody , @Subject = ReportElements.Subject , @MailPriority = ReportElements.mailPriority , @jobSuccess = ReportElements.JobSuccess From dbo.MainContextInfo(null) as Ctx CROSS APPLY ( Select Email_Address=@email_Address , MaintJobName=@MaintJobName , Inf.* , Ctx.JobNo , SendOnErrorOnly=@SendOnErrorOnly From yMaint.InstructionsToGetJobHistory -- format HTML message related to what Maint.HistoryView to run for the situation (Success or error) ( @StartOfMaint , Ctx.JobNo ) as Inf ) as ReportElements CROSS APPLY (Select JsonReportElements=(Select ReportElements.* FOR JSON PATH)) as jsonReportElements CROSS APPLY (Select MsgBody=g.Code From S#.GetTemplateFromCmtAndReplaceTags ('===MsgBody===', NULL, jsonReportElements) as g) as MsgBody /*===MsgBody=== Maintenance report from YourSqlDba #YourSqlDbaVersion#
Server: #ServerInstance#
#reportSource# #JobNameSource#
Start, end: #StartOfMaintTxt#,   #EndOfMaintTxt#
Result: #shortResultMessTmp#
#HowToShowHistory# Command launched from #Host# by #reportSource# #JobNameSource#
#MainSqlCmdWithBreakAndCrLf#
===MsgBody===*/ -- Return without sending a message if the job is success and message must be sent only in case of error If @JobSuccess=1 And @SendOnErrorOnly=1 Return EXEC Msdb.dbo.sp_send_dbmail @profile_name = 'YourSQLDba_EmailProfile' , @recipients = @email_Address , @importance = @mailPriority , @subject = @Subject , @body = @MsgBody , @body_format = 'HTML' --useful to review html Drop table if exists dbo.DumpHtml --Select msgBody=@msgBody into dbo.DumpHtml End -- yMaint.SendExecReports GO -- this procedure allows calling HistoryView from SqlAgent history Create Or Alter Proc Maint.ShowJobErrors @JobNo Int as Begin Select H.cmdStartTime, H.JobNo, H.seq, H.Typ, H.line, H.Txt , H.MaintJobName, H.MainSqlCmd, H.Who, H.Prog, H.Host, H.SqlAgentJobName, H.JobId, H.JobStart, H.JobEnd From (Select JobStart=Convert(Nvarchar(23),JobStart,121) , JobEnd=Convert(Nvarchar(23),JobEnd,121) From Maint.JobHistory Where JobNo=@Jobno ) as Times CROSS APPLY ( Select cmdStartTime, JobNo, seq, Typ, line, Txt, typSeq, MaintJobName, MainSqlCmd, Who, Prog, Host, SqlAgentJobName, JobId, JobStart, JobEnd From YourSQLDba.Maint.HistoryView(JobStart, jobEnd, 1) ) as H Order By H.cmdStartTime, H.Seq, H.TypSeq, H.Typ, H.Line End Go -- ------------------------------------------------------------------------------ -- Procedure that performs the CheckFullRecoveryModel policy. Database not in FULL Recovery -- model will generate an error of the maintenance. It is possible to exclude -- this check for particular databases with the parameter @ExcDbFromPolicy_CheckFullRecoveryModel -- ------------------------------------------------------------------------------ -- @@MARK: Maintenance : Apply Full recovery model policy Create Or Alter Proc yMaint.CheckFullRecoveryModelPolicy as Begin Declare @dblist nvarchar(max) Declare @context nvarchar(max) Declare @DbCount int Declare @IncDb nVARCHAR(max) , @ExcDb nVARCHAR(max) , @ExcDbFromPolicy_CheckFullRecoveryModel nvarchar(max) Select @IncDb=IncDb, @ExcDb=ExcDb, @ExcDbFromPolicy_CheckFullRecoveryModel=ExcDbFromPolicy_CheckFullRecoveryModel From dbo.MainContextInfo(null) Exec yExecNLog.LogAndOrExec @context = 'yMaint.CheckFullRecoveryModelPolicy' , @Info = 'Check Recovery policy' -- Add the exclusions of @ExcDbFromPolicy_CheckFullRecoveryModel to the selection Set @ExcDb = @ExcDb + CHAR(10) + @ExcDbFromPolicy_CheckFullRecoveryModel Set @dblist = '' Select @dblist = @dblist + ',' + x.DbName From sys.databases db join yUtl.YourSQLDba_ApplyFilterDb(@IncDb, @ExcDb) x on db.name = x.DbName collate database_default Where x.FullRecoveryMode <> 1 And db.source_database_id is Null AND x.DbName Not In ('master', 'YourSQLDba', 'msdb', 'model') AND x.DbName Not Like 'ReportServer%TempDB' AND x.DbName Not Like 'YourSQLDba%' AND DatabasepropertyEx(DbName, 'Status') = 'Online' -- To Avoid db that can't be processed Set @dbcount = @@ROWCOUNT Set @dblist = Stuff( @dblist, 1, 1, '') If @dbcount > 0 Begin declare @err nvarchar(max) Set @err = 'Violation of Recovery model policy for db :'+@dbList Exec yExecNLog.LogAndOrExec @context = 'yMaint.CheckFullRecoveryModelPolicy' , @YourSqlDbaNo = '006' , @err = @err , @Info = 'If you are sure you want those databases in SIMPLE recovery model you can use the «@ExcDbFromPolicy_CheckFullRecoveryModel» parameter of the «YourSQLDba_DoMaint» to exclude databases from the check' End End -- yMaint.CheckFullRecoveryModelPolicy GO -- ------------------------------------------------------------------------------ -- Procedure who perform log shrink -- ------------------------------------------------------------------------------ -- @@MARK: Maintenance : shrink logs Create Or Alter Proc yMaint.ShrinkLog @db Sysname , @MustLogBackupToShrink int output as Begin Declare @DatSize Int Declare @LogSize Int Declare @primaryFileName sysname Declare @Sql Nvarchar(max) = 'Select @primaryFileName=name from ['+@Db+'].sys.database_files Where data_space_id=1' Exec Sp_executeSql @Sql, N'@primaryFileName sysname Output', @primaryFileName output -- Here we workaround a bad practice that consist to have more than one log file -- So we ensure to have the most pertinent log file, by guessing that the biggest is the best Declare @LogFileName sysname Declare @LogPhysFileName sysname Select Top 1 @LogFileName = FileName, @LogPhysFileName=physical_name From dbo.DbsFileSizes(@db) Where type_desc='LOG' Order by fSizeInMb Desc -- we seek to guess an appropriate log size to data size ratio -- We make the assumption that aside the primary file, some other secondary -- files contibute less to log growth (history, blob) -- So in computing data size to log size ratio, we sum entire space of the primary file -- and 1/5 the size of other filegroup of type rows Select @DatSize=SUM(fSizeInMb/SizeDivisor) From ( Select FileName, fSizeInMb, SizeDivisor From Dbo.DbsFileSizes (@Db) CROSS APPLY (Select sizeDivisor=IIF(fileName=@PrimaryFileName, 1, 5)) as sizeDivisor Where type_desc = 'ROWS' ) as S Select Top 1 @LogSize=fSizeInMb From Dbo.DbsFileSizes (@Db) Where type_desc = 'LOG' Order by fSizeInMb Desc Declare @newSize Int Set @MustLogBackupToShrink = 0 -- Test if there is nothing that prevent log truncation and shrink -- The goal is to avoid causing errors to other transactions or replication/mirroring/backup processes. -- because of concurrent DBCC ShrinkFile -- In SQL2012 SP2 it happens frequently that a status LOG_BACKUP is there when there is not current log backup If exists (Select * from sys.databases where name = @Db And log_reuse_wait not in (0,2)) Begin -- Wait for 10 sec and try again WAITFOR DELAY '00:00:10'; If exists (Select * from sys.databases where name = @Db And log_reuse_wait not in (0,2)) BEGIN Print 'Log shrinking delayed for '+@Db Return ---- ******* Exit here END End Print 'Database '+@Db print 'Actual data size ' + convert(nvarchar(30), @datSize)+'Mb' print 'Actual log size ' + convert(nvarchar(30), @logSize)+'Mb' -- Condition to no perform a log shrink If (@logSize < @DatSize * 0.20) Or -- log size < 20% data size (@logSize < 10) And -- log size < 10 meg (@DatSize * 0.20 < 10) -- target datasize reduction must be greater than 10 meg Return -- new log size is reduced to one fifth of datafile Set @newSize = @DatSize * 0.20 Print 'Log shrink in process for '+@Db /*===ShrinkTemplate=== ---------------------------------------------------------------------------------------------- -- Shrink of log file () ---------------------------------------------------------------------------------------------- USE [#DbName#] Begin Try DBCC SHRINKFILE (N'#name#', #targetSize#) with no_infomsgs -- if still here, shrink is successful, so erase past shrink failure Delete YourSqlDba.Maint.DbccShrinkLogState Where dbName = Db_name() End Try Begin Catch Declare @errm nvarchar(4000); Select @errm = ErrMsg From YourSqlDba.S#.FormatCurrentMsg(null) -- first error already logged, do not do it again Insert into YourSqlDba.Maint.DbccShrinkLogState (DbName, FailedShrinkTime) Select Db_name(), Getdate() Where Not Exists (Select * From YourSqlDba.Maint.DbccShrinkLogState Where dbName = Db_name()) -- FailedShrinkTime -- or when there was an error during last shrink If Exists ( -- not succeed shrink happen since 2 hours so error still in log Select * From YourSqlDba.Maint.DbccShrinkLogState Where dbName = Db_name() And Datediff(hh, FailedShrinkTime, Getdate()) > 2 ) Begin -- If there is not succeed shrink since 2 hours, notify it as a YourSqlDba error Exec yExecNLog.LogAndOrExec @context = 'yMaint.ShrinkLog' , @YourSqlDbaNo = '015' , @Info = 'Shrink Log error' , @err = @errm End End Catch ===ShrinkTemplate===*/ Select @Sql = R.Code From (Select DbName=@Db, name=@LogFileName, physName=@LogPhysFileName, TargetSize=Str(@newSize,10)) as TagCols CROSS APPLY (Select toReplace=(Select TagCols.* for Json Path, INCLUDE_NULL_VALUES)) as ToReplace CROSS APPLY S#.GetTemplateFromCmtAndReplaceTags ('===ShrinkTemplate===', NULL, ToReplace) as R Exec yExecNLog.LogAndOrExec @context = 'yMaint.ShrinkLog' , @Info = 'Log Shrink' , @sql = @sql /*===GetLogSize=== Select @logSize = (size / 128) From [#DbName#].sys.database_files df ===GetLogSize===*/ Select @Sql = R.Code From (Select ToReplace = (Select DbName=@Db for Json Path, INCLUDE_NULL_VALUES)) as ToReplace CROSS APPLY S#.GetTemplateFromCmtAndReplaceTags ('===GetLogSize===', NULL, ToReplace) as R print @sql Exec sp_executeSql @sql , N'@logSize Int Output' , @logSize Output -- if log doesn't shrink, shrink needs to be done more than once, with log backups in between -- a return value instruct the caller to do so If (Abs(@newSize - @logSize) / @newSize) > 0.01 Set @MustLogBackupToShrink = 1 End -- yMaint.ShrinkLog GO -- ------------------------------------------------------------------------------ -- Utility proc to shrink all logs -- Intended for use with non YourSqlDba backup solution like CommVault -- when its does its log backups. -- Must be call as Commvault post-job, through SQLCMD, to perform log shrinking -- See https://tinyurl.com/YourSqlDbaAncCommVault for a more detailed overview. -- ------------------------------------------------------------------------------ -- @@MARK: Maintenance : shrink all logs Create Or Alter Proc Maint.ShrinkAllLogs as Begin Declare @Sql Nvarchar(max) Declare @ignore int Select @sql=S.Sql From (Select JsonPrm= (Select MaintJobName='Maint.ShrinkAllLogs' For JSON PATH, WITHOUT_ARRAY_WRAPPER ) ) as JsonPrm CROSS APPLY dbo.ScriptSetGlobalAccessToPrm (JsonPrm) as S Exec (@Sql) -- Execute SQL generated by previous query --Drop Table IF Exists #Db --avoid this in production, cause reentrancy problem Select DbName=name Into #Db From sys.databases Where user_access_desc IN ('MULTI_USER','SINGLE_USER') And state_desc ='ONLINE' And recovery_model_desc <> 'SIMPLE' Declare @Db sysname While (1=1) Begin Select Top 1 @Db=DbName From #Db If @@ROWCOUNT=0 Break Delete #Db Where DbName=@Db exec yMaint.ShrinkLog @Db=@Db, @MustLogBackupToShrink = @ignore output -- in that case do not attempt log backup if shrinking attempt do not change something End End GO -- ------------------------------------------------------------------------------ -- Utility proc to bring back all Db offline in normal mode -- in case YourSqlDba put them offline because of a disconnected drive -- ------------------------------------------------------------------------------ -- @@MARK: Maintenance : bring back online all Create Or Alter Proc Maint.BringBackOnlineAllOfflineDb as Begin Declare @sql nvarchar(max) Select name, cast (databasepropertyex(name, 'status') as Sysname) as Status into #Db From sys.databases Where databasepropertyex(name, 'status') = 'OFFLINE' Declare @n sysname, @status sysname While exists (select * from #Db) Begin Select top 1 @n = name, @status = Status from #Db Set @sql = ' Alter database [] Set online ' Set @sql = yExecNLog.Unindent_TSQL(@sql) Set @Sql= Replace (@sql, '', @n) Exec (@sql) print @sql Delete from #Db where name = @n End End -- Maint.BringBackOnlineAllOfflineDb GO ----------------------------------------------------------------------------- -- yMaint.LogCleanup (for entries older than 30 days) -- Mail logs -- Backup history logs -- Job history -- Job History in YourSqlDba Tables -- Cycle SQL Server error log ----------------------------------------------------------------------------- -- @@MARK: Maintenance : logs cleanup Create Or Alter Proc yMaint.LogCleanup as Begin declare @d nvarchar(8) declare @lockResult int declare @sql nvarchar(max) Begin try Set @sql = 'Exec msdb.dbo.sysmail_delete_log_sp @logged_before = "";' Set @sql = replace (@sql, '', convert(nvarchar(8), dateadd(dd, -30, getdate()), 112)) Set @sql = replace (@sql, '"', '''') Exec yExecNLog.LogAndOrExec @context = 'yMaint.LogCleanup' , @info = 'Cleanup log entries older than 30 days, begins with mail' , @sql = @sql Set @sql = 'EXECUTE msdb.dbo.sysmail_delete_mailitems_sp @sent_before = "";' Set @sql = replace (@sql, '', convert(nvarchar(8), dateadd(dd, -30, getdate()), 112)) Set @sql = replace (@sql, '"', '''') Exec yExecNLog.LogAndOrExec @context = 'yMaint.LogCleanup' , @info = 'Cleanup log entries older than 30 days, for mailitems' , @sql = @sql -- clean backup history Set @sql = 'exec Msdb.dbo.sp_delete_backuphistory @oldest_date = "" ' Set @sql = replace (@sql, '', convert(nvarchar(8), dateadd(dd, -30, getdate()), 112)) Set @sql = replace (@sql, '"', '''') Exec yExecNLog.LogAndOrExec @context = 'yMaint.LogCleanup' , @info = 'Cleanup log entries older than 30 days, for backup history' , @sql = @sql -- clean sql agent job history Set @sql = 'EXECUTE Msdb.dbo.sp_purge_jobhistory @oldest_date = ""' Set @sql = replace (@sql, '', convert(nvarchar(8), dateadd(dd, -30, getdate()), 112)) Set @sql = replace (@sql, '"', '''') Exec yExecNLog.LogAndOrExec @context = 'yMaint.LogCleanup' , @info = 'Cleanup log entries older than 30 days, for job history' , @sql = @sql -- clean job maintenance job history (SQL Server own maintenance) Set @sql = 'EXECUTE Msdb.dbo.sp_maintplan_delete_log null,null,""' Set @sql = replace (@sql, '', convert(nvarchar(8), dateadd(dd, -30, getdate()), 112)) Set @sql = replace (@sql, '"', '''') Exec yExecNLog.LogAndOrExec @context = 'yMaint.LogCleanup' , @info = 'Cleanup log entries older than 30 days, for SQL Server job maintenace plans' , @sql = @sql -- archive current log, and start a new one Set @sql = 'Execute sp_cycle_errorlog' Set @sql = replace (@sql, '', convert(nvarchar(8), dateadd(dd, -30, getdate()), 112)) Set @sql = replace (@sql, '"', '''') Exec yExecNLog.LogAndOrExec @context = 'yMaint.LogCleanup' , @info = 'Recycle Sql Server error log, start a new one' , @sql = @sql -- cleanup YourSqlDba's job history (keep no more than 30 days behind) -- I limit the drop to 100 jobs at a time so the log file can clear itself between each batch and avoid log growth. -- Under the new logging system, each run (including log backups) has a different job number. -- Therefore, log backups account for approximately 96 jobs (24 hours * 4 times per hour) While (1=1) Begin Delete top (100) H From ( Select distinct JobNo -- From Maint.JobHistory Where JobStart < dateadd(dd, -30, getdate()) ) as T join Maint.JobHistory H On H.JobNo = T.JobNo If @@rowcount = 0 Break End End try Begin catch Exec yExecNLog.LogAndOrExec @context = 'yMaint.LogCleanup' , @Info = 'Error caught in proc' , @err = '?' End Catch End -- yMaint.LogCleanup GO ---------------------------------------------------------------------------------------- -- yMaint.IntegrityTesting -- Process integrity testing using -- @@MARK: Maintenance : process integrity testing Create Or Alter Proc yMaint.IntegrityTesting as Begin declare @cmptlevel Int declare @dbName nvarchar(512) declare @sql nvarchar(max) declare @lockResult int declare @errorN int declare @seqCheckNow Int declare @doFullCheckDb Int declare @sizeDb bigInt Declare @action XML Declare @seq Int Declare @SpreadCheckDb Int Select @SpreadCheckDb=SpreadCheckDb From dbo.MainContextInfo(null) Set @DbName = '' --drop table if exists #db;select dbName = name into #db from sys.databases; declare @SpreadCheckDb int = 7, @seqCheckNow int Update Maint.JobSeqCheckDb Set @seqCheckNow = (seq + 1) % MinSpread, seq = @seqCheckNow From ( Select MinSpread=Min(IIF(MaxSpread<1,1,MaxSpread)) From ( -- do not let seq get higher than the number of databases -- and avoid divide by zero error if database filter gives no db Select MaxSpread=(Select NbOfDb=Count(*) From #Db) UNION ALL Select MaxSpread=@SpreadCheckDb ) as MinPrm ) as MinPrm --drop table if exists #tmp --insert into #tmp ----------------------------------------------------------------------------------------------------------- -- This query finds a db processing order that helps to evently distribute the DBCC work based on -- database size, because I suppose the work for DBCC CheckDb is somewhat proportional to db size. -- Either we do a full DBCC checkdb or a DBCC checkdb with physical_only -- -- So the idea of the query is to rank databases by size and then from each group this rank makes -- I choose to process one only one of every n database where n is either the minimum of -- @spreadCheckDb or the number of database. Suppose spreadCheckDb is 7 and there is only three databases -- that qualifies, we are going to cycle among theses 3, without waiting for n to reach spreadcheckDb -- from values 4 to 7 without doing any fullcheckdb. -- This smooths a lot TOTAL difference (and hence execution time) between Db size to procesd. -- However there is little remaining increasing size at each increasing n value. To alleviate even more -- this effect, I introduce a factor to sorting by size that bias size both up or down based on database id. -- This bias applied to sort order give a more even distribution for close database sizes -- Obviously there is nothing that can be done for large disrcrepancies (over 40%) in database sizes at top sizes -- and this remains unavoidable ---------------------------------------------------------------------------------------------------------- -- !!!some initialisation code to test isolately the query below, keep the comment -- Drop table if exists #Db -- Select DbName=Name Into #Db From Sys.Databases -- declare @SpreadCheckdb int=3, @seqCheckNow int = 0 Select SpreadSet.*, CheckTurnValue, DbTurn into #DbToCheck From (Select SpreadCheckDb = @SpreadCheckDb, seqCheckNow = @seqCheckNow) as Prm CROSS APPLY ( -- this query gives a sizeOrder starting to 1 in each group -- so it simplify computation of shift in turn sequence Select * -- this value helps compute a value to distribute and then execution time more -- order based on size by using size*bias to sort processing order. , SizeOrderInSpreadGroup=row_number() Over(Order by BiaisedValueFromSize) - 1 From (Select NbOfDb=count(*) From #db) as NbOfDb CROSS JOIN ( -- Get database Sizes by the sum of size all of their files Select BdSizes.*, cmptlevel=D.compatibility_level, D.page_verify_option_desc From ( -- Sum by db name SELECT Db.dbname, sizedb = SUM(S.fSizeInMb) FROM #db as Db CROSS APPLY dbo.DbsFileSizes (Db.DbName) as S GROUP BY Db.dbname ) as BdSizes JOIN Sys.Databases as D -- get complementary info about the database ON D.name = BdSizes.Dbname ) as Dbs -- If the number of database is lower than SpreadCheckPrm, reduce maximum of sequence to the minimum of both -- otherwise no database is going to match for those runs where seq of Maint.JobSeqCheckDb is higher than the -- number of databases Cross Apply (Select MinSpreadSetSize=MIN(x) From (Values (SpreadCheckDb),(NbOfdb)) as t(x)) as maxSpreadSetSize -- A bias is introduced to avoid bigger total execution time with bigger DB -- Actually this bias is broken down by taking one of the db every n db where n is either -- the least of SpreadCheckDb Prm or the number of databases to process, but this is prone to bigger execution times -- The compute factor below, will allow sizes to shift a little higher or lower depending on dbId Cross Apply (Select BiasToApplyToSizeInSequenceOrder=IIF(Db_Id(DbName)%2=1, 1.20, 0.8)) as BiasToApplyToSizeInSequenceOrder Cross Apply (Select BiaisedValueFromSize=Dbs.SizeDb * BiasToApplyToSizeInSequenceOrder) as BiaisedValueFromSize ) as SpreadSet -- Decide if it is this Bd turn in increasing order CROSS APPLY (Select CheckTurnValue=SizeOrderInSpreadGroup % MinSpreadSetSize) as CheckTurnValue CROSS APPLY (Select DbTurn=IIF( page_verify_option_desc <> 'CHECKSUM' Or CheckTurnValue = SeqCheckNow, 1, 0)) as doFullCheckDb While(1 = 1) -- simulate simple do -- loop Begin -- process on database at the time in name order Select top 1 @DbName = DbName , @cmptlevel = cmptlevel , @doFullCheckDb = DbTurn , @sizeDb = sizedb From #DbToCheck Where DbName > @DbName -- next Dbname greater than @dbname Order By DbName -- dbName order -- exit loop if no more name greater than the last one used If @@rowcount = 0 Break ----------------------------------------------------------------------------------------------- -- Very Large Databases : Not using DBCC CHECKDB but instead CHECKTABLE -- On sunday only, CheckAlloc and CheckCatalog being are done first on the database. -- CHECKTABLE are done everyday spreaded across the number of day to spread the job -- For VLDB Integrity testing DBCC Checktable proceed par parts all the spread period everyday ----------------------------------------------------------------------------------------------- if (@sizeDb > 10000000) -- Database sizes are in MB. begin SET DATEFIRST 7 DECLARE @DayOfWeek int SELECT @DayOfWeek = DATEPART(WEEKDAY, GETDATE()) CREATE TABLE #tableNames (Query nvarchar(max), seq int) set @sql = ' Use [] set nocount on ;With TableSizeStats as ( select object_schema_name(Ps.object_id) as scn --collate Latin1_General_CI_AS , object_name(Ps.object_id) as tb --collate Latin1_General_CI_AS From sys.dm_db_partition_stats ps JOIN sys.tables t ON ps.object_id = t.object_id join sys.indexes i on i.object_id=t.object_id where object_name(Ps.object_id) not like "Temp%" Group by Ps.object_id UNION SELECT object_schema_name(o.id), o.name FROM sysobjects o INNER JOIN sysindexes i ON o.id = i.id WHERE o.xtype = "V" -- View ) INSERT INTO #tableNames (Query, seq) Select "Use []; DBCC CHECKTABLE ("""+scn+"."+tb+""")" as Query, row_number() over (order by scn, tb) as seq From TableSizeStats where scn is not null and tb is not null and (abs(checksum(tb)) % ) = ' -- On Saturday IF (@DayOfWeek = 7) BEGIN Set @sql = replace(@sql,'', 'DBCC CHECKALLOC (""); DBCC CHECKCATALOG ("");' ) END ELSE BEGIN Set @sql = replace(@sql,'', '' ) END Set @sql = replace(@sql,'', @dbName ) Set @sql = replace(@sql,'', @SpreadCheckDb ) Set @sql = replace(@sql,'', @seqCheckNow ) set @sql = replace(@sql,'"','''') -- useful to avoid duplicating of single quote in boilerplate Set @ErrorN = 0 Exec yExecNLog.LogAndOrExec @context = 'yMaint.IntegrityTesting' , @sql = @sql , @ErrorN = @ErrorN Output -- For real integrity problem, we put database offline to avoid worsening of integrity problems. -- But when only error 5128 occurs there is no integrity problem. -- DBCC use an internal database snapshot and if there is a lack of space -- error message 5128 happens 'Write to sparse file '%ls' failed due to lack of disk space.' -- So we don't put the DB offline, if it is THE ONLY problem. -- LogAndOfExec takes care of returning 1 if any errors other than 5128 happens, and 5128 if it is the only type of error If @errorN NOT IN (0, 5128) Exec yMaint.PutDbOffline @DbName WHILE EXISTS(SELECT TOP 1 1 FROM #tableNames) BEGIN DECLARE @query nvarchar(max), @seqquery int SELECT top 1 @query=query, @seqquery=seq from #tableNames delete from #tableNames where seq=@seqquery set @query = replace(@query,'"','''') -- useful to avoid duplicating of single quote in boilerplate Set @query = replace(@query,'', @dbName ) Set @ErrorN = 0 Exec yExecNLog.LogAndOrExec @context = 'yMaint.IntegrityTesting' , @sql = @query , @ErrorN = @ErrorN Output -- For real integrity problem, we put database offline to avoid worsening of integrity problems. -- But when only error 5128 occurs there is no integrity problem. -- DBCC use an internal database snapshot and if there is a lack of space -- error message 5128 happens 'Write to sparse file '%ls' failed due to lack of disk space.' -- So we don't put the DB offline, if it is THE ONLY problem. -- LogAndOfExec takes care of returning 1 if any errors other than 5128 happens, -- and 5128 if it is the only type of error If @errorN <> 0 Begin If @errorN <> 5128 Exec yMaint.PutDbOffline @DbName End END Drop Table If Exists #tableNames END -- If for Very Large Databases ELSE BEGIN -- Regular databases, either do Physical_ONLY most of the time, or once every n run without Physical_only Set @sql = 'DBCC checkDb("") '+ IIF(@doFullCheckDb = 0,' WITH PHYSICAL_ONLY;',';') Set @sql = replace(@sql,'', @dbName ) set @sql = replace(@sql,'"','''') -- useful to avoid duplicating of single quote in boilerplate Set @ErrorN = 0 Exec yExecNLog.LogAndOrExec @context = 'yMaint.IntegrityTesting' , @sql = @sql , @ErrorN = @ErrorN Output If @errorN <> 0 Begin If Not exists -- check if this Txt has no error 5128, put it offline ( Select * From ( -- get current Txt, which is the latest (highest seq) for this spid and this job Select Top 1 HD.JobNo, Seq From Dbo.MainContextInfo(null) as ctx JOIN Maint.JobHistoryDetails as HD ON HD.jobNo = Ctx.jobNo order by HD.JobNo, seq desc ) as lastAct JOIN Maint.JobHistoryLineDetails as LD ON LD.JobNo = LastAct.JobNo And LD.seq = LastAct.Seq -- if the logged error is the one below, this is due to lack of space -- and we don't put the database offline for this And Ld.Txt Like 'Error 5128%' ) Exec yMaint.PutDbOffline @DbName End -- if error END -- Regular database case End -- While boucle banque par banque End -- yMaint.IntegrityTesting GO -- @@MARK: Maintenance : update stats Create Or Alter Proc yMaint.UpdateStats as Begin declare @seqStatNow Int declare @cmptlevel Int declare @dbName sysname declare @sql nvarchar(max) declare @lockResult int Declare @seq Int -- row sequence for row by row processing Declare @scn sysname -- schema name Declare @tb sysname -- table name declare @sampling Int -- page count to get an idea if the size of the table Declare @idx sysname -- index name Declare @object_id int -- a proof that an object exists Declare @SpreadUpdStatRun Int Select @SpreadUpdStatRun=SpreadUpdStatRun From dbo.MainContextInfo(null) Begin Try Create table #TableNames ( scn sysname , tb sysname , sampling nvarchar(3) , seq int , primary key clustered (seq) ) Update Maint.JobSeqUpdStat Set @seqStatNow = (seq + 1) % @SpreadUpdStatRun, seq = @seqStatNow Set @DbName = '' While(1 = 1) -- simple do loop Begin Select top 1 -- first next in alpha sequence after the last one. @DbName = DbName , @cmptLevel = CmptLevel From #Db Where DbName > @DbName Order By DbName -- exit if nothing after the last one processed If @@rowcount = 0 Break -- -- If database is not updatable, skip update stats for this database If DATABASEPROPERTYEX(@DbName, 'Updateability') = N'READ_ONLY' Continue -- If database is in emrgency, skip update stats for this database If DatabasepropertyEx(@DbName, 'Status') = 'OFFLINE' Continue -- makes query boilerplate with replacable parameter identified by -- labels between "<" et ">" -- this query select table for which to perform update statistics truncate table #TableNames set @sql = ' Use [] set nocount on ;With TableSizeStats as ( select object_schema_name(Ps.object_id) as scn --collate , object_name(Ps.object_id) as tb --collate , Sum(Ps.Page_count) as Pg From sys.dm_db_index_physical_stats (db_id(""), NULL, NULL, NULL, "LIMITED") Ps Where ( OBJECTPROPERTYEX ( Ps.object_id , "IsTable" ) = 1 Or OBJECTPROPERTYEX ( Ps.object_id , "IsView" ) = 1) Group by Ps.object_id ) Insert into #tableNames (scn, tb, seq, sampling) Select scn , tb , row_number() over (order by scn, tb) as seq , Case When Pg > 5000001 Then "0" When Pg between 1000001 and 5000000 Then "1" When Pg between 500001 and 1000000 Then "5" When pg between 200001 and 500000 Then "10" When Pg between 50001 and 200000 Then "20" When Pg between 5001 and 50000 Then "30" else "100" End From TableSizeStats where scn is not null and tb is not null and (abs(checksum(tb)) % ) = ' set @sql = replace(@sql,'',convert(nvarchar(100), Serverproperty('collation'))) Set @sql = replace(@sql,'', convert(nvarchar(20), @seqStatNow)) Set @sql = replace(@sql,'', convert(nvarchar(20), @SpreadUpdStatRun)) set @sql = replace(@sql,'"','''') -- to avoid doubling of quotes in boilerplate set @sql = replace(@sql,'',@DbName) Exec yExecNLog.LogAndOrExec @context = 'yMaint.UpdateStats' , @Info = 'Table selection for update statistics' , @sql = @sql , @forDiagOnly = 1 set @seq = 0 While (1 = 1) begin Select top 1 @scn = scn, @tb = tb, @sampling = sampling, @seq = seq from #TableNames where seq > @seq order by seq if @@rowcount = 0 break Set @sql = 'Select @object_id = object_id("..") ' set @sql = replace (@sql, '', @DbName) set @sql = replace (@sql, '', @scn) set @sql = replace (@sql, '', @tb) set @sql = replace (@sql, '"', '''') Exec sp_executeSql @Sql, N'@object_id int output', @object_id output If @object_id is not null Begin Set @sql = 'update statistics [].[].[] WITH sample PERCENT' set @sql = replace (@sql, '', @DbName) set @sql = replace (@sql, '', @scn) set @sql = replace (@sql, '', @tb) If @sampling = 0 set @sql = replace (@sql, 'WITH Sample PERCENT', '') Else BEGIN If @sampling < 100 set @sql = replace (@sql, '', Str(@sampling)) Else set @sql = replace (@sql, 'Sample PERCENT', 'FULLSCAN') END set @sql = replace (@sql, '"', '''') Exec yExecNLog.LogAndOrExec @sql = @sql End end -- While End -- While boucle banque par banque End try Begin catch Exec yExecNLog.LogAndOrExec @context = 'yMaint.UpdateStats Error', @err = '?' End Catch End -- yMaint.UpdateStats GO -- @@MARK: Maintenance : Todo : outaded way to reorganize / rebuild indexes Create Or Alter Proc yMaint.ReorganizeOnlyWhatNeedToBe as Begin declare @cmptlevel Int declare @dbName sysname declare @sql nvarchar(max) declare @lockResult int Declare @seq Int -- row sequence in work table Declare @scn sysname -- schema name Declare @tb sysname -- table name Declare @td sysname -- object type Declare @idx sysname -- index name Declare @colName sysname -- index column name Declare @pgLock int -- index page_locking flag Declare @partitionNum Int Declare @frag float Declare @index_type_desc NVARCHAR(60) Declare @Page_count BigInt Declare @alloc_unit_type_desc NVARCHAR(60) Declare @TotPartNb Int Declare @Info nvarchar(max) Declare @ReorgType nvarchar(10) Begin Try Declare @recMode sysname Create table #IndexNames ( scn sysname null , tb sysname null , td sysname null , idx sysname null , pgLock int null , partitionnum Int null , frag float null , index_type_desc NVARCHAR(60) null , Page_count BigInt null , alloc_unit_type_desc NVARCHAR(60) null , TotPartNb Int null , colname Sysname null , ReorgType as Case When (frag between 10.0 and 50.0 And pgLock = 1 and page_count > 8) Then 'Reorg' When (frag > 50.0 and page_count > 8) Or (frag > 10.0 and pgLock = 0 and page_count > 8) Then 'Rebuild' Else '' End , seq int , primary key clustered (seq) ) Set @DbName = '' While(1 = 1) -- Emulate simple loop, exit internally by a break statement on a given condition Begin -- read only one database at the time -- Top 1 clause with order is used to get the next database -- in alphebetic order and which is next to the last database name processed or "" -- makes simpler shorter and ffaster code than using cursors Select top 1 @DbName = DbName , @cmptLevel = CmptLevel From #Db Where DbName > @DbName Order By DbName -- If there is no next database to the last one read If @@rowcount = 0 Break -- exit -- If database is not updatable, GO to the next in the list If DATABASEPROPERTYEX(@DbName, 'Updateability') = N'READ_ONLY' Continue -- If database is not updatable, GO to the next in the list If DatabasepropertyEx(@DbName, 'Status') IN (N'EMERGENCY', N'OFFLINE') Continue truncate table #IndexNames set @sql = ' Use [] set nocount on select IDX.object_id, IDX.index_id, IDX.name as IndexName, IDX.allow_page_locks, IDX.type_desc into #Indexes From sys.indexes IDX join sys.objects OBJ on IDX.object_id = OBJ.object_id join sys.schemas S on S.schema_id = OBJ.schema_id Where OBJ.type_desc = "User_Table" And IDX.Is_Disabled = 0 insert into #IndexNames ( scn, tb, td, IDX, pglock, partitionnum, frag, index_type_desc , Page_count, alloc_unit_type_desc, TotPartNb, ColName , seq) select S.name --collate , OBJ.name --collate , OBJ.type_desc --collate , IDX.IndexName , IDX.allow_page_locks , PS.partition_number AS partitionnum , PS.avg_fragmentation_in_percent AS frag , IDX.type_desc , PS.Page_count , PS.alloc_unit_type_desc , Max (partition_number) OVER(PARTITION BY IDX.object_id, IDX.index_id) as TotPartNb , ( select top 1 SC.name from sys.columns SC Where SC.object_id = IDX.object_id And Columnproperty(OBJ.object_id, SC.name, "IsIndexable") = 1 -- Version 1.2 Order by SC.column_id ) as ColName , row_number() over (order by S.name, OBJ.name, IDX.IndexName, PS.partition_number) as seq From #Indexes IDX join sys.objects OBJ on IDX.object_id = OBJ.object_id join sys.schemas S on S.schema_id = OBJ.schema_id CROSS APPLY sys.dm_db_index_physical_stats (db_id(""), IDX.object_id, IDX.index_id, NULL, "LIMITED") PS Where PS.avg_fragmentation_in_percent > 5 Drop Table If Exists #Indexes ' -- Version 1.2 If not exists (select * from sys.databases where name = @DbName And compatibility_level >= 90) Begin Set @sql = replace (@sql, 'sys.dm_db_index_physical_stats (db_id(""), NULL, NULL, NULL, "LIMITED")', '(select 0 as partition_number, 100 as avg_fragmentation_in_percent, 1000 as Page_count, "" as alloc_unit_type_desc)' ) Set @sql = replace (@sql, 'join --cross join', 'Cross join') Set @sql = replace (@sql, 'on Idx.object_id = Ps.object_id And Idx.index_id = Ps.index_id', '') End -- Version 1.2 set @sql = replace(@sql,'',convert(nvarchar(100), Serverproperty('collation'))) set @sql = replace(@sql,'"','''') -- trick to use " instead of doubling quotes in query string set @sql = replace(@sql,'',@DbName) Exec yExecNLog.LogAndOrExec @sql = @sql , @forDiagOnly = 1 -- display all messages not only those with maxSeverity=1 -- select 'trace', * from #IndexNames -- makes query boilerplate with replacable parameter identified by -- labels between "<" et ">" -- build only one message for tables that need not defrag of any indexes Select @info = ( Select Convert (nvarchar(max), '') + scn + '.' + tb + NCHAR(10) as [text()] from #IndexNames --Where index_type_desc <> 'HEAP' Group By scn, tb Having Min(ReorgType) = '' for XML PATH('') ) set @Info = 'Index and heap Reorg' + nchar(10) + 'Defragmentation not needed to be done in ' + @DbName+ ' for tables:' + NCHAR(10) + @info Exec yExecNLog.LogAndOrExec @context = 'yMaint.ReorganizeOnlyWhatNeedToBe' , @Info = @info , @forDiagOnly = 0 -- process defrag set @seq = 0 While (1 = 1) begin Select top 1 @scn = scn, @tb = tb, @idx = idx, @pgLock = pgLock, @partitionNum = partitionnum, @index_type_desc = index_type_desc, @alloc_unit_type_desc = alloc_unit_type_desc, @TotPartNb = TotPartNb, @Colname = Colname, @ReorgType = ReorgType, @seq = seq from #IndexNames I where seq > @seq -- And index_type_desc <> 'HEAP' order by seq if @@rowcount = 0 break If @index_type_desc <> 'HEAP' Begin Set @sql = Case When @ReorgType = 'Reorg' Then ' ALTER INDEX [] ON [].[].[] Reorganize PARTITION = With (LOB_COMPACTION = On) ' When @ReorgType = 'Rebuild' Then ' ALTER INDEX [] ON [].[].[] Rebuild; ' Else '' End End Else -- don't try to handle heap Begin -- don't try to reorganize Heap -- If @page_count > 8 And @colName is not NULL -- Version 1.2 -- Set @sql = -- ' -- Use [] -- Create clustered index [IdxDefrag] ON [].[] ([]) with (fillfactor = 95); -- Exec("Drop index [].[].[IdxDefrag]") -- ' -- Else Set @sql = '' End set @sql = replace (@sql, '', @scn collate database_default) set @sql = replace (@sql, '', @tb collate database_default) set @sql = replace (@sql, '', isnull(@idx, '') collate database_default) set @sql = replace (@sql, '', @colName collate database_default) -- if no clustered index set @sql = replace (@sql, '', @DbName collate database_default) set @sql = replace (@sql, '"', '''') If @TotPartNb > 1 Set @sql = replace(@sql, '', Convert(nvarchar(20), @partitionNum)) Else Set @sql = replace(@sql, 'PARTITION = ', '') If @sql <> '' Exec yExecNLog.LogAndOrExec @context = 'yMaint.ReorganizeOnlyWhatNeedToBe' , @Info = 'Index and heap Reorg' , @sql = @sql End -- While loop index by index End -- While loop database by database End try Begin catch Exec yExecNLog.LogAndOrExec @context = 'yMaint.ReorganizeOnlyWhatNeedToBe Error', @err='?' End Catch End -- yMaint.ReorganizeOnlyWhatNeedToBe GO -- ------------------------------------------------------------------------------ -- Function that get the installation language of the instance -- ------------------------------------------------------------------------------ -- @@MARK: Todo : better way? Create Or Alter Procedure yInstall.InstallationLanguage @language nvarchar(512) output as Begin create table #SVer(ID int, Name sysname, Internal_Value int, Value nvarchar(512)) insert #SVer exec master.dbo.xp_msver Language Select @language = Value from #SVer where Name = N'Language' End -- yInstall.InstallationLanguage GO -- ------------------------------------------------------------------------------ -- Function that builds backup file name -- ------------------------------------------------------------------------------ Create Or Alter Function yMaint.iTvf_MakeBackupFileName ( @DbName sysname , @bkpTyp Char(1) , @FullBackupPath nvarchar(512) , @Language nvarchar(512) , @Ext nvarchar(7) = NULL , @TimeStampNamingForBackups Int = 1 ) returns Table as -- @@MARK: Todo : Make backup file name - itvf Return Select * From (Select DebugMode=0) as DebugMode CROSS APPLY ( Select prmDbName = @Dbname , prmBkpTyp = @BkpTyp , prmFullBackupPath=@FullBackupPath , prmLanguage=@Language , prmExt=IIF(@Ext Not IN ('F', 'D'), 'L', @Ext) , prmTimeStampNamingForBackups= @TimeStampNamingForBackups Where DebugMode=0 UNION ALL Select prmDbName = 'MSDB' , prmBkpTyp = 'F' , prmFullBackupPath='\\SomeServer\SomeShare\' , prmLanguage='Francais' , prmExt='BAK' , prmTimeStampNamingForBackups= 1 Where DebugMode=1 ) as prm CROSS APPLY (Select BkpTS0 = Convert(nvarchar(30), getdate(), 120) ) as BkpTS0 CROSS APPLY (Select BkpTS1 = STUFF (BkpTS0, 11, 1, '_')) as BkpTS1 CROSS APPLY (Select BkpTS2 = STUFF (BkpTS1, 14, 1, 'h')) as BkpTS2 CROSS APPLY (Select BkpTS3 = STUFF (BkpTS2, 17, 1, 'm')) as BkpTS3 -- For Msdb I don't keep details up to the minute -- utiliser format 120 pour MSDB CROSS APPLY (Select BkpTS = IIF(prmDbName<>'MSDB', BkpTS3, Left(Convert(nvarchar(30), getdate(), 121),10))) as BkpTS -- This formula give an DayOfWeekNo that is independant of the language -- starts with 1, for Us_english Dim=1, for french CROSS APPLY (Select DayOfWeekNo=((@@datefirst + DatePart(dw, getdate())) % 7) + 1) as DayOfWeekNo CROSS APPLY ( Select DayOfWeek = CHOOSE (DayOfWeekNo, 'Dim', 'Lun', 'Mar', 'Mer', 'Jeu', 'Ven', 'Sam') Where prmLanguage like 'Français%' UNION ALL Select DayOfWeek = CHOOSE (DayOfWeekNo, 'Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat') Where prmLanguage Not like 'Français%' ) as DayStr CROSS APPLY (Select destin=yUtl.NormalizePath(prmFullBackupPath)) as destin LEFT JOIN (Values ('F', 'Database', 'Bak'), ('D', 'Differential', 'Bak'), ('L', 'Logs', 'Trn')) as t(typCode, typ, Ext) On t.typCode=prmBkpTyp OUTER APPLY (Select DteHrJr='['+BkpTs+'_'+DayOfWeek+']_' Where ISNULL(prmTimeStampNamingForBackups, 1)=1) as DteHrJr CROSS APPLY (Select Justfilename=prmDbName+'_'+ISNULL(DteHrJr,'')+typ+'.'+Ext) as JustFilename CROSS APPLY (Select filename=Destin+prmDbName+'_'+ISNULL(DteHrJr,'')+typ+'.'+Ext) as Filename Go -- ------------------------------------------------------------------------------ -- Function that builds backup file name -- ------------------------------------------------------------------------------ -- @@MARK: Todo : Make backup file name - scalar Create Or Alter Function yMaint.MakeBackupFileName ( @DbName sysname , @bkpTyp Char(1) , @FullBackupPath nvarchar(512) , @Language nvarchar(512) , @Ext nvarchar(7) = NULL , @TimeStampNamingForBackups Int = 1 ) returns nvarchar(max) as Begin Return ( Select fileName From yMaint.iTvf_MakeBackupFileName(@DbName,@BkpTyp,@FullBackupPath,@Language,@Ext,@TimeStampNamingForBackups) ) End GO Create Or Alter Function yMaint.iTvf_MakeBackupCmd -- ------------------------------------------------------------------------------ -- Function iTVF that builds backup command -- Can be used in a set based fashion, into a compound query -- or directly from a udf wrapper -- ------------------------------------------------------------------------------ -- @@MARK: Maintenance - Make backup Cmd iTvf ( @DbName sysname , @bkpTyp Char(1) , @fileName nvarchar(512) , @overwrite Int , @name nvarchar(512) , @EncryptionAlgorithm nvarchar(10) , @EncryptionCertificate nvarchar(100) ) Returns Table as Return SELECT Sql=Internals.Code, Internals.* FROM (Select DebugMode=0) as DebugMode CROSS APPLY ( Select * From ( SELECT DbName = @DbName , prmBkpTyp = @bkpTyp , FileName = @fileName , prmOverwrite = @overwrite , prmName = @name , EncryptionAlgorithm = @EncryptionAlgorithm , EncryptionCertificate = @EncryptionCertificate , Self = 'yMaint.iTvf_MakeBackupCmd' Where DebugMode = 0 Union All Select DbName , prmBkpTyp = 'F' , fileName , prmOverwrite = 1 , prmName = N'TestDb full backup' , EncryptionAlgorithm = N'AES_256' , EncryptionCertificate = N'MyBackupCert' , Self = 'yMaint.iTvf_MakeBackupCmd' -- select NULL if selected code run from proc From (Select filler=Replicate('#', 10)) as filler CROSS APPLY (Select DbName=N'TestDbWithAVeryLongDbName') as DbName CROSS APPLY (Select Dir=N'C:\ATestVeryLoooooooooooooooongDirectoryForBackups\') as Dir CROSS APPLY (Select justFileName, fileName From iTvf_MakeBackupFileName(DbName, 'F', Dir, 'FRANCAIS', 'BAK', 1)) as f Where DebugMode = 1 ) AS PrmsFct OUTER APPLY ( Select fBkpTyp='Database', fDiffOption= '' Where prmBkpTyp = 'F' Union All Select fBkpTyp='Database', fDiffOption= ', DIFFERENTIAL' Where prmBkpTyp = 'D' ) As BkpTyp CROSS APPLY (Select BkpTyp=ISNULL (fBkpTyp, 'Log'), DiffOption=ISNULL(fDiffOption,'')) as TypAndDiffOpt CROSS APPLY ( Select overWrite='Init, Format' Where prmOverwrite = 1 Union All Select overWrite='noInit' Where ISNULL(prmOverwrite,0) <> 1 ) as OverWrite CROSS APPLY (Select EncryptionTemplate = ', ENCRYPTION (ALGORITHM = #EncryptionAlgorithm#, SERVER CERTIFICATE = #EncryptionCertificate#)') As EncryptionTemplate CROSS APPLY (Select EncryptionOpt=IIF(EncryptionAlgorithm<>'' and EncryptionCertificate<>'', EncryptionTemplate, '')) as EncryptionOpt -- produce different name, depending of the caller only 2 possibility, the default is YourSqlDba, the other is a utiliy function of YourSqlDba CROSS APPLY (Select HeaderName=IIF(prmName Like 'SaveDbOnNewFileSet%', 'SaveDbOnNewFileSet','YourSQLDba')) as HeaderName -- include some date information creation in the name CROSS APPLY (Select HrMin = left(convert(varchar(8), getdate(), 108),5)) as HrMin CROSS APPLY (Select HrMinSemiColonToh = Replace (HrMin, ':', 'h')) as HrMinCommaToh CROSS APPLY (Select FileOnly=p.FileName, PathOnly=p.DriveAndPath From S#.ParseFileParts(FileName) as p) as FP CROSS APPLY (Select FullPrefixBkpName=HeaderName+':'+HrMinSemiColonToh+': '+PathOnly) as FullPrefixBkpName CROSS APPLY (Select LgPrefixBkpName=LEN(FullPrefixBkpName), LgFileOnly=Len(FileOnly)) as Lg CROSS APPLY (Select ExcessLen=LgPrefixBkpName+LgFileOnly-128) as ExcessLen CROSS APPLY ( Select BkpName = FullPrefixBkpName+FileOnly Where ExcessLen <= 0 UNION ALL Select BkpName = Left(FullPrefixBkpName,LgPrefixBkpName-ExcessLen-3)+'...'+FileOnly Where ExcessLen>0 ) as BkpName CROSS APPLY ( Select SerializeIfLogBkp=IIF(bkpTyp='log', Ser.TxtInCmt, '') , EndSerializeIfLogBkp=IIF(bkpTyp='log', EndSer.TxtInCmt, '') From S#.GetCmtBetweenDelim('===SerializeIfLogBkp===', Self) as Ser CROSS Apply S#.GetCmtBetweenDelim('===EndSerializeIfLogBkp===', Self) As EndSer /*===SerializeIfLogBkp=== Exec Sp_getapplock @Resource = 'SerializeLogShip_#DbName#', @LockOwner = 'Session', @LockMode = 'Exclusive', @LockTimeout = 60000; -- 60s ===SerializeIfLogBkp===*/ /*===EndSerializeIfLogBkp=== EXEC sp_releaseapplock @Resource = 'SerializeLogShip_#DbName#', @LockOwner = 'Session'; ===EndSerializeIfLogBkp===*/ ) As Ser CROSS APPLY (SELECT jPrms = (SELECT SerializeIfLogBkp, EndSerializeIfLogBkp , BkpTyp,DbName, FileName, BkpName, OverWrite, DiffOption, EncryptionOpt , EncryptionAlgorithm,EncryptionCertificate For JSON PATH, INCLUDE_NULL_VALUES) ) as J CROSS APPLY (Select * From S#.GetTemplateFromCmtAndReplaceTags('===BkpCmdTempl===', Self, jPrms) as C) as Sql ) Internals -- here is the templace for backup commands /*===BkpCmdTempl=== #SerializeIfLogBkp# backup #BkpTyp# [#DbName#] to disk = '#fileName#' with #OverWrite##DiffOption#, checksum, name = '#BkpName#', bufferCount = 20, MAXTRANSFERSIZE = 1048576 #EncryptionOpt# #EndSerializeIfLogBkp# ===BkpCmdTempl===*/ --Select * From yMaint.iTvf_MakeBackupCmd('','','',1,'', '', '') go --Select Cmd.* --From -- Sys.databases -- CROSS APPLY -- yMaint.iTvf_MakeBackupFileName -- (name,'F', '\\SomeServer\SQL2k22\Backups', 'Francais', 'Bak', 1) as Fn -- CROSS APPLY yMaint.iTvf_MakeBackupCmd(name,'F',Fn.filename, 1, NULL, '', '') as Cmd GO -- ------------------------------------------------------------------------------ -- Function that builds backup command -- ------------------------------------------------------------------------------ Create Or Alter Function yMaint.MakeBackupCmd ( @DbName sysname , @bkpTyp Char(1) , @fileName nvarchar(512) , @overwrite Int , @name nvarchar(512) , @EncryptionAlgorithm nvarchar(10) , @EncryptionCertificate nvarchar(100) ) -- @@MARK: Maintenance - Make backup Cmd returns nvarchar(max) as Begin Return ( Select Sql From yMaint.iTvf_MakeBackupCmd( @DbName, @bkpTyp, @fileName, @overwrite, @name, @EncryptionAlgorithm, @EncryptionCertificate) ) End -- yMaint.MakeBackupCmd GO ------------------------------------------------------------------------------------- -- Sp to ensure and or create specific SQL Agent Job for restore to MirrorServer ------------------------------------------------------------------------------------- -- @@MARK: Mirroring - Set restore Job Create Or Alter Procedure Mirroring.HandleRestoreJobAsNecessary @MirrorServer Sysname, @dropJob Int = 0, @ForceRecreateJob Int = 0 As Begin DECLARE @ReturnCode INT = 0 DECLARE @jobId BINARY(16) Declare @DeploymentName sysName Declare @context sysname Select @DeploymentName = 'YourSQLDba_RestoreJob to '+@MirrorServer Select @jobId = job_id From msdb.dbo.sysjobs where name =@DeploymentName Begin Try -- Use case. Mirroring.AddServer call this proc with @ForceRecreateJob=1 to ensure there is a clean job to handle restore -- If there is already one, it drops it. This leads to its recreation. -- Mirroring.DropServer orders the cleanup with @dropJob=1 If (@jobId IS NOT NULL) And (@ForceRecreateJob=1 Or @DropJob = 1) Begin Set @context = 'Removing previous job definition for '+@DeploymentName EXEC @ReturnCode = msdb.dbo.sp_delete_job @job_name=@DeploymentName IF (@ReturnCode <> 0) Raiserror ('Return code %d from msdb.dbo.sp_delete_schedule ',11,1,@returnCode) End -- For a drop server, this ends here If @DropJob=1 -- case of a call by Mirroring.dropServer Return -- Use case. Mirroring.QueueRestoreToMirrorCmd call this proc to ensure there is a job to handle restore -- If there is already one, it exits. -- case for a call with Mirroring.addServer Or Mirroring. -- When Mirroring.StartRestartRestoreJobForMirrorServer called this proc -- this ensures the job is present by recreating it otherwise it skips this step If Exists(Select * From msdb.dbo.sysjobs where name =@DeploymentName) Return BEGIN TRANSACTION; Set @context = 'Add job title' Set @jobId = NULL EXEC @ReturnCode = msdb.dbo.sp_add_job @job_name=@DeploymentName, @enabled=1, @notify_level_eventlog=0, @notify_level_email=0, @notify_level_netsend=0, @notify_level_page=0, @delete_level=0, @description=N'No description available.', @category_name=N'[Uncategorized (Local)]', @owner_login_name=N'sa', @job_id = @jobId OUTPUT IF (@ReturnCode <> 0) Raiserror ('Return code %d from msdb.dbo.sp_add_job ',11,1,@returnCode) Set @context = 'Add job step' EXEC @ReturnCode = msdb.dbo.sp_add_jobstep @job_id=@jobId, @step_name=N'Run', @step_id=1, @cmdexec_success_code=0, @on_success_action=1, @on_success_step_id=0, @on_fail_action=2, @on_fail_step_id=0, @retry_attempts=0, @retry_interval=0, @os_run_priority=0, @subsystem=N'TSQL', @command= N'EXECUTE [Mirroring].[ProcessRestores]', @database_name=N'YourSqlDba', @flags=4 IF (@ReturnCode <> 0) Raiserror ('Return code %d from msdb.dbo.sp_add_job_Step ',11,1,@returnCode) Set @context = 'Set (local) as job server for the job' EXEC @ReturnCode = msdb.dbo.sp_add_jobserver @job_id = @jobId, @server_name = N'(local)' IF (@ReturnCode <> 0) Raiserror ('Return code %d from msdb.dbo.sp_add_jobserver ',11,1,@returnCode) Declare @operator sysname Select @operator=ISNULL(oper, N'YourSqlDba_Operator') From MainContextInfo (null) EXEC msdb.dbo.sp_update_job @job_id=@jobId, @notify_level_email=2, @notify_level_page=2, @notify_email_operator_name=@operator COMMIT End Try Begin catch Declare @msg nvarchar(max) Select @msg = @context + nChar(10)+F.ErrMsg From S#.FormatCurrentMsg(NULL) as F Print 'Error When running Mirroring.HandleRestoreJobAsNecessary : '+@msg ROLLBACK End catch End -- Mirroring.HandleRestoreJobAsNecessary GO -- ---------------------------------------------------------------------------------------------- -- Start_Restart Restore job for mirrorServer -- ---------------------------------------------------------------------------------------------- -- @@MARK: Mirroring - Start restore job Create or Alter Proc Mirroring.StartRestartRestoreJobForMirrorServer @MirrorServer Sysname as Begin Exec Mirroring.HandleRestoreJobAsNecessary @MirrorServer=@MirrorServer Drop Table if Exists #PrmStartRestartRestoreJobForMirrorServer Select MirrorServer, JobName, Job_Id Into #PrmStartRestartRestoreJobForMirrorServer From -- (Select MirrorServer='MauriceSql\Maint19') as MirrorServer (Select MirrorServer=@MirrorServer) as MirrorServer Cross Apply (Select JobName='YourSQLDba_RestoreJob to '+MirrorServer) as JobName Outer Apply (Select Job_id=job_id FROM msdb.dbo.sysjobs WHERE name = JobName) as JobId If EXISTS -- job already running? then quit ( SELECT * From (Select MaxSessionId = MAX(session_id) FROM msdb.dbo.syssessions) as MaxSessionId CROSS JOIN (Select NotRunning=0) as NotRunning CROSS JOIN #PrmStartRestartRestoreJobForMirrorServer OUTER APPLY ( SELECT Running=1 FROM msdb.dbo.sysjobactivity AS ja WHERE ja.job_id = #PrmStartRestartRestoreJobForMirrorServer.job_id AND ja.session_id = MaxSessionId AND ja.start_execution_date IS NOT NULL AND ja.stop_execution_date IS NULL ) as Running CROSS APPLY (Select isRunning=COALESCE(Running, NotRunning)) as isRunning Where isRunning = 1 ) Return BEGIN TRY Waitfor Delay '00:00:05' -- Wait 5 seconds to start Declare @jobName Sysname = (Select JobName From #PrmStartRestartRestoreJobForMirrorServer) EXEC msdb.dbo.sp_start_job @job_name = @JobName; PRINT 'Job started: ' + @JobName; Waitfor Delay '00:00:30' -- Wait 30 seconds to ensure job state reports correctly in msdb.dbo.sysjobactivity END TRY BEGIN CATCH IF ERROR_NUMBER() = 14262 RAISERROR('SQL Server Agent is not running.', 16, 1); ELSE THROW; END CATCH End -- Mirroring.StartRestartRestoreJobForMirrorServer GO CREATE OR ALTER PROCEDURE dbo.LogEvent @MsgTemplate nvarchar(2048) , @JsonPrms Nvarchar(max) , @Severity varchar(20) = 'informational' -- 'informational', 'warning', 'error' AS BEGIN SET NOCOUNT ON; -- Validation rapide du niveau IF @Severity NOT IN ('informational', 'warning', 'error') BEGIN SET @Severity = 'Informational'; END; -- Préfixe facultatif (source) DECLARE @FullMessage nvarchar(2048); Select @FullMessage = FullMsg From S#.MultipleReplaces (@MsgTemplate, @JSonPrms) as R CROSS APPLY (Select FullMsg='[YourSqlDba]: '+R.replacedTxt) as FullMsg -- Appel silencieux à xp_logevent (écrit dans SQL + Windows) BEGIN TRY EXEC master..xp_logevent 50001, @FullMessage, @Severity; END TRY BEGIN CATCH RETURN; END CATCH; END; GO -- @@MARK: Mirroring - Queue restore Create Or Alter Procedure yMirroring.QueueRestoreToMirror @context nvarchar(4000) = '' , @DbName sysname , @bkpTyp Char(1) , @fileName nvarchar(512) , @MirrorServer sysname , @MigrationTestMode Int -- behaves differently at restore... See yMirroring.DoRestore , @ReplaceSrcBkpPathToMatchingMirrorPath nvarchar(max) = '' , @ReplacePathsInDbFilenames nvarchar(max) = '' as -- --------------------------------------------------------------------------------------------- -- Stores backup/restore parameters into Mirroring.RestoreQueue -- Call Exec Mirroring.StartRestartRestoreJobForMirrorServer to ensure -- that the SQL Agent Job that execute Mirroring.ProcessRestores is -- there and started -- --------------------------------------------------------------------------------------------- Begin -- If the mirror server is disabled or this is a system database then return -- easier to trace in profiler if written this way If isnull(@MirrorServer, '') = '' Return(0) If @DbName in ('master', 'model', 'msdb', 'tempdb', 'YourSQLDba') Return( 0 ) -- Test that the Mirror server was defined Declare @sql nvarchar(max) Declare @Info nvarchar(max) Declare @err nvarchar(max) If Not Exists (Select * From Mirroring.TargetServer Where MirrorServerName = @MirrorServer) Begin Set @err = 'Mirror server «' + @MirrorServer + '» not defined. Use stored procedure «Mirroring.AddServer»' Exec yExecNLog.LogAndOrExec @context = 'yMirroring.QueueRestoreToMirror' , @Info = 'Error at launch restore to mirror server' , @YourSqlDbaNo = '008' , @Err = @Err Return( 0 ) End -- CallingJobNo is required to allow to the procedure that does restores and that run under -- a SQL Agent Job with a different connection to set the same session context jobNo -- It then gives access to many parameters stored for this job into Maint.JobHistory, through the jobNo of this session context -- for a more detailed explanation, see Mirroring.ProcessRestores. Insert into Mirroring.RestoreQueue (CallingJobNo, JsonPrms) Select M.JobNo, J.JsonPrms From dbo.MainContextInfo(null) as M -- get JobNo from current context that asks for a restore CROSS APPLY (Select JsonPrms= ( Select M.JobNo , DbName = @DbName , bkpTyp = @bkpTyp , fileName = @fileName , M.MirrorServer , M.MigrationTestMode , M.ReplaceSrcBkpPathToMatchingMirrorPath , M.ReplacePathsInDbFilenames For Json Path, INCLUDE_NULL_VALUES ) ) as j Declare @jSonPrms Nvarchar(2048) Select @JsonPrms=JsonPrms From Mirroring.RestoreQueue Where RestoreSeq=SCOPE_IDENTITY() Exec Dbo.LogEvent @MsgTemplate = 'JobNo:#JobNo# - Queing Database backup (#bkpTyp#) #DbName# for restore at server #MirrorServer#' , @JsonPrms=@JsonPrms Set @Info = 'Restore to mirror server sent to Restore Queue:' + @sql Exec yExecNLog.LogAndOrExec @yourSqlDbaNo='020' , @context='yMirroring.QueueRestoreToMirror' , @Info = @info Exec Mirroring.StartRestartRestoreJobForMirrorServer @MirrorServer End -- yMirroring.QueueRestoreToMirror GO -- @@MARK: Mirroring - Replicate logins - cleanup before Create Or Alter Procedure Maint.DropOrphanLogins as begin create table #logins (name sysname primary key clustered) declare @sql nvarchar(max) Select @sql = ( select convert(nvarchar(max), '') + 'select suser_sname(sid) from ['+name+'].sys.database_principals where suser_sname(sid) is not null union '+nchar(10) as [text()] From sys.databases for XML path('') )+ 'Select '''' as name' insert into #logins Exec(@sql) Select @sql = ( Select convert(nvarchar(max), '') + 'drop login ['+sp.name+']'+nchar(10) as [text()] from sys.server_principals SP left join #logins L ON SP.name = L.Name Where type_desc = 'SQL_LOGIN' and L.name is null for XML path('') ) print @sql Exec(@sql) End go -- ------------------------------------------------------------------------------ -- Procedure to delete old backup files selected by all the following conditions: -- -- 1. The files must be in the files path @path. -- (subdirectories are not selected). -- -- 2. The files name must contain the date and time of its creation. -- The format is like -- '%[_][[][0-9][0-9][0-9][0-9]-[0-9][0-9]-[0-9][0-9][_][0-9][0-9]h[0-9][0-9]m[0-9][0-9][_]___][_]%' -- Example: AdventureWorks_[2009-04-27_00h06m53_Mon]_database.Bak -- and -- The files name must end with the optional @extension. -- -- 3. ( @BkpRetDays is not NULL -- and -- The beginning of the file name is in the selected database list in the -- temporary table @tDb -- and -- AgeInMinutes > (@BkpRetDays * 1440) -- AgeInMinutes is the age of the file in minutes -- ) -- Or -- ( @BkpRetDaysForUnSelectedDb is not NULL -- and -- The file was not selected by @tDb -- and -- AgeInMinutes > (@BkpRetDaysForUnSelectedDb * 1440) -- AgeInMinutes is the age of the file in minutes -- ) -- -- In all cases, the msdb database file backup is always deleted by -- the Maint.DeleteOldBackups procedure when @extension = .bak -- ------------------------------------------------------------------------------ -- @@MARK: Maintenance - Old backup cleanup Create Or Alter Procedure Maint.DeleteOldBackups @oper nvarchar(200) = 'YourSQLDba_Operator' , @path nVARCHAR(max) -- Path to the files , @BkpRetDays Int = NULL -- Number of days to keep the backup files -- selected by there database name. -- by default no cleanup. , @BkpRetDaysForUnSelectedDb int = NULL -- Optional number of days to keep the backup -- files not selected by there database name. -- by default no cleanup. , @RefDate Datetime = NULL -- Optional reference date and time for the clean up -- Format: '20090925 18:00' -- yyyymmdd hh:mm , @extension sysname = '' -- Optional file extention -- any file extension of any length is accepted -- Examples: .bak for full backups -- or .trn for log backups -- or '' for all files in the @path , @IncDb nVARCHAR(max) = '' , @ExcDb nVARCHAR(max) = '' , @SendOnErrorOnly int = 1 -- 1 = send an email only when there is an error , @DeleteOnlyLogDiffBackups int = 0 -- 1 = delete only logBackups and differential backups as Begin Set NoCount On Declare @Info nvarchar(max) Declare @FullFilePath nvarchar(max) declare @err nvarchar(max) If Right(@path, 1) <> '\' Set @path = @path + '\' Declare @StartOfCleanup datetime set @StartOfCleanup = getdate() If @RefDate is NULL Set @RefDate = convert(datetime, getdate(), 120) -- Maint.DeleteOldBackups is mostly called from a YourSqlDba_DoMaint -- so we should have a context to init some parameters Select @oper = Ctx.oper , @IncDb = CTX.IncDb , @ExcDb = CTX.ExcDb , @RefDate = Ctx.JobStart From dbo.MainContextInfo (null) as Ctx -- if we don't have a context, set on to create a new job entry in the job history table to log this standalone delete action If @@ROWCOUNT=0 Begin Declare @Sql Nvarchar(max) Select @sql=S.Sql From (Select oper = @oper , MaintJobName = 'Maint.DeleteOldBackups' , jobStart = @RefDate ) as Prm Cross Apply ( Select JsonPrm= (Select oper, MaintJobName='DeleteOldBackups', jobStart For JSON PATH, WITHOUT_ARRAY_WRAPPER ) ) as JsonPrm CROSS APPLY dbo.ScriptSetGlobalAccessToPrm (JsonPrm) as S Exec (@Sql) -- Execute SQL generated by previous query End Exec yExecNLog.LogAndOrExec @context = 'Maint.DeleteOldBackups' , @Info = 'Start of backup cleanup' -- Create a table of the databases selected declare @tDb table ( DbName sysname collate database_default primary key clustered , DbOwner sysname NULL -- because actual owner may be invalid after a restore , FullrecoveryMode int -- If = 1 log backup allowed , cmptLevel tinyInt ) insert into @tDb SELECT * FROM YourSQLDba.yUtl.YourSQLDba_ApplyFilterDb (@IncDb, @ExcDb) Where DatabasepropertyEx(DbName, 'Status') = 'Online' -- Avoid db that can't be processed --select * from @tDb -- remove snapshot database from the list Delete Db From @tDb Db Where Exists ( Select * From sys.databases d Where d.name COLLATE Database_default = db.DbName and source_database_Id is not null ) --select * from @tDb -- create table of directory info lines declare @FilesFromFolder table ( line nvarchar(1000) collate database_default) If LEFT(@extension,1)<> '.' Set @extension = '.'+@extension Insert into @FilesFromFolder Select FileName from S#.Clr_GetFolderListDetailed (@path, '*'+@extension) If Exists(Select * from @FilesFromFolder Where line = '') Begin Set @err = ( Select CONVERT(nvarchar(max),'')+Line+NCHAR(10) as [text()] From @FilesFromFolder Where line <> '' for XML PATH('') ) Exec yExecNLog.LogAndOrExec @context = 'Maint.DeleteOldBackups' , @err = @err Return End --SELECT * --From @FilesFromFolder declare @dbFiles table ( Seq int primary key clustered , DbName sysname null , FileName nvarchar(max) null , Creation_Date nvarchar(23) null , RefDate nvarchar(23) null , AgeInMinutes Int null ) ;With FilesReturnedBy_Clr_GetFolderList as ( SELECT ROW_NUMBER() OVER (ORDER BY d.line) As Seq , ltrim(rtrim(d.line)) as FileName , patindex ('%[_][[][0-9][0-9][0-9][0-9]-[0-9][0-9]-[0-9][0-9][_][0-9][0-9]h[0-9][0-9]m[0-9][0-9][_]___][_]%' , d.line) as posPatternDate , patindex ('MSDB_[[]%', d.line) as PosMsdb FROM @FilesFromFolder as d ) , T1 as ( select seq , FileName , Case When PosMsDb > 0 Then Substring(FileName, posMsdb+6, 10) + ' 00:00' Else Substring(FileName, posPatternDate+2, 16) End as DateCreate From FilesReturnedBy_Clr_GetFolderList Where PosMsDb > 0 Or PosPatternDate > 0 ) , T2 as (select seq, FileName, replace(DateCreate, 'h', ':') as DateCreate From T1) , T3 as (select seq, FileName, replace(DateCreate, '_', ' ') as DateCreate From T2) , T4 as ( select Distinct -- distinct helped to circumvent a funny run-time error seq , FileName , convert(datetime, DateCreate, 121) as Creation_Date , @RefDate As RefDate -- There is 1440 minutes per day , datediff(mi, convert(datetime, DateCreate, 121), @RefDate) As AgeInMinutes From T3 ) Insert into @dbFiles Select F.Seq, db.DbName, F.FileName, F.Creation_Date, F.RefDate, F.AgeInMinutes From T4 as f Left Join @tDb as db On (db.DbName + '_[') = (Substring(f.FileName, 1, len(db.DbName) + 2 )) Where ( (@BkpRetDays is not NULL) and (f.AgeInMinutes > (@BkpRetDays * 1440)) and (db.DbName Is Not Null) -- The file was selected by @tDb and ((@DeleteOnlyLogDiffBackups = 1 and F.FileName like '%_logs%') or (@DeleteOnlyLogDiffBackups = 1 and F.FileName like '%_differential%') or @DeleteOnlyLogDiffBackups = 0) ) Or (Substring(f.FileName, 1, 6) = 'MSDB_[') -- Always delete old backups from MSDB Or ( (@BkpRetDaysForUnSelectedDb is not NULL) -- Delete files not seleted by @tDb and (db.DbName Is Null) -- The file was not selected by @tDb and (f.AgeInMinutes > (@BkpRetDaysForUnSelectedDb * 1440) )) --SELECT * --, (@BkpRetDays * 1440) as 'BkpRetDays in minutes' --, (@BkpRetDaysForUnSelectedDb * 1440) as 'BkpRetDaysForUnSelectedDb in minutes' --From @DbFiles --Order by FileName Declare @Cmd nvarchar(1000) declare @filename nvarchar(max) declare @dbName sysname declare @SeqFile int declare @context nvarchar(max) Set @SeqFile = 0 Set @filename = '' While (1=1) Begin Select top 1 @filename = FileName, @SeqFile = seq, @dbName = dbName From @DbFiles Where Seq > @SeqFile Order by seq If @@rowcount = 0 Break Set @FullFilePath = @path+@Filename Exec S#.Clr_DeleteFile @FullFilePath, @Err output If @err <> '' -- If file is not found no error is generated Begin Exec yExecNLog.LogAndOrExec @context = 'Maint.DeleteOldBackups' , @err = @err end Else Begin Set @FullFilePath = @FullFilePath + ' deleted' Exec yExecNLog.LogAndOrExec @context = 'Maint.DeleteOldBackups' , @info = @FullFilePath End End -- While -- From here, send execution report and any error message if found -- If the operator is missing, emit an error message -- and exit now to put error status in the SQL Agent job. Declare @email_Address sysname -- to read email address of the operator select @email_Address = email_Address from Msdb..sysoperators where name = @oper and enabled = 1 If @@rowcount = 0 -- here the error is loggued into job Exec yExecNLog.LogAndOrExec @context = 'Maint.DeleteOldBackups' , @YourSqlDbaNo = '009' , @Info = 'Maint.DeleteOldBackups' , @err = ' The operator name supplied to the procedure Maint.DeleteOldBackups, must exist and be enabled in msdb..sysoperators ' Exec yExecNLog.LogAndOrExec @context = 'Maint.DeleteOldBackups' , @Info = 'End of backup cleanup' Declare @maintJobName sysname Select @MaintJobName=MaintJobName From dbo.MainContextInfo(null) If @email_Address is NOT NULL -- Do not perform message report if caller is Maint.YourSqlDba_DoMaint because the caller will do And Not Exists (Select * From Dbo.MainContextInfo(NULL) as Ctx Where Ctx.MainSqlCmd Not Like '%Exec %Maint.YourSqlDba_DoMaint$') Exec yMaint.SendExecReports @email_Address = @email_Address , @MaintJobName = @MaintJobName , @StartOfMaint = @StartOfCleanup , @SendOnErrorOnly = @SendOnErrorOnly -- 1 = Send email only when there is a error End -- Maint.DeleteOldBackups GO -- @@MARK: Mirroring - Replicate on login to mirror Create Or Alter Procedure yMirroring.MirrorLoginSync @servername sysname , @loginname sysname , @type char(1) , @password_hash varbinary(256) , @sid varbinary(85) , @policy_checked nvarchar(3) , @expiration_checked nvarchar(3) , @deflanguage sysname , @sysadmin int , @securityadmin int , @serveradmin int , @setupadmin int , @processadmin int , @diskadmin int , @dbcreator int , @bulkadmin int , @jobNo int = NULL , @is_disabled int = 0 As Begin declare @sql nvarchar(max) declare @loginExists int declare @password_hash_local varbinary(256) declare @sid_local varbinary(85) declare @is_disabled_local int Set @loginExists = 0 -- Update local domain on new server if needed before trying a select/drop/alter login If @type IN ('G', 'U') and left(@loginname, len(@servername+'\')) = @servername+'\' Set @loginname = Replace(@loginname, @servername+'\', convert(sysname, serverproperty('machinename'))+'\') Select @loginExists = 1 , @password_hash_local = sl.password_hash , @sid_local = sp.sid , @is_disabled_local = sp.is_disabled From sys.server_principals sp Left join sys.sql_logins sl on sp.name = sl.name Where sp.name = @loginname -- If Login is the same with same SID, same password and same disabled values, we can skip this one If @loginExists = 1 AND @sid = @sid_local AND IsNull(@password_hash, 0x) = IsNull(@password_hash_local, 0x) AND @is_disabled_local = @is_disabled Return(0) Begin Try Set @sql = '' -- If login already exists and needs a sid or password update, we drop it so we can recreate it with good password and good sid If @loginExists = 1 AND ( @sid <> @sid_local Or IsNull(@password_hash, 0x) <> IsNull(@password_hash_local, 0x)) Begin Set @sql = @sql + 'DROP LOGIN [];' Set @loginExists = 0 End -- If login needs to be created If @loginExists = 0 Begin If @type IN ('G', 'U') Set @sql = @sql + 'CREATE LOGIN [] FROM WINDOWS WITH DEFAULT_LANGUAGE=;' Else Set @sql = @sql + ' CREATE LOGIN [] WITH PASSWORD= HASHED , SID= , CHECK_POLICY= , CHECK_EXPIRATION=, DEFAULT_LANGUAGE=; ' End -- If login has been disabled, disable it If @is_disabled_local = 0 AND @is_disabled = 1 Set @sql = @sql +'ALTER Login [] DISABLE;' -- If login has been enabled, enable it If @is_disabled_local = 1 AND @is_disabled = 0 Set @sql = @sql +'ALTER Login [] ENABLE;' If @sysadmin = 1 Set @sql = @sql + 'EXEC sp_addsrvrolemember "", "sysadmin";' If @securityadmin = 1 Set @sql = @sql + 'EXEC sp_addsrvrolemember "", "securityadmin";' If @serveradmin = 1 Set @sql = @sql + 'EXEC sp_addsrvrolemember "", "serveradmin";' If @setupadmin = 1 Set @sql = @sql + 'EXEC sp_addsrvrolemember "", "setupadmin";' If @processadmin = 1 Set @sql = @sql + 'EXEC sp_addsrvrolemember "", "processadmin";' If @diskadmin = 1 Set @sql = @sql + 'EXEC sp_addsrvrolemember "", "diskadmin";' If @dbcreator = 1 Set @sql = @sql + 'EXEC sp_addsrvrolemember "", "dbcreator";' If @bulkadmin = 1 Set @sql = @sql + 'EXEC sp_addsrvrolemember "", "bulkadmin";' Set @sql = REPLACE(@sql, '', @loginname) Set @sql = REPLACE(@sql, '', Convert(nvarchar(max),@password_hash,1)) Set @sql = REPLACE(@sql, '', Convert(nvarchar(max),@sid,1)) Set @sql = REPLACE(@sql, '', @policy_checked) Set @sql = REPLACE(@sql, '', @expiration_checked) If @defLanguage is Not NULL Set @sql = REPLACE(@sql, '', @deflanguage) Else Begin Set @sql = REPLACE(@sql, ' WITH DEFAULT_LANGUAGE=', '') Set @sql = REPLACE(@sql, ', DEFAULT_LANGUAGE=', '') End Set @sql = REPLACE(@sql, '"', '''') Exec yExecNLog.LogAndOrExec @context = 'yMirroring.MirrorLoginSync' , @sql = @sql , @Info = 'Synchronizing accounts to mirror Server ' End Try Begin Catch End Catch End --yMirroring.MirrorLoginSync GO -- @@MARK: Mirroring - Replicate logins to mirror Create Or Alter Procedure yMirroring.LaunchLoginSync As Begin declare @MirrorServerName sysname declare @sql nvarchar(max) declare @servername sysname declare @loginname sysname declare @type char(1) declare @password_hash varbinary(256) declare @sid varbinary(85) declare @policy_checked nvarchar(3) declare @expiration_checked nvarchar(3) declare @deflanguage sysname declare @sysadmin int declare @securityadmin int declare @serveradmin int declare @setupadmin int declare @processadmin int declare @diskadmin int declare @dbcreator int declare @bulkadmin int declare @is_disabled int declare @mirrorServer sysname Select @MirrorServer=MirrorServer From Dbo.MainContextInfo(null) Set NoCount On SELECT p.name , Convert(sysname, serverproperty('machinename')) as servername , p.type , IsNull(sl.password_hash, 0x) As password_hash , p.sid , Case When sl.is_policy_checked = 1 Then 'ON' Else 'OFF' End As is_policy_checked , Case When sl.is_expiration_checked = 1 Then 'ON' Else 'OFF' End As is_expiration_checked , p.default_language_name , l.sysadmin , l.securityadmin , l.serveradmin , l.setupadmin , l.processadmin , l.diskadmin , l.dbcreator , l.bulkadmin , p.is_disabled INTO #Logins FROM sys.server_principals p LEFT JOIN sys.sql_logins sl ON sl.name = p.name Left Join sys.syslogins l on l.sid = p.sid WHERE p.type IN ( 'S', 'G', 'U' ) AND p.name <> 'YourSQLDba' AND p.name <> 'SA' AND p.name Not Like 'AUTORITE NT\%' AND p.name Not Like 'NT AUTHORITY\%' AND p.name Not Like 'NT SERVICE\%' AND p.name Not Like 'BUILTIN\Administra%' AND p.name Not Like '##Ms[_]Policy%##' CREATE UNIQUE CLUSTERED INDEX Logins_P ON #Logins (name) Set @MirrorServerName = '' While 1= 1 Begin Select Top 1 @MirrorServerName=MirrorServerName From Mirroring.TargetServer Where MirrorServerName > @MirrorServerName AND MirrorServerName = @MirrorServer If @@rowcount = 0 break Set @loginname = '' While 1=1 Begin Select Top 1 @loginname = name , @servername = servername , @type = type , @password_hash = password_hash , @sid = sid , @policy_checked = is_policy_checked , @expiration_checked = is_expiration_checked , @deflanguage = default_language_name , @sysadmin = sysadmin , @securityadmin = securityadmin , @serveradmin = serveradmin , @setupadmin = setupadmin , @processadmin = processadmin , @diskadmin = diskadmin , @dbcreator = dbcreator , @bulkadmin = bulkadmin , @is_disabled = is_disabled From #Logins Where name > @loginname Order by name If @@rowcount = 0 break Set @sql = 'Exec [].YourSqlDba.yMirroring.MirrorLoginSync @servername = "", @loginname = "", @type = "", @password_hash = , @sid = , @policy_checked = "", @expiration_checked = "", @deflanguage = "", @sysadmin = , @securityadmin = , @serveradmin = , @setupadmin = , @processadmin = , @diskadmin = , @dbcreator = , @bulkadmin = , @jobNo = NULL, @is_disabled=' Set @sql = yExecNLog.Unindent_TSQL(@sql) Set @sql = REPLACE(@sql, '', @MirrorServerName) Set @sql = REPLACE(@sql, '', @servername) Set @sql = REPLACE(@sql, '', @loginname) Set @sql = REPLACE(@sql, '', @type) Set @sql = REPLACE(@sql, '', Convert(nvarchar(max),@password_hash,1)) Set @sql = REPLACE(@sql, '', Convert(nvarchar(max),@sid,1)) Set @sql = REPLACE(@sql, '', @policy_checked) Set @sql = REPLACE(@sql, '', @expiration_checked) Set @sql = REPLACE(@sql, '', @deflanguage) Set @sql = REPLACE(@sql, '', @sysadmin) Set @sql = REPLACE(@sql, '', @securityadmin) Set @sql = REPLACE(@sql, '', @serveradmin) Set @sql = REPLACE(@sql, '', @setupadmin) Set @sql = REPLACE(@sql, '', @processadmin) Set @sql = REPLACE(@sql, '', @diskadmin) Set @sql = REPLACE(@sql, '', @dbcreator) Set @sql = REPLACE(@sql, '', @bulkadmin) Set @sql = REPLACE(@sql, '',@is_disabled) Set @sql = REPLACE(@sql, '"', '''') Declare @Info nvarchar(max) Set @Info = 'Synchronizing account: "' + @loginname+'"' Exec yExecNLog.LogAndOrExec @context = 'yMirroring.LaunchLoginSync' , @Info = @Info , @sql = @sql End -- for each login End -- for each server End -- yMirroring.LaunchLoginSync GO -- @@MARK: Mirroring - Diag matching versions Create Or Alter Procedure yMirroring.ReportYourSqlDbaVersionOnTargetServers @MirrorServer sysname , @LogToHistory int = 1 , @silent int = 0 , @remoteVersion nvarchar(100) = NULL OUTPUT As Begin set nocount on Declare @sql nvarchar(max) Declare @err nvarchar(max) Declare @Info nvarchar(max) -- Ensure that target servers are still in sys.servers, otherwise remove them If not exists ( select * from sys.servers S Where S.name = @MirrorServer collate database_default And S.is_linked = 1 ) Begin Set @remoteVersion = 'Server undefined' Set @Err = 'No linked server is defined under the name: ['+@MirrorServer+']' If @silent = 0 Print @Info If @LogToHistory = 1 And @silent = 0 Begin Exec yExecNLog.LogAndOrExec @context = 'yMirroring.ReportYourSqlDbaVersionOnTargetServers' , @YourSqlDbaNo = '021' , @Err = @err , @raiseError = 0 End Return -- don't go further End -- Check if YourSQLDba is installed at remote Begin try Set @sql = ' Declare @Exists Int Set @RemoteVersionInfo="" Select @exists = Dbid from Openquery ([], "select Db_Id(""YourSQLDba"") as DbId") as x If @Exists Is NULL Set @RemoteVersionInfo = "Remote YourSqlDba is missing" ' Set @sql = REPLACE( @sql, '', @MirrorServer) Set @sql = REPLACE( @sql, '"', '''') --Print @sql Exec sp_executeSql @sql, N'@LogToHistory int = 1, @remoteVersionInfo nvarchar(100) Output', @LogToHistory = @LogToHistory, @remoteVersionInfo = @remoteVersion Output If @RemoteVersion = 'Remote YourSqlDba is missing' Begin Set @err = 'YourSQLDba must be installed on ['+@MirrorServer+'] for mirroring purpose. Run YourSQLDba_InstallOrUpdateScript.sql on this server.' If @silent = 0 Print @Info If @LogToHistory = 1 And @silent = 0 Begin Exec yExecNLog.LogAndOrExec @context = 'yMirroring.ReportYourSqlDbaVersionOnTargetServers' , @YourSqlDbaNo = '021' , @Err = @Err , @raiseError = 0 End Return -- don't go further End End try Begin catch If ERROR_NUMBER () = 7416 Print 'Access to the remote server is denied because no login-mapping exists.' set @remoteVersion = 'no remote mapping exists' return End catch Set @sql = ' Declare @Exists Int Set @RemoteVersionInfo="" Select @exists = Objectid from Openquery ([], "select OBJECT_ID(""YourSQLDba.Install.versioninfo"") as ObjectId") as x If @exists IS NULL Set @RemoteVersionInfo = "Version before Install.VersionInfo" ' Set @sql = REPLACE( @sql, '', @MirrorServer) Set @sql = REPLACE( @sql, '"', '''') --Print @sql Exec sp_executeSql @sql, N'@LogToHistory int = 1, @remoteVersionInfo nvarchar(100) Output', @LogToHistory = @LogToHistory, @remoteVersionInfo = @remoteVersion Output If @RemoteVersion = 'Version before Install.VersionInfo' Begin Set @err = 'Versions of YourSQLDba on [' + @@servername + '] And ['+@MirrorServer+'] need to be the same for mirroring purpose. Re-run YourSQLDba_InstallOrUpdateScript.sql on both servers.' If @silent = 0 Print @Info If @LogToHistory = 1 And @silent = 0 Begin Exec yExecNLog.LogAndOrExec @context = 'yMirroring.ReportYourSqlDbaVersionOnTargetServers' , @YourSqlDbaNo = '021' , @err = @err , @raiseError = 1 End Return -- don't go further End Set @sql = ' Declare @Exists Int Set @RemoteVersionInfo="" Select @RemoteVersionInfo = versionNumber from Openquery ([], "select versionNumber From YourSQLDba.Install.VersionInfo() F") as x ' Set @sql = REPLACE( @sql, '', @MirrorServer) Set @sql = REPLACE( @sql, '"', '''') --Print @sql Exec sp_executeSql @sql, N'@LogToHistory int = 1, @remoteVersionInfo nvarchar(100) Output', @LogToHistory = @LogToHistory, @remoteVersionInfo = @remoteVersion Output If (Select versionNumber From YourSQLDba.Install.VersionInfo()) <> @RemoteVersion Begin Set @err = 'Versions of YourSQLDba on [' + @@servername + '] And ['+@MirrorServer+'] need to be the same for mirroring purpose. Re-run YourSQLDba_InstallOrUpdateScript.sql on both servers.' If @silent = 0 Print @Info If @LogToHistory = 1 And @silent = 0 Begin Exec yExecNLog.LogAndOrExec @context = 'yMirroring.ReportYourSqlDbaVersionOnTargetServers' , @YourSqlDbaNo = '021' , @err = @err , @raiseError = 1 End End End go --declare @DbName sysname, @DoBackup char(1), @FullBackupPath nvarchar(512), @overwrite int --Select @DbName = 'LeDbName', @DoBackup = 'L', @FullBackupPath = 'c:\unedestin\', @overwrite = 1 --Select yMaint.MakeBackupCmd (@DbName, @DoBackup, @FullBackupPath, @overwrite) --GO -- ------------------------------------------------------------------------------ -- Proc for doing backup. MUST BE CALLED from YourSqlDba_DoMaint because -- many parameters are passed through a context -- ------------------------------------------------------------------------------ -- @@MARK: Maintenance - Process backups Create Or Alter Proc yMaint.Backups as Begin Set nocount On declare @JobNo Int declare @Info nvarchar(max) declare @DbName sysname Declare @filename nvarchar(512) declare @sql nvarchar(max) -- Sql Command Declare @sql2 nvarchar(max) Declare @FullRecoveryMode Int -- recovery mode of the database Declare @seq Int -- row seq. in work tables Declare @ctx sysname -- context id Declare @email_Address sysname declare @d datetime -- start hour --declare @StartOfDay Datetime declare @lockResult Int declare @errorN Int -- return code for full backups declare @errorN_BkpPartielInit Int -- return code for log backups declare @FailedBkpCnt Int -- failed backups count on a given database Declare @MustLogBackupToShrink int Declare @MaintJobName nVarchar(200) Declare @DoBackup nvarchar(5) Declare @DoFullBkp nvarchar(5) Declare @DoDiffBkp nvarchar(5) Declare @DoLogBkp nvarchar(5) Declare @TimeStampNamingForBackups Int Declare @FullBkpRetDays Int Declare @LogBkpRetDays Int Declare @NotifyMandatoryFullDbBkpBeforeLogBkp int Declare @BkpLogsOnSameFile int Declare @SpreadUpdStatRun int Declare @SpreadCheckDb int Declare @FullBackupPath nvarchar(512) Declare @LogBackupPath nvarchar(512) Declare @ConsecutiveDaysOfFailedBackupsToPutDbOffline Int Declare @IncDb nVARCHAR(max) Declare @ExcDb nVARCHAR(max) Declare @JobId uniqueidentifier Declare @StepId Int Declare @Language nvarchar(512) Declare @jobStart Datetime Declare @MirrorServer sysname Declare @MigrationTestMode Int Declare @FullBkExt nvarchar(7) Declare @LogBkExt nvarchar(7) Declare @err nvarchar(max) Declare @msg nvarchar(max) Declare @ReplaceSrcBkpPathToMatchingMirrorPath nvarchar(max) Declare @ReplacePathsInDbFilenames nvarchar(max) Declare @EncryptionAlgorithm nvarchar(10) = '' Declare @EncryptionCertificate nvarchar(100) = '' -- enrich @context or @info paramter Declare @ContextData nvarchar(max) Declare @InfoData nvarchar(max) -- replace null by empty string Set @ReplaceSrcBkpPathToMatchingMirrorPath = ISNULL(@ReplaceSrcBkpPathToMatchingMirrorPath, '') Set @ReplacePathsInDbFilenames = ISNULL(@ReplacePathsInDbFilenames , '') create table #MustLogBackupToShrink (i int) Declare @DbTable table (dbname sysname, FullRecoveryMode int) Insert into @Dbtable select dbname, FullRecoveryMode from #Db Select @JobNo = JobNo , @MaintJobName = MaintJobName , @DoFullBkp = DoFullBkp , @DoLogBkp = DoLogBkp , @DoDiffBkp = DoDiffBkp , @FullBackupPath = yUtl.NormalizePath(FullBackupPath ) , @LogBackupPath = yUtl.NormalizePath(LogBackupPath ) , @FullBkpRetDays = FullBkpRetDays , @TimeStampNamingForBackups = TimeStampNamingForBackups , @LogBkpRetDays = LogBkpRetDays , @NotifyMandatoryFullDbBkpBeforeLogBkp = NotifyMandatoryFullDbBkpBeforeLogBkp , @BkpLogsOnSameFile = BkpLogsOnSameFile , @FullBkExt = FullBkExt , @LogBkExt = LogBkExt , @ConsecutiveDaysOfFailedBackupsToPutDbOffline = ConsecutiveDaysOfFailedBackupsToPutDbOffline , @IncDb = IncDb , @ExcDb = ExcDb , @jobStart = JobStart , @MirrorServer = MirrorServer , @MigrationTestMode = MigrationTestMode , @JobId = JobId , @StepId = StepId , @MirrorServer = MirrorServer , @ReplaceSrcBkpPathToMatchingMirrorPath = ReplaceSrcBkpPathToMatchingMirrorPath , @ReplacePathsInDbFilenames= ReplacePathsInDbFilenames , @EncryptionAlgorithm = EncryptionAlgorithm , @EncryptionCertificate = EncryptionCertificate From dbo.MainContextInfo (NULL) -- for now, do this with vars, but this function is enough to get param vals If ISNULL(@EncryptionAlgorithm, '') <> '' AND ISNULL(@EncryptionCertificate, '') <> '' Begin -- backups on the same file are not supported for encrypted backups Set @BkpLogsOnSameFile=0 End -- check if MirrorServer is still valid If ISNULL(@MirrorServer, '') <> '' Begin Declare @remoteVersion nvarchar(100) Exec yMirroring.ReportYourSqlDbaVersionOnTargetServers @MirrorServer = @MirrorServer, @LogToHistory = 1, @remoteVersion = @remoteVersion OUTPUT If (select VersionNumber from Install.VersionInfo()) <> @remoteVersion Begin Set @MirrorServer = '' -- this disable restore to remote server end End -- clean-up entries for now inexistent (removed databases) Delete LB From Maint.JobLastBkpLocations LB LEFT JOIN master.sys.databases D ON LB.dbName = D.name COLLATE Database_default Where D.Name is NULL And LB.keepTrace = 0 If @DoFullBkp = 1 Set @DoBackup = 'F' If @DoDiffBkp = 1 Set @DoBackup = 'D' If @DoLogBkp = 1 Set @DoBackup = 'L' -- ============================================================================== -- Start of backup processing -- ============================================================================== -- Delete old full backups, only when full backup must be done -- FulBkpRet is the amount of day with 0=today none delete is done, 1=yesterday and so on. -- some global params like @incDb and @excDb are available through context If @DoFullBkp = 1 And @FullBkpRetDays >= 0 -- no cleanup if < 0 or null Begin Exec Maint.DeleteOldBackups @Path = @FullBackupPath , @BkpRetDays = @FullBkpRetDays , @extension = @FullBkExt End -- If If @DoDiffBkp = 1 And @FullBkpRetDays >= 0 -- no cleanup if < 0 or null Begin Exec Maint.DeleteOldBackups @Path = @FullBackupPath , @BkpRetDays = @FullBkpRetDays , @extension = @FullBkExt , @DeleteOnlyLogDiffBackups = 1 -- Cleanup only log and differential files if we're doing a differential backup, keep other full backups End -- If -- Delete Log backups older than n days If (@DoLogBkp = 1 Or @DoFullBkp = 1 Or @DoDiffBkp = 1) And @LogBkpRetDays >= 0 -- no cleanup if < 0 or null Begin Exec YourSQLDba.Maint.DeleteOldBackups @Path = @LogBackupPath , @BkpRetDays = @LogBkpRetDays , @extension = @LogBkExt End Begin Try -- Get the installation language of the SQL Server instance Exec yInstall.InstallationLanguage @Language output -- ===================================================================================== -- main database backup loop by database -- ===================================================================================== set @info = ( Select CONVERT(nvarchar(max), '|') + case when d.FullRecoveryMode = 1 Then '(Full recovery) ' Else '(Simple Recovery)' End + d.dbName + '|' from @DbTable D Order by d.dbname for XML PATH('') ) Set @info = 'Database list obtained by @incBd and @ExecDb' + nchar(10) + REPLACE(@info, '|', nchar(10)) If ISNULL(ltrim(@Info),'') = '' Set @info = 'No databases either qualify by name or are available' Exec yExecNLog.LogAndOrExec @context = 'yMaint.Backups' , @Info = @info Set @DbName = '' While(1 = 1) -- T-SQL lacks simple Do Loop, work around... Begin -- this query get the next database (get the first when @dbname='') Select top 1 -- the first one next in alpha order (because top 1 + Where + Order by) @DbName = DbName, @contextData = 'yMaint.Backups '+ @dbName From @DbTable Where DbName > @DbName -- next db in alpha order Order By DbName -- ... database name alpha order -- Loop exit if last database processed (in alphabetic order) If @@rowcount = 0 Begin set @msg = @dbName + ' is the last database processed in the backups ' Exec yExecNLog.LogAndOrExec @context = 'yMaint.Backups' , @Info = @msg Break -- exit, no more db to process End If @DbName = 'MSDB' -- Skip over, because it is always backuped up at the end Continue Set @msg = 'Checking if '+@dbname + ' must be processed...' Exec yExecNLog.LogAndOrExec @context = 'yMaint.Backups' , @info = @msg If DatabasepropertyEx(@DbName, 'Status') <> 'ONLINE' -- if not online don't try to maintain Continue -- Validation block only, is log backup can be done? If @DoLogBkp = 1 -- log backups ? Begin -- If the database is read_only it is impossible to take log backup because -- first full backup is not recorded to the database, which void log backup -- And if the database is read-only what is the point to backup its log -- it is not supposed to grow If DATABASEPROPERTYEX(@DbName, 'Updateability') = 'READ_ONLY' Continue -- this Database don't move so no need to backup the log -- If the database is in simple recovery, it is impossible to do a save -- This situation is signaled if the user asked explicitely for it -- using @incDb. This is for production database forgotten in simple recovery mode If DATABASEPROPERTYEX(@DbName, 'Recovery') NOT IN ('Full', 'BULK_LOGGED') Begin -- User explicity asked for this database, and it is in simple recovery mode -- It must be told to him that log backups can be fulfilled If replace(replace(replace(@IncDb, ' ', ''), char(10), ''), char(13), '') <> '' Begin -- User explicity stated by @incDb that he wants a log backup but that it can't done -- so signal it as an error. Set @msg = 'Forbidden log backup of ['+@DbName+'] because it is in simple recovery mode ' Exec yExecNLog.LogAndOrExec @context = 'yMaint.Backups' , @YourSqlDbaNo = '012' , @Info = @Msg End -- if user asked for this database Continue -- Jump to the next database End -- if simple recovery mode Else Begin -- full recovery mode -- if log backup can't be performed because no full backup is done -- let it know to the user, if the option is not turned off by the user If Not Exists ( select * from sys.database_recovery_status where database_id = db_id(@DbName) and last_log_backup_lsn is not null -- backup can't be done ) Begin If @NotifyMandatoryFullDbBkpBeforeLogBkp = 1 Begin Set @err = 'Log backup forbidden before doing a first full backup of ' + '['+@DbName+'] status is ' + CONVERT(nvarchar(100), DATABASEPROPERTYEX(@DbName, 'status') ) Exec yExecNLog.LogAndOrExec @context = 'yMaint.Backups' , @YourSqlDbaNo = '013' , @Info = 'Log backups' , @err = @err End Else Begin Set @err = 'Log backup forbidden before doing a first full backup of ' + '['+@DbName+']' Exec yExecNLog.LogAndOrExec @context = 'yMaint.Backups' , @YourSqlDbaNo = '013' , @Info = 'Log backups' , @err = @err End Continue -- jump to next one End -- if log backup can't be performed End End -- if log backups -- Get backup commande for full backup or log backup If @DoFullBkp = 1 Begin Set @fileName = yMaint.MakeBackupFileName (@DbName, 'F', @FullBackupPath, @Language, @FullBkExt, @TimeStampNamingForBackups) Set @ctx = 'Full backups for '+@DbName End Else If @DoDiffBkp = 1 Begin Set @fileName = yMaint.MakeBackupFileName (@DbName, 'D', @FullBackupPath, @Language, @FullBkExt, @TimeStampNamingForBackups) Set @ctx = 'Diff backupsfor '+@DbName End Else Begin -- for log backups I want to continue to use the same file for the rest of the day -- usually it is there because the proc does an initial log backup with any full backup Select @fileName = lastLogBkpFile From Maint.JobLastBkpLocations Where dbName = @DbName If @@rowcount = 0 Or @filename IS NULL -- backup done manualy Or @BkpLogsOnSameFile = 0 -- backup the log on a new file Begin Set @fileName = yMaint.MakeBackupFileName (@DbName, 'L', @LogBackupPath, @Language, @LogBkExt, @TimeStampNamingForBackups) End Set @Info = 'Log backups for '+@DbName Select -- get most up-to-date value for mirroring parameter @ReplaceSrcBkpPathToMatchingMirrorPath = ReplaceSrcBkpPathToMatchingMirrorPath , @ReplacePathsInDbFilenames = ReplacePathsInDbFilenames , @MirrorServer = MirrorServer , @MigrationTestMode = MigrationTestMode From Maint.JobLastBkpLocations Where dbName = @DbName End -- If there is row record for this database update it If Exists(Select * from Maint.JobLastBkpLocations Where dbName = @DbName) Begin -- Mirror server change to reflect now from this backup. Accept a mirror server only at full backup -- but if there is no or no more mirrorServer ensure to stop mirroring any time If @DoFullBkp = 1 Or @MirrorServer = '' Or @DoDiffBkp = 1 Update Maint.JobLastBkpLocations Set mirrorServer = @MirrorServer , MigrationTestMode = @MigrationTestMode , ReplaceSrcBkpPathToMatchingMirrorPath = @ReplaceSrcBkpPathToMatchingMirrorPath , ReplacePathsInDbFilenames = @ReplacePathsInDbFilenames , lastFullBkpFile = Case When @DoFullBkp = 1 Then @FileName Else lastFullBkpFile End , lastDiffBkpFile = Case When @DoDiffBkp = 1 Then @FileName Else lastDiffBkpFile End , EncryptionAlgorithm = ISNULL(@EncryptionAlgorithm,'') , EncryptionCertificate = ISNULL(@EncryptionCertificate,'') Where dbName = @DbName End Else -- Insert new row records for this database, if it doesn't exists Insert into Maint.JobLastBkpLocations (dbName, lastLogBkpFile, MirrorServer, lastFullBkpDate, ReplaceSrcBkpPathToMatchingMirrorPath, ReplacePathsInDbFilenames,EncryptionAlgorithm,EncryptionCertificate) Select @DbName, Null, @MirrorServer, getdate(), @ReplaceSrcBkpPathToMatchingMirrorPath, @ReplacePathsInDbFilenames , ISNULL(@EncryptionAlgorithm,''), ISNULL(@EncryptionCertificate,'') Where Not Exists(Select * from Maint.JobLastBkpLocations Where dbName = @DbName) -- raise flag to not let dbcc log shrink go, when any type of backup occurs on this database Declare @resourceName sysname ='YourSqlDbaBkpOf_'+@dbName Exec sp_getapplock @dbprincipal='public', @Resource=@resourceName, @lockMode='Shared', @lockOwner='session' Set @sql = yMaint.MakeBackupCmd ( @DbName , @DoBackup , @fileName , IIF(@DoBackup = 'F', 1, 0) -- overwrite if full backup , @MaintJobName , @EncryptionAlgorithm , @EncryptionCertificate ) -- Launch backup Set @ContextData = 'yMaint.backups for '+@dbname+ ' to '+ISNULL(@filename, 'Oups destination is NULL') Exec yExecNLog.LogAndOrExec @context = @ContextData , @sql = @sql , @errorN = @errorN output -- drop flag to let dbcc log shrink go, since backup is done Exec sp_releaseapplock @Resource=@resourceName, @lockOwner='Session' -- Restore the backup to the mirror server -- Internally the procedure check if mirrorServer is specified -- otherwise it doesn't Exec yMirroring.QueueRestoreToMirror @context = @ctx , @DbName = @DbName , @bkpTyp = @DoBackup , @fileName = @fileName , @MirrorServer = @MirrorServer , @MigrationTestMode = @MigrationTestMode , @ReplaceSrcBkpPathToMatchingMirrorPath = @ReplaceSrcBkpPathToMatchingMirrorPath , @ReplacePathsInDbFilenames = @ReplacePathsInDbFilenames -- do not shrink log while a full or diff backup is performed on the database If @DoLogBkp = 1 And APPLOCK_TEST ('public', @resourceName, 'exclusive', 'session')=1 Begin -- shrink the log after backup (the procedure acts depending on the size) -- ShrinkLog may perform no shrink depending on internal database state (sys.databases.log_reuse_wait value) Set @sql2 = ' set nocount on declare @MustLogBackupToShrink int Exec yMaint.ShrinkLog @Db = "", @MustLogBackupToShrink = @MustLogBackupToShrink output truncate table #MustLogBackupToShrink insert into #MustLogBackupToShrink Values(@MustLogBackupToShrink) ' Set @sql2 = replace(@sql2, '', @DbName) Set @sql2 = replace(@sql2, '', convert(nvarchar, @JobNo)) Set @sql2 = replace(@sql2, '"', '''') Exec yExecNLog.LogAndOrExec @context = 'yMaint.backups' , @sql = @sql2 , @Info = 'Log shrinking attempt' , @errorN = @errorN output If exists(select * from #MustLogBackupToShrink Where @MustLogBackupToShrink = 1) Exec yExecNLog.LogAndOrExec @context = 'yMaint.backups' , @sql = @sql , @Info = 'Supplementary log backup to help log shrinking' , @errorN = @errorN output End -- If a full backup must be done, and if the database is in full recovery mode -- an initial log backup must be done If (@DoFullBkp = 1 or @DoDiffBkp = 1) And DATABASEPROPERTYEX(@DbName, 'Recovery') <> 'Simple' Begin Exec yExecNLog.LogAndOrExec @context = 'yMaint.backups' , @sql = @sql , @Info = 'Supplementary log backup to help log shrinking' , @errorN = @errorN output -- tracing of an error --Declare @trc nvarchar(max) = (select db=@DbName, typ='L', LogBackupPath=@LogBackupPath, pLanguage=@Language, LogBkExt=@LogBkExt, TimeStampNamingForBackups=@TimeStampNamingForBackups for json path, INCLUDE_NULL_VALUES) --Exec yExecNLog.LogAndOrExec @context = 'Trace', @info=@trc, @err = '999' Select @fileName = yMaint.MakeBackupFileName(@DbName, 'L', @LogBackupPath, @Language, @LogBkExt, @TimeStampNamingForBackups) --Set @trc = (select db=@DbName, typ='L', Filename=@Filename, pLanguage=@Language, LogBkExt=@LogBkExt, TimeStampNamingForBackups=@TimeStampNamingForBackups for json path, INCLUDE_NULL_VALUES) --Exec yExecNLog.LogAndOrExec @context = 'Trace', @info=@trc, @err = '999' Set @sql = yMaint.MakeBackupCmd ( @DbName , 'L' -- say explicitely full backup command , @fileName , 1 , @MaintJobName , @EncryptionAlgorithm , @EncryptionCertificate ) -- Launch first log backup that creates the file that will be used -- to stored log backups usually for the rest of the days unless -- end-user launch Maint.SaveDbOnNewFileSet Declare @infoAndDb Nvarchar(4000) = 'Log backups (init) for '+@dbname+ ' to '+ISNULL(@fileName, 'oups the filename is NULL') Exec yExecNLog.LogAndOrExec @context = 'yMaint.backups' , @sql = @sql , @Info = @InfoAndDb , @errorN = @errorN_BkpPartielInit output -- Restore the backup to the mirror server if enabled Exec yMirroring.QueueRestoreToMirror @context = 'yMaint.backups (queue restore of log backup init)' , @DbName = @DbName , @bkpTyp = N'L' , @fileName = @fileName , @MirrorServer = @MirrorServer , @MigrationTestMode = @MigrationTestMode , @ReplaceSrcBkpPathToMatchingMirrorPath = @ReplaceSrcBkpPathToMatchingMirrorPath , @ReplacePathsInDbFilenames = @ReplacePathsInDbFilenames If @errorN_BkpPartielInit = 0 -- version Begin Update Maint.JobLastBkpLocations Set lastLogBkpFile = @filename Where dbName = @DbName -- shrink the log after backup (the procedure acts depending on the size) -- Exec yMaint.ShrinkLog @DbName End End -- the decision to put a database offline only occurs on full db backup -- initial log backup error are taken into account at this time to If @DoFullBkp = 1 or @DoDiffBkp = 1 Begin -- increment error count on any of the two backup types If @errorN <> 0 Or @errorN_BkpPartielInit <> 0 Begin Update Maint.JobLastBkpLocations Set FailedBkpCnt = FailedBkpCnt + 1 , @FailedBkpCnt = FailedBkpCnt + 1 , LastFullBkpDate = getdate() -- record the day when it happens again Where dbName = @DbName And datediff(hh, lastFullBkpDate, getdate()) > 24 -- increment if it happens on different days And @DoDiffBkp <> 1 If @FailedBkpCnt >= @ConsecutiveDaysOfFailedBackupsToPutDbOffline -- if to many error put in offline mode Exec yMaint.PutDbOffline @DbName, @JobNo End Else Update Maint.JobLastBkpLocations Set FailedBkpCnt = 0 , LastFullBkpDate = getdate() -- record the day when it succeed Where dbName = @DbName And @DoDiffBkp <> 1 End End -- Loop While (1 = 1) process each database selected -- a full backup of msdb always occurs even after log backup -- to get the most accurate up-to-date log history Set @fileName = yMaint.MakeBackupFileName('MsDb', 'F', @FullBackupPath, @Language, @FullBkExt, @TimeStampNamingForBackups) Set @sql = yMaint.MakeBackupCmd ('Msdb', 'F', @fileName, 1, '', @EncryptionAlgorithm, @EncryptionCertificate) Exec yExecNLog.LogAndOrExec @context = 'yMaint.backups' , @sql = @sql , @Info = 'Full Msdb backup to save the most up-to-date backup history' , @errorN = @errorN output End try Begin catch Exec yExecNLog.LogAndOrExec @context = 'yMaint.Backups' , @Info = 'Error in yMaint.backups' , @err = '?' End Catch End -- yMaint.Backups GO ---------------------------------------------------------------------------------------------------------- -- Cleanup YourSqlDba tables for removed servers. -- Install a the same YourSqlDba account on existing YourSqlDba mirror servers -- and do a linked server login mapping impersonnation between the local account and the remote one. -- Replicate local YourSqlDba account to to remote server. -- Process is very safe, since nobody knows YourSqlDba account on both side -- If multiple servers mirror their database to a single server, one must explicitely sets the same -- YourSqlDba account password on source servers, so the same account is going to be replicated -- by every participating server. ---------------------------------------------------------------------------------------------------------- -- @@MARK: Maintenance Mirroring - ConfigureSecurity Create Or Alter Procedure Mirroring.SetYourSqlDbaAccountForMirroring @YourSqlDbaAccountForMirroringPwd Nvarchar(max) = NULL as Begin Set nocount on Declare @MirrorServerName sysname Declare @Sql nvarchar(max) declare @loginExists int declare @password_hash_local varbinary(256) declare @original_password_hash_local varbinary(256) declare @err int = 0 declare @newPwd nvarchar(max) = NULL If @YourSqlDbaAccountForMirroringPwd = 'choose some password' Begin Raiserror ('Seriously, we don''t accept place holder ''choose some password'' as valid password, password rejected, please specify another one', 11, 1) Return End -- remember actual password in hashed form Set @original_password_hash_local = convert(varbinary(max), LOGINPROPERTY('YourSqlDba', 'PasswordHash')) -- get new password if specified or compute a new random value SET @newPwd = ISNULL(@YourSqlDbaAccountForMirroringPwd, replace(convert(nvarchar(max), newid(), 0)+convert(nvarchar(max), newid(), 0), '-', '')) Set @sql = 'Alter login YourSqlDba With password = '''+@NewPwd+'''' Exec (@sql) -- Get new password hash for remote login, which is easy with login property. Set @password_hash_local = convert(varbinary(max), LOGINPROPERTY('YourSqlDba', 'PasswordHash')); -- but if no password input was specified, set actual local password back to its original value -- now that we have the new password hash for remote login, otherwise this means that admin set also -- local password of YourSqlDba account with the same value If @YourSqlDbaAccountForMirroringPwd IS NULL Begin Set @sql = 'Alter login YourSqlDba With password='+Convert(nvarchar(max),@Original_password_hash_Local,1)+' HASHED' Exec (@sql) End Set @MirrorServerName = '' While (1=1) Begin Select top 1 @MirrorServerName = MirrorServerName From Mirroring.TargetServer Where MirrorServerName > @MirrorServerName Order By MirrorServerName If @@ROWCOUNT = 0 Break -- Reinstall YourSqlDba mapping If Exists ( Select * From Sys.Servers S JOIN Sys.linked_logins LL ON LL.server_id = S.server_id JOIN Sys.server_principals P ON P.principal_id = LL.local_principal_id Where P.Name = 'YourSqlDba' And S.Name = @MirrorServerName And S.is_linked = 1 ) Begin Print 'Drop previous YourSqlDba login mapping from ' + @MirrorServerName Exec Master.dbo.sp_Droplinkedsrvlogin @rmtsrvname = @MirrorServerName, @locallogin = 'YourSqlDba' End Print 'Reinstall YourSqlDba login mapping on ' + @MirrorServerName EXEC master.dbo.sp_addlinkedsrvlogin @rmtsrvname = @MirrorServerName, @locallogin = 'YourSqlDba', @rmtUser='YourSqlDba', @rmtpassword=@newPwd, @useself='False' -- Make YourSqlDba be the same with same account on other serveur. Set @sql = ' Print "Synchronize YourSqlDba account on " Execute ( " Use YourSqlDba Begin try -- proceed only if YourSqlDba database and its account exists Declare @currentSysAdmin sysname = SUSER_SNAME() If SUSER_SID(""YourSqlDba"") IS NOT NULL AND DB_ID(""YourSqlDba"") IS NOT NULL Begin Exec (""alter authorization on database::yoursqldba to [""+@currentSysAdmin+""]"") Alter LOGIN YourSqlDba WITH PASSWORD= HASHED Exec (""alter authorization on database::yoursqldba to [YourSqlDba]"") CREATE CREDENTIAL YourSqlDbaRemoteServerCred WITH IDENTITY = ""YourSqlDba"", SECRET = """"; End End try Begin catch Declare @msg nvarchar(max) = ""Msg ""+convert(varchar, error_number())+"" ""+Error_message() Raiserror (@Msg, 11, 1) End catch " ) At [] ' Set @sql = REPLACE(@sql, '', @MirrorServerName) Set @sql = REPLACE(@sql, '', Convert(nvarchar(max),@password_hash_Local,1)) Set @sql = REPLACE(@sql, '', @newPwd) Set @sql = REPLACE(@sql, '"', '''') Begin try Print @sql Exec(@Sql) End Try Begin Catch Set @err = 1 Declare @msg nvarchar(max) = error_message () Print @Msg End Catch End -- for each link server return @err End -- Mirroring.SetYourSqlDbaAccountForMirroring GO ---------------------------------------------------------------------------------------------------------- -- Cleanup YourSqlDba tables for removed servers. -- Check access of mirror servers through YourSqlDba account. If a single one fails, email -- and action to do set YourSqlDba password and set YourSqlDba account bridge to MirrorServer. -- Return a success or status ---------------------------------------------------------------------------------------------------------- -- @@MARK: Maintenance Mirroring - Cleanup and diag Linked Server for Mirroring Create Or Alter Procedure yMirroring.CleanMirrorServerForMissingServerAndCheckServerAccessAsYourSqlDbaAccount @oper sysname = NULL , @MirrorServer sysname = NULL , @returnMsg sysname = NULL OUTPUT As Begin Declare @err Int = 0 -- cleanup inconsistent mirror references of the past Delete Mirroring.TargetServer Where isnull(MirrorServerName, '') = '' Delete M From Mirroring.TargetServer M Where Not Exists(Select * From Sys.Servers S Where S.Name = M.MirrorServerName Collate Database_Default And S.is_linked = 1) ;With Vue_Update as ( Select MirrorServer, ISNULL(TS.MirrorServerName, '') as ServerNameReplacement from Maint.JobLastBkpLocations JBL -- cleanup mirroring.TargetServers, Maint.JobLastBkpLocations LEFT JOIN Mirroring.TargetServer TS ON TS.MirrorServerName = JBL.MirrorServer Where JBL.MirrorServer <> '' ) Update Vue_Update Set MirrorServer = ServerNameReplacement Declare @sql nvarchar(max) Declare @MirrorServerName sysname -- when called from maintenance SP, but there is no linked server under that name If ISNULL(@MirrorServer, '') <> '' And Not Exists (Select * From Sys.servers Where name = @MirrorServer And is_linked = 1) Set @err = 1 -- impersonate YourSqlDba account to test connection Execute as login = 'yoursqldba' -- for each server, check remote access as YourSqlDba Set @MirrorServerName = '' declare @i int While (@err = 0) -- or break from the inside Begin Select top 1 @MirrorServerName = MirrorServerName From Mirroring.TargetServer Where MirrorServerName > @MirrorServerName Order By MirrorServerName If @@ROWCOUNT = 0 Break Begin try Set @sql = ' Select @i=Dbid from Openquery ([], "select Db_Id(""Master"") as DbId") as x ' Set @sql = REPLACE( @sql, '', @MirrorServerName) Set @sql = REPLACE( @sql, '"', '''') Exec sp_executeSql @sql, N'@i int output', @i output End try Begin catch Select @returnMsg = ErrMsg From (Select MsgTemplate= '---------------------------------------------------------------------------------------------- -- Msg: #ErrMessage# -- Error: #ErrNumber# Severity: #ErrSeverity# State: #ErrState##atPos# ----------------------------------------------------------------------------------------------' ) as MsgTemplate CROSS APPLY S#.FormatRunTimeMsg (MsgTemplate, ERROR_NUMBER (), ERROR_SEVERITY(), ERROR_STATE(), ERROR_LINE(), ERROR_PROCEDURE (), ERROR_MESSAGE ()) as Fmt Set @err = 1 -- possible error causes = -- 7202 : Could not find server in sys.servers. Verify that the correct server name was specified. If necessary, execute the stored procedure sp_addlinkedserver to add the server to sys.servers. --18456 : Failure to open session with 'yoursqldba'. --7437 : Linked servers cannot be used under impersonation without a mapping for the impersonated login. End catch End -- While REVERT; -- leave YourSqlDba account persona If @err = 0 -- all mirror servers provide access through YourSqlDba account Return; -- try to auto-repair broken connections, which is possible only if currently -- with an account that have sysadmin privileges that maps to a sysadmin account on each Mirror servers -- Must not be YourSqlDba, because we just tested it and it failed. If SUSER_SNAME () <> 'YourSqlDba' And IS_SRVROLEMEMBER('sysadmin', SUSER_SNAME () )=1 -- Mirror server exists or not specified And (Exists (Select * From Sys.servers Where name = @MirrorServer And is_linked = 1) Or ISNULL(@MirrorServer, '') = '') Begin Exec @err = Mirroring.SetYourSqlDbaAccountForMirroring If @err = 0 Return 0; End -- If here auto-repaired was not performed or couldn't be performed -- figures out who to notify if necessary Declare @email_address nvarchar(512) = NULL ;With MostSusceptibleOperator as ( select email_Address from Msdb..sysoperators Where enabled = 1 And name = @oper -- if called from YourSqlDba_DoMaint UNION ALL SELECT top 1 S.recipients as email_Address -- @oper is not specified, tries figure it out last message from YourSqlDba FROM msdb.dbo.sysmail_sentitems S Where s.subject like '%YourSqlDba%' And sent_status = 'Sent' And @oper is NULL ) Select @email_address = email_Address From MostSusceptibleOperator If @email_address IS NULL -- still can't figure out who to notify, nothing else to do Return Declare @body nvarchar(max) If ISNULL(@MirrorServer,'') <> '' -- if mirror server yoursqldba If Exists (Select * From Sys.servers Where name = @MirrorServer And is_linked = 1) -- real server Set @body = -- link couldn't be repaired send email to ask for it '
First and foremost, ensure that the destination server for the YourSqlDba mirroring feature is up and running. If it is not, start it and ignore the rest of this message!

Ensure that you are granted admin access to every remote linked server defined for your mirror servers and execute the following command on corresponding local servers:

Exec YourSQLDba.Mirroring.SetYourSqlDbaAccountForMirroring
If the same MirrorServer has multiple source servers specify a common password on every of them:

Exec YourSQLDba.Mirroring.SetYourSqlDbaAccountForMirroring @YourSqlDbaAccountForMirroringPwd = ''choose some password''
' Else -- says that the parameter is invalid, @mirrorServer doesn't match linked server Set @body = ' Specified @mirrorServer parameter doesn''t match with any linked servers names. Do Mirroring.Addserver to add the missing server or correct the parameter. ' EXEC Msdb.dbo.sp_send_dbmail @profile_name = 'YourSQLDba_EmailProfile' , @recipients = @email_Address , @importance = 'High' , @subject = 'YourSqlDba : Check if YourSqlDba mirror server(s) are all running. It they are, reset YourSqlDba account for MirrorServer or correct @mirrorServer parameter' , @body = @body , @body_format = 'HTML' Print 'Message sent to '+@email_Address Print 'Subject: ' + @body return 1 End Go -- @@MARK: TODO : Check for use and duplicate fonction in S#, check also if function that dedup space can be replaced by this Create Or Alter Function yUtl.DedupSeqOfChars (@Dup nvarchar(5), @Str Nvarchar(max)) Returns Table as Return ( Select s = NoMoreRepeatingChar, NoMoreRepeatingChar -- return both values with different names for compatibility purposes From (Select StrWithCharToDedup=@Str, Dup=@Dup) as vPrm Cross Apply (Select StartEndPair=NChar(0x25BA)+NChar(0x25C4)) As vStartEndPair Cross Apply (Select EndStartPair=NChar(0x25C4)+NChar(0x25BA)) as vEndStartPair Cross Apply (Select DupCharReplacedByStartEndPairs=Replace(StrWithCharToDedup, Dup, StartEndPair)) as vDupCharToStartEndPair Cross Apply (Select EndStartPairsRemoved=replace(DupCharReplacedByStartEndPairs, EndStartPair, '')) as vEndStartPairsRemoved Cross Apply (Select NoMoreRepeatingChar=replace(EndStartPairsRemoved, StartEndPair, Dup)) as vNoRepeatingChar ) GO ------------------------------------------------------------------------------------------- -- Wait for inactivity on YourSqlDba_DoMaint ------------------------------------------------------------------------------------------- -- @@MARK: Maintenance : Sync with backups/restore done by external backup/restore tooling ex: Commvault, ProcessRestores Create Or Alter Proc Maint.SetSyncWith_YourSqlDba_DoMaint @WaitType Sysname = 'Exclusive' AS Begin -- Exclusive mode is intented to be used when synchronizing external backup process with CommVault -- See https://tinyurl.com/YourSqlDbaAndCommVault for a more detailed overview. If @WaitType In ('Exclusive', 'Shared') exec sp_getapplock @resource='YourSqlDba.Do_Maint', @lockMode=@WaitType, @lockOwner='Session', @DbPrincipal='dbo' Else Raiserror ('@WaitType parameter must either be Exclusive or Shared',11,1) with nowait; End GO -- @@MARK: Maintenance : CommVault - Sync with backups done by external backup tooling Create Or Alter Proc Maint.SignalEndOf_YourSqlDba_DoMaint AS -- To cancel lock acquired by Maint.SetSyncWith_YourSqlDba_DoMaint exec sp_releaseapplock @resource='YourSqlDba.Do_Maint', @lockOwner='Session', @DbPrincipal='dbo' GO ------------------------------------------------------------------------------------------- -- Maint Stored proc. that is scheduled for maintenance ------------------------------------------------------------------------------------------- Create Or Alter Proc Maint.YourSqlDba_DoMaint @oper nvarchar(200) , @MaintJobName nvarchar(200) = 'Ad-Hoc Job' -- a name is given to override mecanism that gets this information from Sql Agent job when NULL , @DoInteg int = 0 , @DoUpdStats int = 0 , @DoReorg int = 0 , @DoBackup nvarchar(5) = '' , @FullBackupPath nvarchar(512) = NULL , @LogBackupPath nvarchar(512) = NULL , @TimeStampNamingForBackups Int = 1 -- by default all backups are timestamped, when using deduplication tools, it is better to keep same backup name , @FullBkExt nvarchar(7) = 'BAK' -- default backup extension for full backups , @LogBkExt nvarchar(7) = 'TRN' -- default backup extension for transaction log backups , @FullBkpRetDays Int = NULL -- by default no cleanup of full backups , @LogBkpRetDays Int = NULL -- by default no cleanup of log backups , @NotifyMandatoryFullDbBkpBeforeLogBkp int = 1 , @BkpLogsOnSameFile int = 1 , @SpreadUpdStatRun int = 7 , @SpreadCheckDb int = 7 , @ConsecutiveDaysOfFailedBackupsToPutDbOffline Int = 9999 -- max consecutives failure when of full backup and initial log backup , @MirrorServer sysname = '' , @MigrationTestMode Int = 0 , @ReplaceSrcBkpPathToMatchingMirrorPath nvarchar(max) = '' -- replaces on srcBkpPath to match corresponding path from mirror , @ReplacePathsInDbFilenames nvarchar(max) = '' -- replaces in db files names to restore , @IncDb nVARCHAR(max) = '' -- @IncDb : See comments later for further explanations , @ExcDb nVARCHAR(max) = '' -- @ExcDb : See comments later for further explanations , @ExcDbFromPolicy_CheckFullRecoveryModel nVARCHAR(max) = '' -- @ExcDbFromPolicy_CheckFullRecoveryModel : -- See comments later for further explanations , @EncryptionAlgorithm nvarchar(10) = '' , @EncryptionCertificate nvarchar(100) = '' --, @JobId UniqueIdentifier = NULL -- job id of SQL Server Agent Job that launched the job --, @StepId Int = NULL -- stepid of SQL Server Agent Jobstep that launched the job as Begin -- @@MARK: Maintenance : YourSqlDba_DoMaint Main DoMaint ! Set nocount On -- YourSqlDba does always a shared lock to prevent external backup process with CommVault -- See https://tinyurl.com/YourSqlDbaAndCommVault for a more detailed overview. Exec yExecNLog.LogAndOrExec @Info = 'Start of YourSqlDba DoMaint' -- log as soon as possible, since logging and error reporting depends on a job entry exec Maint.SetSyncWith_YourSqlDba_DoMaint @WaitType = 'Shared' -- reporting is heavily supported by email, and this info must be avail on-hand before the begin try Declare @email_Address sysname -- to read email address of the operator select @email_Address=email_address from Msdb..sysoperators Where enabled = 1 And name = @oper -- if called from YourSqlDba_DoMaint Begin Try declare @sql nvarchar(max) -- SQL query declare @StartOfMaint datetime -- when maintenance started declare @JobNo Int -- job number declare @SendOnErrorOnly Int=0 -- when to send and error message declare @lockResult Int Declare @SqlBinRoot nvarchar(512) Declare @pathBkp nvarchar(512) Declare @JobId UniqueIdentifier -- job id of SQL Server Agent Job that launched the job Declare @StepId Int -- stepid of SQL Server Agent Jobstep that launched the job -- If maintenance is called from SqlAgent we manage this: -- Log viewer has a poor display of job history. It supresses line feeds and truncate output -- The only way to have a nice output is through a simple select, and by checking -- option : Include Step output in history Set @SendOnErrorOnly = 0 Select @ReplacePathsInDbFilenames = finished.NoMoreRepeatingChar From (Select r0=Isnull(@ReplacePathsInDbFileNames, '')) as vr0 Cross Apply (Select r1=Replace(r0, nChar(10), '')) as vr1 Cross Apply (Select r2=Replace(r1, nChar(10), '')) as vr2 Cross Apply yUtl.DedupSeqOfChars(' ', r2) as finished Select @ReplaceSrcBkpPathToMatchingMirrorPath = finished.NoMoreRepeatingChar From (Select r0=Isnull(@ReplaceSrcBkpPathToMatchingMirrorPath, '')) as vr0 Cross Apply (Select r1=Replace(r0, nChar(10), '')) as vr1 Cross Apply (Select r2=Replace(r1, nChar(10), '')) as vr2 Cross Apply yUtl.DedupSeqOfChars(' ', r2) as finished -- the query below is a first step in re-engeneering some parts of YourSqlDba -- it allows to globalize in the current connection context the parameters -- and makes easy to obtain them throught Select * from dbo.MainContextInfo () in a tabular form Select @sql=S.Sql From (Select oper = @oper , MaintJobName = @MaintJobName , DoInteg = @DoInteg , DoUpdStats = @DoUpdStats , DoReorg = @DoReorg , DoBackup = @DoBackup , DoFullBkp = IIF( @DoBackup = 'F' , 1 , 0 ) , DoLogBkp = IIF( @DoBackup = 'L' , 1 , 0 ) , DoDiffBkp = IIF( @DoBackup = 'D' , 1 , 0 ) , IncDb = @IncDb , ExcDb = @ExcDb , MigrationTestMode = @MigrationTestMode , FullBackupPath = @FullBackupPath , LogBackupPath = @LogBackupPath , TimeStampNamingForBackups = @TimeStampNamingForBackups , FullBkExt = @FullBkExt , LogBkExt = @LogBkExt , FullBkpRetDays = @FullBkpRetDays , LogBkpRetDays = @LogBkpRetDays , NotifyMandatoryFullDbBkpBeforeLogBkp = @NotifyMandatoryFullDbBkpBeforeLogBkp , BkpLogsOnSameFile = @BkpLogsOnSameFile , SpreadUpdStatRun = @SpreadUpdStatRun , SpreadCheckDb = @SpreadCheckDb , ConsecutiveDaysOfFailedBackupsToPutDbOffline = @ConsecutiveDaysOfFailedBackupsToPutDbOffline , MirrorServer = @MirrorServer , ReplaceSrcBkpPathToMatchingMirrorPath = @ReplaceSrcBkpPathToMatchingMirrorPath , ReplacePathsInDbFilenames = @ReplacePathsInDbFilenames , ExcDbFromPolicy_CheckFullRecoveryModel = @ExcDbFromPolicy_CheckFullRecoveryModel , EncryptionAlgorithm = @EncryptionAlgorithm , EncryptionCertificate = @EncryptionCertificate ) as Prm Cross Apply ( Select JsonPrm= ( Select oper, MaintJobName , DoInteg, DoUpdStats, DoReorg, DoBackup, DoFullBkp, DoDiffBkp, DoLogBkp , FullBackupPath, LogBackupPath, TimeStampNamingForBackups, FullBkExt, LogBkExt, FullBkpRetDays, LogBkpRetDays , NotifyMandatoryFullDbBkpBeforeLogBkp, BkpLogsOnSameFile , SpreadUpdStatRun, SpreadCheckDb , ConsecutiveDaysOfFailedBackupsToPutDbOffline , MirrorServer, MigrationTestMode, ReplaceSrcBkpPathToMatchingMirrorPath, ReplacePathsInDbFilenames , IncDb, ExcDb, ExcDbFromPolicy_CheckFullRecoveryModel , EncryptionAlgorithm, EncryptionCertificate For JSON PATH, WITHOUT_ARRAY_WRAPPER ) ) as JsonPrm cross apply dbo.ScriptSetGlobalAccessToPrm (JsonPrm) as S -- Execute SQL generated by previous query which adds a row to -- Maint.JobHistory to record a new job Exec (@Sql) -- @@MARK: Full logging through emai starts here, because job context must be set. Exec yExecNLog.LogAndOrExec @context = 'Maint.YourSqlDba_DoMaint' , @Info = 'Beginning of job' If @email_Address IS NULL Exec yExecNLog.LogAndOrExec @context = 'Maint.YourSqlDba_DoMaint' , @YourSqlDbaNo = '009' , @Info = 'Maint.YourSqlDba_DoMaint' , @err = ' The operator name supplied to the procedure Maint.YourSqlDba_DoMaint, must exist and be enabled in msdb..sysoperators ' If Exists (Select * From dbo.WhoCalledWhat Where Prog Like 'SqlAgent%') Select Convert(Nvarchar(256),'If an error is reported for this job, run the following EXEC command in a query window:') UNION ALL Select 'Select cmdStartTime, JobNo, seq, Typ, line, Txt,, MaintJobName, MainSqlCmd, Who, Prog, Host, SqlAgentJobName, JobId, JobStart, JobEnd From YourSQLDba.Maint.HistoryView(''' + convert(nvarchar, @StartOfMaint, 121) + ''', ''' + convert(nvarchar, Getdate(), 121) + ''', 1) Order By cmdStartTime, JobNo, Seq, TypSeq, Typ, Line' If ISNULL(@EncryptionAlgorithm, '') <> '' AND ISNULL(@EncryptionCertificate, '') <> '' Begin -- backups on the same file are not supported for encrypted backups Set @BkpLogsOnSameFile=0 End -- alter admin when a valid linked server is specified, that this one or another needs a YourSqlDba password account reset If ISNULL(@MirrorServer, '') <> '' Begin Declare @rc Int Declare @returnMsg Nvarchar(4000) Select @returnMsg='Check the previous YourSqlDba email whose subject starts with: "YourSqlDba: Check if YourSqlDba mirror server(s)..."'+E.NL+ISNULL(@returnMsg,'') From S#.Enums as E Exec @rc = yMirroring.CleanMirrorServerForMissingServerAndCheckServerAccessAsYourSqlDbaAccount @oper=@oper, @MirrorServer=@MirrorServer, @returnMsg=@returnMsg Output If @rc <> 0 Begin Raiserror ('Check the previous YourSqlDba email whose subject starts with: "YourSqlDba: Check if YourSqlDba mirror server(s)..."', 11, 1); Return End End Exec Install.PrintVersionInfo If @ReplaceSrcBkpPathToMatchingMirrorPath<> '' Begin If charindex('>', @ReplaceSrcBkpPathToMatchingMirrorPath) = 0 Begin Raiserror ('Parameter @ReplaceSrcBkpPathToMatchingMirrorPath content must be separated by a ''>'' char between the search and the replace expression', 11, 1) End If right(rtrim(@ReplaceSrcBkpPathToMatchingMirrorPath),1) <> '|' Begin Raiserror ('Parameter @ReplaceSrcBkpPathToMatchingMirrorPath content must be ended by a pipe char ''|'' ', 11, 1) End End If @ReplacePathsInDbFilenames <> '' Begin If charindex('>', @ReplacePathsInDbFileNames) = 0 Begin Raiserror ('Parameter @ReplacePathsInDbFileNames content must be separated by a ''>'' char between the search and the replace expression', 11, 1) End If right(rtrim(@ReplacePathsInDbFileNames),1) <> '|' Begin Raiserror ('Parameter @ReplacePathsInDbFileNames content must be ended by a pipe char ''|'' ', 11, 1) End End Set @FullBackupPath = yUtl.NormalizePath(@FullBackupPath) Set @LogBackupPath = yUtl.NormalizePath(@LogBackupPath) If @doBackup = 'C' Set @doBackup = 'F' -- translate 'C' = complete to 'F' = Full If @doBackup = 'P' Set @doBackup = 'L' -- translate 'P' = complete to 'L' = Log exec master.dbo.xp_instance_regread N'HKEY_LOCAL_MACHINE' , N'Software\Microsoft\MSSQLServer\Setup' , N'SqlBinRoot' , @SqlBinRoot OUTPUT , 'no_output' exec master.dbo.xp_instance_regread N'HKEY_LOCAL_MACHINE' , N'Software\Microsoft\MSSQLServer\MSSQLServer' , N'DefaultData' , @pathBkp OUTPUT , 'no_output' -- remind backup directory used, very useful when it comes to restore If @FullBackupPath IS NOT NULL and @FullBackupPath <> @pathBkp And (@DoBackup IN ('F','D')) Begin Declare @tmp nvarchar(512) Set @tmp = @FullBackupPath If right(@tmp, 1) = '\' Set @tmp = stuff(@tmp, len(@tmp), 1, '') EXEC master.dbo.xp_instance_regwrite N'HKEY_LOCAL_MACHINE' , N'Software\Microsoft\MSSQLServer\MSSQLServer' , N'BackupDirectory' , REG_SZ , @tmp End -- use to timestamp filename set @StartOfMaint = getdate() -- Remove trace of backup location for databases no longer there Delete LB From Maint.JobLastBkpLocations LB LEFT JOIN master.sys.databases D ON LB.dbName = D.name COLLATE Database_default Where D.Name is NULL And LB.keepTrace = 0 If @FullBackupPath IS NULL Or @LogBackupPath IS NULL Begin If @DoBackup IN ('F', 'L', 'D') Begin Raiserror ('Specify @FullBackupPath and/or @LogBackupPath to the procedure ', 11, 1) End End -- Error message if operator is missing, exit now to let error status in Sql agent job select @email_Address = email_Address from Msdb..sysoperators where name = @oper and enabled = 1 If @@rowcount = 0 Begin Raiserror (' A valid operator name must be supplied to the procedure', 11, 1) End --Exec yExecNLog.LogAndOrExec -- test logging mecanism on severe errord that does connexion lost (severity 20 and above) -- @context = 'Maint.YourSqlDba_DoMaint' --, @Info = 'Test fatal err' --, @sql = 'raiserror (''test err fatale'', 25, 1) with log ' -- Advise user that best practices are not followed If @DoBackup <> 'L' Exec PerfMon.ReportIgnoredBestPractices @email_Address = @email_Address If Exists(Select * From Dbo.MainContextInfo (NULL) Where DoInteg=1 Or DoUpdStats=1 Or DoReorg=1 Or DoBackup IN ('F','D')) Begin -- Warns it this version of YourSqlDba is quite Old If GETDATE() > yInstall.NextUpdateTime() And datepart(dd, getdate()) = 1 -- just do it once a month Begin declare @msgBody nvarchar(max) declare @subject nvarchar(max) declare @version nvarchar(20) Select @version = VersionNumber From Install.VersionInfo() If CONVERT(nvarchar(20), SERVERPROPERTY('LCID')) <> '1036' Begin set @subject = yInstall.DoubleLastSpaceInFirst78Colums ('YourSqlDba '+@Version+' reminder. Time to check for the free newer YourSqlDba version at https://onedrive.live.com/redir?resid=12C385255443C4ED%217080&authkey=%21AAUr-EDkGO3RESc&page=View&wd=target%28Introduction.one%7Cc7014943-14b8-4c1d-9ae7-429002e0759c%2FQuick%20Start%20%20download%7C7baefd6f-3103-45b4-899f-8c9f4be9e119%2F%29&wdorigin=703, then open it and and run it.') Set @msgBody = ' This message is to remind you to get the latest and most reliable YourSqlDba code for this Sql instance: .

By applying the latest version you get rid of this monthly reminder.

Update is very easy. Just get the latest script from versions links from YourSqlDba documentation home page, then open it copy/paste and then run it.

Actually this project is subject to frequent improvments as the support of our large user community help us to find many uses cases. It is located on YourSqlDba''s Github home project and from this home page there is a Read me section where you can also find a link to the most recent script version.
' End Else Begin set @subject = yInstall.DoubleLastSpaceInFirst78Colums ('YourSqlDba '+@Version+' Rappel: Il est temps de vérifier la disponiblité d''une version plus récente de YourSqlDba à YourSqlDba.codeplex.com') Set @msgBody = ' Ce message a pour but de vous rappeller de récupérer la version la plus récente de YourSqlDba pour l''instance: .

L''application de la dernière version fait disparaître ce message.

La mise à jour est très simple. Obtenez le script en cliquant sur le lien de la plus récente version à partir de la Page d''acceuil de la documentation de YourSqlDba et puis ouvrez, copier/collez le script puis exécutez-le.

Ce projet fait l''objet d''améliorations fréquentes compte tenu que le support de notre grande communauté d''utilisateurs nous aide à découvrir beaucoup de cas d''utilisation. Il est localisé sur ce site Github et à partir de cette page d''acceuil se trouve une section Lisez-moi, ou se trouve également un lien vers la version la plus récente.
' End Set @msgBody = replace(@msgBody, '', convert(sysname, serverproperty('ServerName'))) Set @msgBody = replace(@msgBody, '', convert(sysname, serverproperty('ServerName'))) EXEC Msdb.dbo.sp_send_dbmail @profile_name = 'YourSQLDba_EmailProfile' , @recipients = @email_Address , @importance = 'Normal' , @subject = @subject , @body = @msgBody , @body_format = 'HTML' End End -- avoid easy mistake (a narrow space between 2 quotes) Set @DoBackup = replace(@DoBackup, ' ', '') -- add '\' to path name just in case it is missing If right(@FullBackupPath,1)<> '\' Set @FullBackupPath = @FullBackupPath + '\' If right(@LogBackupPath,1)<> '\' Set @LogBackupPath = @LogBackupPath + '\' -- Record all databases online, and if they are in full recovery mode or not (log backup allowed or not) -- The function udf_YourSQLDba_ApplyFilterDb apply filter parameters on this list Create table #Db ( DbName sysname primary key clustered , DbOwner sysname NULL -- because actual owner may be invalid after a restore , FullRecoveryMode int -- If = 1 log backup allowed , cmptLevel tinyInt , DbIsCaseInsensitive tinyInt ) Insert into #Db Select F.*, DbIsCaseInsensitive from yUtl.YourSQLDba_ApplyFilterDb (@IncDb, @ExcDb) as F JOIN Sys.databases as D ON D.Name collate database_default = F.DbName CROSS APPLY (Select DbIsCaseInsensitive =cast(COLLATIONPROPERTY(D.collation_name,'ComparisonStyle') as int) & 1) as DbIsCaseInsensitive Where DatabasepropertyEx(DbName, 'Status') = 'Online' -- Avoid db that can't be processed -- remove snapshot database from the list Delete Db From #Db Db Where Exists(Select * From sys.databases d Where d.name = db.DbName and source_database_Id is not null) -- if any database is defined as Case sensitive issue an error message asking to exclude it from -- yoursqldba process. -- create database TestiscaseSensitive collate french_cs_as declare @err nvarchar(max) Select @err = 'The following case sensitive database(s) will not be processed by YoursqlDba :'+ Stuff ( (Select convert(nvarchar(max), ','+DbName) as [text()] From #Db Where DbIsCaseInsensitive=0 Order by DbName For XML PATH('')) , 1, 1, '' ) If @err is not null Exec yExecNLog.LogAndOrExec @context = 'Maint.YourSqlDba_DoMaint pre-checks' , @YourSqlDbaNo = '007' , @err = @err , @Info = 'Case sensitive databases are not processed by YourSqlDba. Exclude them from maintenance by setting «@ExcDb» parameter of the «YourSQLDba_DoMaint» to exclude databases from the check' Delete From #Db Where DbIsCaseInsensitive = 0 -- ============================================================================== -- Cleanup backup history, and log cleanup if it appears to be the big maintenance -- integrity test and full backup are the only important maintenance parameters -- that identify a full maintenance -- ============================================================================== If @DoInteg=1 And @DoBackup = 'F' Begin exec yMaint.LogCleanup End -- @@MARK: TODO : Cleanup should be based on job name, and ProcessRestore should take that into account Delete Mirroring.RestoreQueue Where @DoBackup = 'F' -- remove leftover from previous exec -- ============================================================================== -- perform integrity tests or not -- ============================================================================== If @DoInteg = 1 Exec yMaint.IntegrityTesting -- ============================================================================== -- perform Update stat -- ============================================================================== If @DoUpdStats = 1 Exec yMaint.UpdateStats -- ============================================================================== -- Reorganize index -- ============================================================================== If @DoReorg = 1 Exec yMaint.ReorganizeOnlyWhatNeedToBe -- ============================================================================== -- backup start -- ============================================================================== -- on complete backups suppress old files just before backup start If @DoBackup IN ('F', 'L', 'D') Begin Exec yMaint.backups -- get its parameters from Job context... End -- If @DoBackup -- Wait for emptying the Mirroring.RestoreQueue, including log backup, to have -- also log restores into the same job. -- typically Mirroring.RestoreQueue has nothing in it when @MirrorServer is not completed -- but someone could re-run the job -- If backups are to be mirrored than we Launch a login synchronisation on the mirror server If isnull(@MirrorServer, '') <> '' And @DoBackup IN ('F', 'L', 'D') Begin Declare @JobRunning Int, @AnErrorFound Int While (1=1) -- wait all mirrored backup to be done before lauching login synchro Begin -- wait while there is still something to for process Select @JobRunning = MAX(RunningHere) Over (Partition By Null) , @AnErrorFound = MAX(AnErrorHere) Over (Partition By Null) From Mirroring.RestoreQueue CROSS APPLY (Select RunningHere=IIF(ErrorN = 0,1,0)) as RunningHere CROSS APPLY (Select AnErrorHere=IIF(ErrorN <> 0,1,0)) as AnErrorHere Where CallingJobNo=@JobNo If @JobRunning = 0 And @AnErrorFound = 1 -- Ended with error Exec yExecNLog.LogAndOrExec @context = 'Maint.YourSqlDba_DoMaint' , @Info = 'Error in Maint.YourSqlDba_DoMaint' , @err = 'Some restores to MirrorServer failed. See Mirroring.RestoreQueue' Break; If @JobRunning is NULL -- clean end Break -- otherwise wait some job are running Waitfor Delay '00:0:10' -- wait a minute for a second test End -- While -- do try to sync whatever is possible -- if some database are missing some login with default_db that points -- to a missing database are going to be rejected Exec yMirroring.LaunchLoginSync End -- Check for databases that are in SIMPLE recovery mode and not excluded form this policy -- with the @ExcDbFromPolicy_CheckFullRecoveryModel parameter If @DoBackup = 'F' Or @DoBackup = 'D' Begin Exec yMaint.CheckFullRecoveryModelPolicy End End Try Begin Catch -- make error go through the log Exec yExecNLog.LogAndOrExec @context = 'Maint.YourSqlDba_DoMaint' , @Info = 'Error in Maint.YourSqlDba_DoMaint' , @err = '?' End catch -- Close App lock that signal that Maint.YourSqlDba_DoMaint Exec Maint.SignalEndOf_YourSqlDba_DoMaint -- From here send execution report and any error message if found -- also update JobEnd Exec yExecNLog.LogAndOrExec @context = 'Maint.YourSqlDba_DoMaint' , @Info = 'End of maintenance' -- send report and avoid raising another error if no operator Select @SendOnErrorOnly = IIF(@DoBackup='L', 1, 0) If @email_Address is NOT NULL Exec yMaint.SendExecReports @email_Address = @email_Address , @MaintJobName = @MaintJobName , @StartOfMaint = @StartOfMaint , @SendOnErrorOnly = @SendOnErrorOnly -- if some error is found in job If Exists ( Select * From Maint.JobHistoryLineDetails Where JobNo=@Jobno And (Typ like 'Err%' Or (Typ like 'Status' And line like 'fail%')) ) Begin Declare @FormatMessage Nvarchar(4000) = NULL Set @FormatMessage = '--->>>>'+nchar(10)+space(300)+ 'To show error in query windows do : EXEC YourSqlDba.Maint.ShowJobErrors '+convert(nvarchar,@jobNo) +nchar(10)+space(300)+'<<<---' Raiserror (@formatMessage,11,1) End End -- Maint.YourSqlDba_DoMaint GO -- ------------------------------------------------------------------------------ -- Function that get path only from complete file path -- ------------------------------------------------------------------------------ -- @@MARK: TODO : Check for use of function parse file parts Create Or Alter Function yUtl.GetPathFromName ( @pathAndFileName nvarchar(512) ) returns nvarchar(max) as Begin Declare @filename nvarchar(512) Declare @rPathAndFileName nvarchar(512) Set @rPathAndFileName = Reverse (@PathAndFileName) Set @filename = Reverse (Stuff(@rPathAndFileName, 1, charindex('\', @rPathAndFileName)-1, '')) Return (@filename) End -- yUtl.GetPathFromName --select yUtl.GetPathFromName ('c:\backup\sub\toto.bak') -- some testing --select yUtl.GetPathFromName (NULL) -- some testing GO GRANT CONNECT TO guest; GO -- ------------------------------------------------------------------------------------------------------------- -- If paths and other params are NULL, this proc gets params from latest backup info saved by maintenance -- into Maint.LastBackupLocations -- There is no need to set a context because, it calls directly Maint.YourSqlDba_DoMaint, which does so -- ------------------------------------------------------------------------------------------------------------- -- @@MARK: Maintenance : Tool for Admin - Backups Create Or Alter Proc Maint.SaveDbOnNewFileSet @DbName nvarchar(128) , @FullBackupPath nvarchar(512) = null , @LogBackupPath nvarchar(512) = null , @oper nvarchar(128) = null , @MirrorServer sysname = '' , @MigrationTestMode Int = 0 , @ReplaceSrcBkpPathToMatchingMirrorPath nvarchar(max) = NULL , @ReplacePathsInDbFileNames nvarchar(max) = NULL , @DoBackup nvarchar(1) = 'F' , @EncryptionAlgorithm nvarchar(10) = '' , @EncryptionCertificate nvarchar(100) = '' WITH execute as Self as Begin Declare @nomTache nvarchar(512) Declare @allowed int Declare @sql nvarchar(max) set @nomTache = 'SaveDbOnNewFileSet of ' + @DbName Set @FullBackupPath = yUtl.NormalizePath(@FullBackupPath) Set @LogBackupPath = yUtl.NormalizePath(@LogBackupPath) -- Check backup permissions with original login EXECUTE AS LOGIN = ORIGINAL_LOGIN(); Set @sql = N' Use [] Set @allowed = 0 Declare @username sysname; Set @username = USER_NAME() Declare @loginName sysname; Set @loginName = SUSER_NAME() Declare @DbName sysname; Set @DbName = db_name() If @username <> @loginName Set @username = @loginName + ":" + @username If IS_MEMBER ("db_owner") = 1 OR IS_MEMBER ("db_backupoperator") = 1 Or ( select count(*) from [].sys.database_permissions where class_desc = "DATABASE" and grantee_principal_id = USER_ID() And permission_name IN ("BACKUP DATABASE", "BACKUP LOG") ) = 2 Print "User "+ @username +" autorized to do backup" Else Begin Raiserror ("User [%s] is not granted required rigths to full backups [%s]!", 11, 1, @username, @DbName) Return End Set @allowed = 1 ' Set @sql = replace (@sql, '"', '''') Set @sql = replace (@sql, '', @DbName) --print @sql Exec sp_ExecuteSql @sql, N'@allowed int output', @allowed output If @allowed = 0 Begin Return End -- Reset sp impersonation to proceed with backup REVERT If not exists(Select * from master.sys.databases where name = @DbName) Begin Raiserror ('Database [%s] doesn''t exists !', 11, 1, @DbName) Return End If @FullBackupPath is NULL Select @FullBackupPath = yUtl.GetPathFromName(lastFullBkpFile) From Maint.JobLastBkpLocations Where dbName = @DbName If @FullBackupPath IS NULL -- toujours null Begin raiserror('No maintenance done yet on this database, parameter @FullBackupPath is then mandatory',11,1) return End If @LogBackupPath is NULL Select @LogBackupPath = yUtl.GetPathFromName(lastLogBkpFile) From Maint.JobLastBkpLocations Where dbName = @DbName If @EncryptionAlgorithm is NULL Select @EncryptionAlgorithm = EncryptionAlgorithm From Maint.JobLastBkpLocations Where dbName = @DbName If @EncryptionCertificate is NULL Select @EncryptionCertificate = EncryptionCertificate From Maint.JobLastBkpLocations Where dbName = @DbName If @LogBackupPath IS NULL -- toujours null Begin If DatabasePropertyEx(@DbName, 'Recovery') = 'Simple' Set @LogBackupPath = @FullBackupPath Else Begin raiserror('No maintenance done yet on this database, parameter @LogBackupPath is then mandatory',11,1) return End End If isnull(@MirrorServer, '') = '' Select @MirrorServer = MirrorServer , @MigrationTestMode = MigrationTestMode , @ReplaceSrcBkpPathToMatchingMirrorPath = ReplaceSrcBkpPathToMatchingMirrorPath , @ReplacePathsInDbFileNames = ReplacePathsInDbFilenames From Maint.JobLastBkpLocations Where dbName = @DbName Set @oper = isnull(@oper, 'YourSQLDba_Operator') Exec Maint.YourSqlDba_DoMaint @oper = @oper , @MaintJobName = @nomTache , @DoBackup = @DoBackup , @FullBackupPath = @FullBackupPath , @LogBackupPath = @LogBackupPath , @IncDb = @DbName , @ConsecutiveDaysOfFailedBackupsToPutDbOffline = 9999 -- doesn't apply here , @MirrorServer = @MirrorServer , @MigrationTestMode = @MigrationTestMode , @ReplaceSrcBkpPathToMatchingMirrorPath = @ReplaceSrcBkpPathToMatchingMirrorPath , @ReplacePathsInDbFileNames = @ReplacePathsInDbFileNames , @ExcDbFromPolicy_CheckFullRecoveryModel = '%' , @EncryptionAlgorithm = @EncryptionAlgorithm , @EncryptionCertificate = @EncryptionCertificate End -- Maint.SaveDbOnNewFileSet GO Create Or Alter Proc Maint.SaveDbCopyOnly @DbName nvarchar(512) , @PathAndFilename nvarchar(512) -- complete file name and path must be specified , @errorN int = 0 output With Execute as Self As Begin Declare @sql nvarchar(max) Declare @cmd nvarchar(1000) Set nocount on -- Exécuter backup with initial login EXECUTE AS LOGIN = ORIGINAL_LOGIN(); Print '----------------------------------------------------------' Print 'Saving Of ' + @DbName + ' to ' + @PathAndFilename Print '----------------------------------------------------------' Print '' Set @sql = ' BACKUP DATABASE [] TO DISK ="" WITH stats=1, INIT, format, COPY_ONLY, NAME = "SaveDbCopyOnly: " ' Set @sql = Replace( @sql, '', @DbName ) Set @sql = Replace( @sql, '', @PathAndFilename ) Set @sql = Replace( @sql, '"', '''' ) Set @sql = yExecNLog.Unindent_TSQL(@sql) Print @sql Print '' Exec (@sql) Set @errorN = @@error -- Revenir à l'impersonification de la Stored Procedure REVERT End -- Maint.SaveDbCopyOnly GO -- @@MARK: Maintenance : Tool for Admin - Duplicate Db Create Or Alter Proc Maint.duplicateDb @sourceDb nvarchar(512) , @TargetDb nvarchar(512) , @PathAndFilename nvarchar(512) = NULL -- complete name including path otherwise use last backup location + @TargetDb + '.bak' , @KeepBackupFile bit = 0 -- by default destroy intermediate backup file, otherwise specify 1 to keep it With Execute as Self as Begin Declare @sql nvarchar(max) Declare @AlterLogicalFiles nvarchar(max) Declare @cmd nvarchar(1000) Declare @FullBackupPath nvarchar(512) Declare @NoSeq int Declare @name sysname Declare @separateur int Declare @physical_name nvarchar(260) Declare @ClauseMove nvarchar(max) Declare @BackupErr int Set nocount on If @PathAndFilename is NULL Begin Select @FullBackupPath = yUtl.GetPathFromName(lastFullBkpFile) From Maint.JobLastBkpLocations Where dbName = @sourceDb If @FullBackupPath IS NULL Begin raiserror('No maintenance has been done yet on this database, complete @PathAndFilename parameter',11,1) return End Set @PathAndFilename = @FullBackupPath + @TargetDb + '.Bak' End Exec Maint.SaveDbCopyOnly @DbName=@sourceDb, @PathAndFilename=@PathAndFilename, @errorN = @BackupErr output Print '' -- Stop processing it any backup error If @BackupErr <> 0 Return Create Table #dbfiles( noseq int, name sysname, physical_name nvarchar(260), separateur int) -- Set @sql = ' Use Insert Into #dbfiles (noseq, name, physical_name, separateur ) Select ROW_NUMBER() OVER(ORDER BY name), name , physical_name , Charindex("\", Reverse(physical_name)) FROM [].sys.database_files ' Set @sql = replace(@sql, '"', '''') Set @sql = replace(@sql, '', @sourceDb) Set @sql = yExecNLog.Unindent_TSQL(@sql) Exec (@sql) Print '----------------------------------------------------------------------------------------------------' Print 'Database ' + @TargetDb + ' is created from ' + @PathAndFilename Print '----------------------------------------------------------------------------------------------------' Print '' Set @AlterLogicalFiles = '' -- Generate restore command Set @sql = 'RESTORE DATABASE [] FROM DISK="" WITH stats=1,REPLACE ' Set @NoSeq = 1 While 1=1 Begin Select @name=name, @physical_name=physical_name, @separateur=separateur From #dbfiles Where noseq = @NoSeq If @@rowcount = 0 break Set @ClauseMove = ', MOVE "" TO ""' Set @ClauseMove = Replace( @ClauseMove, '', @name ) Set @ClauseMove = Replace( @ClauseMove, '', Left(@physical_name, len(@physical_name) - @separateur) ) Set @ClauseMove = Replace( @ClauseMove, '' , Replace( Right(@physical_name, @separateur), @sourceDb, @TargetDb )) Set @sql = Replace(@sql, '', @ClauseMove + nchar(13) + nchar(10) + '') If Replace(@name, @sourceDb, @TargetDb) <> @name Begin Set @AlterLogicalFiles = @AlterLogicalFiles + ' ALTER DATABASE [] MODIFY FILE (NAME="", NEWNAME="")' + nchar(13) + nchar(10) Set @AlterLogicalFiles = replace (@AlterLogicalFiles, '', @TargetDb) Set @AlterLogicalFiles = replace (@AlterLogicalFiles, '', @name) Set @AlterLogicalFiles = replace (@AlterLogicalFiles, '', Replace(@name, @sourceDb, @TargetDb) ) End Set @NoSeq = @NoSeq + 1 End Drop Table if exists #dbfiles Set @sql = Replace(@sql, '', '') Set @sql = replace (@sql, '"', '''') Set @sql = replace (@sql, '', @TargetDb) Set @sql = replace (@sql, '', @PathAndFilename) Set @sql = yExecNLog.Unindent_TSQL(@sql) Set @AlterLogicalFiles = replace (@AlterLogicalFiles, '"', '''') Set @AlterLogicalFiles = yExecNLog.Unindent_TSQL(@AlterLogicalFiles) -- Execute restore with original login permission EXECUTE AS LOGIN = ORIGINAL_LOGIN(); Print @sql Exec (@sql) Print '' If Len(@AlterLogicalFiles) > 0 Begin Print @AlterLogicalFiles Exec (@AlterLogicalFiles) End REVERT If @KeepBackupFile = 0 Begin Print '' Print '----------------------------------------------------------' Print 'Deleting database backup file ' + @PathAndFilename Print '----------------------------------------------------------' Print '' Declare @err nvarchar(4000) Exec S#.Clr_DeleteFile @PathAndFilename, @Err output If @err is not NULL Print @err End End -- Maint.duplicateDb GO -- @@MARK: Maintenance : Tool for Admin - DuplicateDb using backup history Create Or Alter Proc Maint.duplicateDbFromBackupHistory @SourceDb nvarchar(512) , @TargetDb nvarchar(512) , @DoLogBackup int = 1 , @RestoreToSimpleRecoveryModel int = 1 With Execute as Self as Begin Declare @sql nvarchar(max) Declare @AlterLogicalFiles nvarchar(max) Declare @cmd nvarchar(1000) Declare @lastFullBkpFile nvarchar(512) Declare @lastLogBkpFile nvarchar(512) Declare @NoSeq int Declare @name sysname Declare @separateur int Declare @physical_name nvarchar(260) Declare @ClauseMove nvarchar(max) Declare @RestoreLog nvarchar(max) Declare @position smallint Declare @LogBkpFile nvarchar(512) Declare @MediaSetId int Declare @EncryptionAlgorithm nvarchar(10) Declare @EncryptionCertificate nvarchar(100) Set nocount on Select @lastFullBkpFile = lastFullBkpFile , @lastLogBkpFile = lastLogBkpFile , @EncryptionAlgorithm = EncryptionAlgorithm , @EncryptionCertificate = EncryptionCertificate From Maint.JobLastBkpLocations Where dbName = @SourceDb If @SourceDb = @TargetDb Begin raiserror('@SourceDb and @TargetDb can''t be the same',11,1) return End If IsNull(@lastLogBkpFile, '') = '' Begin raiserror('No log backups has been done yet on this database. Use «DuplicateDb» stored procedure to Duplicate this database',11,1) return End -- If sprecified do a last log backup for the database If @DoLogBackup = 1 Begin Print '----------------------------------------------------------------------------------------------------' Print 'Doing a log backup on source database « ' + @SourceDb + '» to have the most up to date data' Print '----------------------------------------------------------------------------------------------------' Print '' Set @sql = yMaint.MakeBackupCmd( @SourceDb, 'L', @lastLogBkpFile, 0, Null, @EncryptionAlgorithm, @EncryptionCertificate) Exec(@sql) End Create Table #dbfiles( noseq int, name sysname, physical_name nvarchar(260), separateur int) -- Set @sql = ' Use Insert Into #dbfiles (noseq, name, physical_name, separateur ) Select ROW_NUMBER() OVER(ORDER BY name), name , physical_name , Charindex("\", Reverse(physical_name)) FROM [].sys.database_files ' Set @sql = replace(@sql, '"', '''') Set @sql = replace(@sql, '', @sourceDb) Set @sql = yExecNLog.Unindent_TSQL(@sql) Exec (@sql) Print '----------------------------------------------------------------------------------------------------' Print 'Database ' + @TargetDb + ' is created from ' + @SourceDb + ' backup chain' Print '----------------------------------------------------------------------------------------------------' Print '' Set @AlterLogicalFiles = '' -- Generate restore command Set @sql = 'RESTORE DATABASE [] FROM DISK="" WITH stats=1,REPLACE,NORECOVERY Restore Log [] With Recovery ' Set @NoSeq = 1 While 1=1 Begin Select @name=name, @physical_name=physical_name, @separateur=separateur From #dbfiles Where noseq = @NoSeq If @@rowcount = 0 break Set @ClauseMove = ', MOVE "" TO ""' Set @ClauseMove = Replace( @ClauseMove, '', @name ) Set @ClauseMove = Replace( @ClauseMove, '' , Left(@physical_name, len(@physical_name) - @separateur) ) Set @ClauseMove = Replace( @ClauseMove, '' , Replace( Right(@physical_name, @separateur), @sourceDb, @TargetDb )) Set @sql = Replace(@sql, '', @ClauseMove + nchar(13) + nchar(10) + '') If Replace(@name, @sourceDb, @TargetDb) <> @name Begin Set @AlterLogicalFiles = @AlterLogicalFiles + ' ALTER DATABASE [] MODIFY FILE (NAME="", NEWNAME="")' + nchar(13) + nchar(10) Set @AlterLogicalFiles = replace (@AlterLogicalFiles, '', @TargetDb) Set @AlterLogicalFiles = replace (@AlterLogicalFiles, '', @name) Set @AlterLogicalFiles = replace ( @AlterLogicalFiles, '' , Replace(@name, @sourceDb, @TargetDb) ) End Set @NoSeq = @NoSeq + 1 End -- Find all log backups associated with the full backup Set @MediaSetId = 0 While 1=1 Begin Select Top 1 @MediaSetId= bm.media_set_id, @LogBkpFile = bm.physical_device_name From ( Select bs.database_name, bs.first_lsn From YourSQLDba.Maint.JobLastBkpLocations lb join msdb.dbo.backupset bs on bs.database_name = lb.dbName collate database_default And RIGHT( bs.name, Len(lb.lastFullBkpFile)) = lb.lastFullBkpFile collate database_default Where lb.lastFullBkpFile = @lastFullBkpFile And (bs.name like 'YourSqlDba%' or bs.name like 'SaveDbOnNewFileSet%') And bs.type = 'D' ) X Join msdb.dbo.backupset bs On bs.database_name = X.database_name And bs.database_backup_lsn = X.first_lsn Join msdb.dbo.backupmediafamily bm On bm.media_set_id = bs.media_set_id Where bs.type = 'L' And bm.media_set_id > @MediaSetId If @@ROWCOUNT = 0 Break -- Generate instruction to restore all logs backups of this database -- if any error they are displayed from the called proc with a raiserror Declare @rc int Exec @rc=yMaint.CollectBackupHeaderInfoFromBackupFile @LogBkpFile If @rc <> 0 Return -- Restore all log backups Set @position = 0 while 1=1 Begin Select Top 1 @position = Position From Maint.TemporaryBackupHeaderInfo Where spid = @@spid And BackupType = 2 And Position > @position Order by Position If @@rowcount= 0 break Set @RestoreLog = 'Restore Log [] From Disk="" With FILE=, NoRecovery' Set @RestoreLog = Replace(@RestoreLog, '', @TargetDb) Set @RestoreLog = Replace(@RestoreLog, '', @LogBkpFile) Set @RestoreLog = Replace(@RestoreLog, '', Convert(nvarchar(255), @position)) Set @Sql = replace(@Sql, '', @RestoreLog + Char(13) + Char(10) + '' ) End End Drop Table if exists #dbfiles Set @sql = replace(@sql, '', '') Set @sql = Replace(@sql, '', '') Set @sql = replace (@sql, '"', '''') Set @sql = replace (@sql, '', @TargetDb) Set @sql = replace (@sql, '', @lastFullBkpFile) Set @sql = yExecNLog.Unindent_TSQL(@sql) Set @AlterLogicalFiles = replace (@AlterLogicalFiles, '"', '''') Set @AlterLogicalFiles = yExecNLog.Unindent_TSQL(@AlterLogicalFiles) -- Execute restore with original login permission EXECUTE AS LOGIN = ORIGINAL_LOGIN(); Begin Try --Print @sql Exec (@sql) End Try Begin Catch Declare @Info nvarchar(max) Set @Info = 'Error_no: '+ Convert(varchar(10), ERROR_NUMBER())+','+ 'Severity: '+ Convert(varchar(10), ERROR_SEVERITY())+','+ 'Status: '+ Convert(varchar(10), ERROR_STATE())+','+ 'LineNo: '+ Convert(varchar(10), ERROR_LINE())+','+ 'Msg: '+ ERROR_MESSAGE() raiserror(@Info,11,1) return End Catch REVERT Print '' If Len(@AlterLogicalFiles) > 0 Begin --Print @AlterLogicalFiles Exec (@AlterLogicalFiles) End -- Ensure database is in SIMPLE recovery model if parameter @RestoreToSimpleRecoveryModel is set to 1 If @RestoreToSimpleRecoveryModel = 1 Begin Set @sql = 'ALTER DATABASE [] SET RECOVERY SIMPLE' Set @sql = REPLACE(@sql, '', @TargetDb) Exec (@sql) End End -- Maint.duplicateDbFromBackupHistory GO -- @@MARK: Maintenance : Tool to restoreDb with move clause Create Or Alter Proc Maint.RestoreDb @TargetDb nvarchar(512) -- database name to restore , @PathAndFilename nvarchar(512) -- complete file and path name must be given , @ReplaceExistingDb int = 0 -- set to 1 to overwrite existing database «REPLACE option» With Execute as Self as Begin If exists (select * from master.sys.databases where name = @TargetDb) and @ReplaceExistingDb = 0 begin Print 'Database ' + @TargetDb Print 'already exists and you did not allow to replace it with parameter @ReplaceExistingDb' Print 'Restore action is cancelled' Return end Declare @pathData nvarchar(512) Declare @pathLog nvarchar(512) Declare @FileType char(1) Declare @sql nvarchar(max) Declare @FileId int Declare @NoSeq int Declare @LogicalName sysname Declare @PhysicalName sysname Declare @DbName sysname Declare @NewPhysicalName sysname Declare @ClauseMove nvarchar(max) Declare @AlterLogicalFiles nvarchar(max) declare @rc int Set nocount on -- get default data and log location Exec master.dbo.xp_instance_regread N'HKEY_LOCAL_MACHINE' , N'Software\Microsoft\MSSQLServer\MSSQLServer' , N'DefaultData' , @pathData OUTPUT Exec master.dbo.xp_instance_regread N'HKEY_LOCAL_MACHINE' , N'Software\Microsoft\MSSQLServer\MSSQLServer' , N'DefaultLog' , @pathLog OUTPUT -- if default data and log location is not specified in server properties, Use master database file location If @pathData Is Null Select Top 1 @pathData = Left( physical_name, Len(physical_name) - Charindex('\', Reverse(physical_name))) FROM master.sys.database_files Where type = 0 If @pathLog Is Null Select Top 1 @pathLog = Left( physical_name, Len(physical_name) - Charindex('\', Reverse(physical_name))) FROM master.sys.database_files Where type = 1 -- recover database name from datase backup file -- if there is any errors thay are displayed from CollectBackupHeaderInfoFromBackupFile with a raiserrpr Exec @rc=yMaint.CollectBackupHeaderInfoFromBackupFile @PathAndFilename If @rc <> 0 Return Select @DbName = DatabaseName From Maint.TemporaryBackupHeaderInfo Where spid = @@spid Exec @rc=yMaint.CollectBackupFileListFromBackupFile @PathAndFilename If @rc <> 0 Return Print '----------------------------------------------------------------------------------------------------' Print 'Database ' + @TargetDb + ' is created from ' + @PathAndFilename Print '----------------------------------------------------------------------------------------------------' Print '' Set @AlterLogicalFiles = '' -- Generate restore command Set @sql = 'RESTORE DATABASE [] FROM DISK="" WITH ' Set @FileId = -1 Set @NoSeq = 0 While 1=1 Begin Select Top 1 @FileType = Type , @FileId = FileId , @LogicalName=LogicalName , @PhysicalName=RIGHT(PhysicalName, Charindex('\', Reverse(PhysicalName)) - 1) From Maint.TemporaryBackupFileListInfo Where Spid = @@spid And FileId > @FileId Order by spid, FileId If @@rowcount = 0 break Set @ClauseMove = 'MOVE "" TO "\"' Set @ClauseMove = Replace( @ClauseMove, '', @LogicalName ) If Replace(@LogicalName, @DbName, @TargetDb) <> @LogicalName Begin Set @AlterLogicalFiles = @AlterLogicalFiles + ' ALTER DATABASE [] MODIFY FILE (NAME="", NEWNAME="")' + nchar(13) + nchar(10) Set @AlterLogicalFiles = replace (@AlterLogicalFiles, '', @TargetDb) Set @AlterLogicalFiles = replace (@AlterLogicalFiles, '', @LogicalName) Set @AlterLogicalFiles = replace ( @AlterLogicalFiles , '' , Replace(@LogicalName, @DbName, @TargetDb) ) End -- Try only a replace of the old database name by the new database name -- in filename on disk. If it doesn't work we will have no choice to generate distinct names If Charindex(@DbName, @PhysicalName) > 0 Begin Set @ClauseMove = Replace( @ClauseMove , '' , Case When @FileType = 'L' Then @pathLog Else @pathData End ) Set @ClauseMove = Replace( @ClauseMove , '' , Replace(@PhysicalName, @DbName, @TargetDb) ) End Else Begin -- Log file name will be renamed by database name followed by «_Log.ldf» If @FileType = 'L' Begin Set @ClauseMove = Replace( @ClauseMove, '', @pathLog ) Set @ClauseMove = Replace( @ClauseMove, '', @TargetDb + '_Log.ldf' ) End -- Data file name are named from database name -- A sequential number is added for databases that have many file name If @FileType = 'D' Begin Set @ClauseMove = Replace( @ClauseMove, '', @pathData ) If @NoSeq = 0 Set @ClauseMove = Replace( @ClauseMove, '', @TargetDb + '.mdf' ) Else Set @ClauseMove = Replace( @ClauseMove , '' , @TargetDb + convert(nvarchar, @NoSeq) + '.ndf') Set @NoSeq = @NoSeq + 1 End -- Catalog file for full text search are named from database name -- with extension «.FtCatalog» plus a sequential number If @FileType = 'F' Begin Set @ClauseMove = Replace( @ClauseMove, '', @pathData ) Set @ClauseMove = Replace( @ClauseMove , '' , @TargetDb + '.FTCatalog' + convert(nvarchar, @NoSeq) ) Set @NoSeq = @NoSeq + 1 End End Set @sql = Replace( @sql , '' , @ClauseMove + nchar(13) + nchar(10) + ',') End Set @sql = Replace(@sql, ',', '') Set @sql = replace (@sql, '"', '''') Set @sql = replace (@sql, '', @TargetDb) Set @sql = replace (@sql, '', @PathAndFilename) Set @sql = replace ( @sql , '' , Case When @ReplaceExistingDb = 1 Then ', REPLACE' Else '' End ) Set @sql = yExecNLog.Unindent_TSQL(@sql) Set @AlterLogicalFiles = replace (@AlterLogicalFiles, '"', '''') Set @AlterLogicalFiles = yExecNLog.Unindent_TSQL(@AlterLogicalFiles) -- Execute a Restore with original login's permissions EXECUTE AS LOGIN = ORIGINAL_LOGIN(); Print @sql Exec (@sql) Print '' If Len(@AlterLogicalFiles) > 0 Begin Print @AlterLogicalFiles Exec (@AlterLogicalFiles) End REVERT End -- Maint.RestoreDb GO GRANT connect to guest GO grant execute on Maint.SaveDbOnNewFileSet to guest GO grant execute on Maint.SaveDbCopyOnly to guest GO grant execute on Maint.DuplicateDb to guest GO grant execute on Maint.DuplicateDbFromBackupHistory to guest GO grant execute on Maint.RestoreDb to guest -- some tests --Exec Maint.SaveDbOnNewFileSet -- @DbName = 'RegardMaurice' --, @FullBackupPath = null --, @LogBackupPath = null GO -- ------------------------------------------------------------------------------ -- Procedure to visualize last statement running or ran -- ------------------------------------------------------------------------------ -- @@MARK: TODO : Check for use Create Or Alter Function yUtl.TextSplitInRows ( @sql nvarchar(max) ) RETURNS @TxtSql TABLE (i int identity, txt nvarchar(max)) AS Begin declare @i int, @d Datetime If @i > 0 Insert into @txtSql (txt) values ('-- Seq:'+ltrim(str(@i))+ ' Time:'+convert(nvarchar(20), @d, 120) + ' ' + replicate('-', 10) ) If @sql is null Or @sql = '' Begin Insert into @txtSql (txt) values ('') return End declare @Start int, @End Int, @line nvarchar(max), @EOLChars int Set @Start = 1 Set @End=0 While(@End < len(@sql)) Begin ;With NearestEndOfLines as ( Select charindex(nchar(13)+nchar(10), @sql, @Start) as EOLPos, 2 as EOLChars union All Select charindex(nchar(13), @sql, @Start) as EOLPos, 1 as EOLChars union All Select charindex(nchar(10), @sql, @Start) as EOLPos, 1 as EOLChars ) Select top 1 @End = Case When EOLPos > 0 Then EOLPos Else LEN(@Sql) End -- End of String @Sql , @EOLChars = Case When EOLPos > 0 Then EOLChars Else 1 End -- EOL length From NearestEndOfLines Order by EOLPos, EolChars Desc -- get nearest EndOfLines Set @line = Substring(@sql, @Start, @End-@Start+@EOLChars) Set @Start = @End+@EOLChars Insert into @txtSql (txt) values (replace (replace (@line, nchar(10), ''), nchar(13), '')) End RETURN End -- yUtl.TextSplitInRows -- --------------------------------------------------------------------------------------- -- Procedure to show maintenance log history -- --------------------------------------------------------------------------------------- GO -- @@MARK: TODO : To remove soon Create Or Alter Proc Maint.ShowHistory @JobNo Int = NULL , @FilterErr Int = 0 , @DispLimit Int = NULL , @Diag int = 0 as Begin Print 'This proc is deprecated, use HistoryView as in this example: For more documentation see https://tinyurl.com/YourSqlDbaHistoryView' Print 'Job info is displayed from a datetime range that must be compatible with format 120, 121' print 'Select cmdStartTime, JobNo, seq, Typ, line, Txt, MaintJobName, MainSqlCmd, Who, Prog, Host, SqlAgentJobName, JobId, JobStart, JobEnd ' print 'From ' print ' Maint.MaintenanceEnums as E -- E.HV$ShowErrOnly=1, E.HV$ShowAll=0' print ' cross apply YourSQLDba.Maint.HistoryView(''2022-11-21 00:00:00.690'', ''2022-11-21 00:03:14.980'', E.HV$ShowAll) ' print 'Order By cmdStartTime, Seq, TypSeq, Typ, Line' End -- Maint.ShowHistory GO -- @@MARK: TODO : Question the need... Create Or Alter Proc Install.AddOrReplaceMaintenance @JobNameSuffix nvarchar(512) = '' , @FullBackupPath nvarchar(512) , @LogBackupPath nvarchar(512) , @ConsecutiveDaysOfFailedBackupsToPutDbOffline Int , @FullMaintenanceScript Nvarchar(max) = NULL Output , @LogBackupScript Nvarchar(max) = NULL Output As --Declare @FullBackupPath nvarchar(512) --Set @FullBackupPath = 'C:\SQL2005Backups\' -- Begin --------------------------------------------------------------------------------------- -- Setup of 2 maintenance tasks --------------------------------------------------------------------------------------- Declare @nomJob Sysname If right(@FullBackupPath, 1)<>'\' Set @FullBackupPath = @FullBackupPath + '\' If right(@LogBackupPath, 1)<>'\' Set @LogBackupPath = @LogBackupPath + '\' Declare @JobLogFile sysname Set @JobLogFile = @FullBackupPath + 'MaintenanceReport.txt' Declare @svrName nvarchar(30) set @svrName = convert(nvarchar(30), serverproperty('servername')) Select @svrname Declare @sql nvarchar(max) DECLARE @jobId uniqueidentifier set @jobId = NULL Set @nomJob = N'YourSQLDba_FullBackups_And_Maintenance'+ISNULL(@JobNameSuffix, '') Declare @operator sysname Select @jobId = job_Id, @operator = OP.name from msdb.dbo.sysjobs J left join msdb.dbo.sysoperators OP On Op.Id = notify_email_operator_id where J.name = @nomJob If @@rowcount = 0 Begin if @operator is NULL Set @operator = 'YourSQLDba_Operator' Print 'Adding job maintenance task '+@nomJob EXEC msdb.dbo.sp_add_job @job_name = @nomJob, @enabled = 1, @notify_level_eventlog = 0, @notify_level_email = 2, @notify_email_operator_name = @operator, @notify_level_netsEnd = 2, @notify_level_page = 2, @delete_level = 0, @description = N'Maintenance: Integrity tests, update statistics, index reorg, Full backups', @category_name = N'Database Maintenance', @owner_login_name = N'YourSQLDba', @job_id = @jobId OUTPUT Print 'Maintenance server parameter setup ' exec msdb.dbo.sp_add_jobserver @job_id = @jobId, @server_name = @svrName End Else Begin if @operator is NULL Set @operator = 'YourSQLDba_Operator' Print 'Updating job maintenance task '+@nomJob EXEC msdb.dbo.sp_update_job @job_name = @nomJob, @enabled = 1, @notify_level_eventlog = 0, @notify_level_email = 2, @notify_email_operator_name = @operator, @notify_level_netsEnd = 2, @notify_level_page = 2, @delete_level = 0, @description = N'Maintenance: Integrity tests, update statistics, index reorg, Full backups', @category_name = N'Database Maintenance', @owner_login_name = N'YourSQLDba' End set @sql = N' exec Maint.YourSqlDba_DoMaint @oper = "" , @MaintJobName = "YourSQLDba: DoInteg,DoUpdateStats,DoReorg,Full backups" , @DoInteg = 1 , @DoUpdStats = 1 , @DoReorg = 1 , @DoBackup = "F" , @FullBackupPath = "" , @LogBackupPath = "" -- Flush database backups older than the number of days , @FullBkpRetDays = 0 -- Flush log backups older than the number of days , @LogBkpRetDays = 8 -- Spread Update Stats over 7 days , @SpreadUpdStatRun = 7 -- Spread Check DB without "PHYSICAL_ONLY" over 7 days , @SpreadCheckDb = 7 -- Maximum number of consecutive days of failed full backups allowed -- for a database before putting that database (Offline). , @ConsecutiveDaysOfFailedBackupsToPutDbOffline = -- Each database inclusion filter must be on its own line between the following quote pair , @IncDb = " " -- Each database exclusion filter must be on its own line between the following quote pair , @ExcDb = " " -- Each database exclusion filter must be on its own line between the following quote pair -- exclusion here applies to DB for which we dont''t want to check the recovery model , @ExcDbFromPolicy_CheckFullRecoveryModel = " " , @EncryptionAlgorithm = "" , @EncryptionCertificate = "" ' Set @sql = replace (@sql, '"', '''') Set @sql = replace (@sql, '', @FullBackupPath) Set @sql = replace (@sql, '', @LogBackupPath) Set @sql = replace (@sql, '', @operator) Set @sql = replace ( @sql , '' , convert(nvarchar(10),@ConsecutiveDaysOfFailedBackupsToPutDbOffline)) Set @sql = yExecNLog.Unindent_TSQL(@sql) Set @FullMaintenanceScript = @Sql Declare @step_name sysname Declare @on_success_action int Declare @on_success_step_id int Declare @on_fail_action int Declare @on_fail_step_id int Declare @step_id Int Declare @schedule_id int Set @step_name = N'Exec YourSQLDba: Maintenance and Full Backups' If Not Exists(select * from msdb.dbo.sysjobsteps where job_Id = @jobId And step_name = @step_name) Begin Print 'Step Add '+@step_name EXEC msdb.dbo.sp_add_jobstep @job_name = @nomJob, @step_name = @step_name , @step_id = 1, @cmdexec_success_code = 0, @on_success_action = 1, @on_fail_action = 2, @retry_attempts = 0, @retry_interval = 0, @os_run_priority = 0, @subsystem = N'TSQL', @command = @sql, @database_name = N'YourSQLDba', @output_file_name = @JobLogFile , @flags = 0 -- overwrite log file End Else Begin Print 'Step Update '+@step_name select @step_id = step_id , @on_success_action = on_success_action , @on_fail_action = on_fail_action , @on_success_step_id = on_success_step_id , @on_fail_Step_id = on_fail_Step_id from msdb.dbo.sysjobsteps where job_Id = @jobId And step_name = @step_name EXEC msdb.dbo.sp_update_jobstep @job_name = @nomJob, @step_name = @step_name , @step_id = @step_id, @cmdexec_success_code = 0, @on_success_action = @on_success_action, @on_success_step_id = @on_success_step_id, @on_fail_action = @on_fail_action, @on_fail_Step_id = @on_fail_Step_id, @retry_attempts = 0, @retry_interval = 0, @os_run_priority = 0, @subsystem = N'TSQL', @command = @sql, @database_name = N'YourSQLDba', @output_file_name = @JobLogFile , @flags = 0 -- overwrite log file End Declare @schedule_name sysname Set @schedule_name = N'Schedule for Maintenance and full backups' Set @schedule_id = NULL Select @schedule_id = s.schedule_id From msdb.dbo.sysschedules s Join msdb.dbo.sysjobschedules js On s.schedule_id = js.schedule_id And js.job_id = @jobId Where name = @schedule_name If @schedule_id Is Null Begin Declare @startDate Int = convert(int,Convert(nvarchar, getdate()+1, 112)) Print 'Adding Schedule '+ @schedule_name Exec msdb.dbo.sp_add_schedule @schedule_name = @schedule_name , @enabled = 1 , @freq_type = 8 , @freq_interval = 127 , @freq_subday_type = 1 , @freq_subday_interval = 0 , @freq_relative_interval = 0 , @freq_recurrence_factor = 1 , @active_start_date = @startDate , @active_end_date = 99991231 , @active_start_time = 000000 , @active_end_time = 235959 , @owner_login_name = 'YourSQLDba' , @schedule_id = @schedule_id OUTPUT EXEC msdb.dbo.sp_attach_schedule @job_name = @nomJob , @schedule_id = @schedule_id End Else Begin Print 'Schedule update '+ @schedule_name Exec msdb.dbo.sp_update_schedule @schedule_id = @schedule_id , @enabled = 1 , @freq_type = 8 , @freq_interval = 127 , @freq_subday_type = 1 , @freq_subday_interval = 0 , @freq_relative_interval = 0 , @freq_recurrence_factor = 1 , @active_start_date = NULL , @active_end_date = 99991231 , @active_start_time = 000000 , @active_end_time = 235959 , @owner_login_name = 'YourSQLDba' End -- --------------------------------------------------------------------------------------------------- set @jobId = NULL Set @nomJob = N'YourSQLDba_LogBackups'+ISNULL(@JobNameSuffix, '') Select @jobId = job_Id, @operator = OP.name from msdb.dbo.sysjobs J left join msdb.dbo.sysoperators OP On Op.Id = notify_email_operator_id where J.name = @nomJob If @@rowcount = 0 Begin if @operator is NULL Set @operator = 'YourSQLDba_Operator' Print 'Adding job maintenance task '+@nomJob EXEC msdb.dbo.sp_add_job @job_name = @nomJob, @enabled = 1, @notify_level_eventlog = 0, @notify_level_email = 2, @notify_email_operator_name = @operator, @notify_level_netsEnd = 2, @notify_level_page = 2, @delete_level = 0, @description = N'Log backups', @category_name = N'Database Maintenance', @owner_login_name = N'YourSQLDba', @job_id = @jobId OUTPUT Print 'Maintenance task''s server parameter setup '+@nomJob exec msdb.dbo.sp_add_jobserver @job_id = @jobId, @server_name = @svrName End Else Begin if @operator is NULL Set @operator = 'YourSQLDba_Operator' Print 'Updating job maintenance task '+@nomJob EXEC msdb.dbo.sp_update_job @job_name = @nomJob, @enabled = 1, @notify_level_eventlog = 0, @notify_level_email = 2, @notify_email_operator_name = @operator, @notify_level_netsEnd = 2, @notify_level_page = 2, @delete_level = 0, @description = N'Log backups', @category_name = N'Database Maintenance', @owner_login_name = N'YourSQLDba' End set @sql = N' exec Maint.YourSqlDba_DoMaint @oper = "" , @MaintJobName = ''Log backups'' , @DoBackup = ''L'' , @FullBackupPath = "" , @LogBackupPath = "" -- Specify to user that full database backups are mandatory before log backups , @NotifyMandatoryFullDbBkpBeforeLogBkp = 1 , @BkpLogsOnSameFile = 1 -- Each database inclusion filter must be on its own line between the following quote pair , @IncDb = " " -- Each database exclusion filter must be on its own line between the following quote pair , @ExcDb = " " , @EncryptionAlgorithm = "" , @EncryptionCertificate = "" ' Set @sql = replace (@sql, '"', '''') Set @sql = replace (@sql, '', @FullBackupPath) Set @sql = replace (@sql, '', @LogBackupPath) Set @sql = replace (@sql, '', @operator) Set @sql = yExecNLog.Unindent_TSQL(@sql) Set @LogBackupScript = @Sql Set @step_name = N'Exec YourSQLDba_DoMaint Log Backups' If Not Exists(select * from msdb.dbo.sysjobsteps where job_Id = @jobId And step_name = @step_name) Begin Print 'Step Add '+@step_name EXEC msdb.dbo.sp_add_jobstep @job_name = @nomJob, @step_name = @step_Name, @step_id = 1, @cmdexec_success_code = 0, @on_success_action = 1, @on_fail_action = 2, @retry_attempts = 0, @retry_interval = 0, @os_run_priority = 0, @subsystem = N'TSQL', @command = @sql, @database_name = N'YourSQLDba', @output_file_name = @JobLogFile , @flags = 2 -- append to the log file End Else Begin Print 'Step Update '+@step_name select @step_id = step_id , @on_success_action = on_success_action , @on_fail_action = on_fail_action , @on_success_step_id = on_success_step_id , @on_fail_Step_id = on_fail_Step_id from msdb.dbo.sysjobsteps where job_Id = @jobId And step_name = @step_name EXEC msdb.dbo.sp_update_jobstep @job_name = @nomJob, @step_name = @step_name , @step_id = @step_id, @cmdexec_success_code = 0, @on_success_action = @on_success_action, @on_success_step_id = @on_success_step_id, @on_fail_action = @on_fail_action, @on_fail_Step_id = @on_fail_Step_id, @retry_attempts = 0, @retry_interval = 0, @os_run_priority = 0, @subsystem = N'TSQL', @command = @sql, @database_name = N'YourSQLDba', @output_file_name = @JobLogFile , @flags = 2 -- append output to previous job step End Set @schedule_name = N'Schedule for Log backups' Set @schedule_id = NULL Select @schedule_id = s.schedule_id From msdb.dbo.sysschedules s Join msdb.dbo.sysjobschedules js On s.schedule_id = js.schedule_id And js.job_id = @jobId Where name = @schedule_name If @schedule_id Is Null Begin Print 'Adding Schedule '+ @schedule_name Exec msdb.dbo.sp_add_schedule @schedule_name = @schedule_name , @enabled = 1 , @freq_type = 8 , @freq_interval = 127 , @freq_subday_type = 4 , @freq_subday_interval = 15 , @freq_relative_interval = 0 , @freq_recurrence_factor = 1 , @active_start_date = NULL , @active_end_date = 99991231 , @active_start_time = 001000 , @active_end_time = 235959 , @owner_login_name = 'YourSQLDba' , @schedule_id = @schedule_id OUTPUT EXEC msdb.dbo.sp_attach_schedule @job_name = @nomJob , @schedule_id = @schedule_id End Else Begin Print 'Schedule update '+ @schedule_name Exec msdb.dbo.sp_Update_schedule @schedule_id = @schedule_id , @enabled = 1 , @freq_type = 8 , @freq_interval = 127 , @freq_subday_type = 4 , @freq_subday_interval = 15 , @freq_relative_interval = 0 , @freq_recurrence_factor = 1 , @active_start_date = NULL , @active_end_date = 99991231 , @active_start_time = 001000 , @active_end_time = 235959 , @owner_login_name = 'YourSQLDba' End End --------------------------------------------------------------------------------------------- -- To be done once when YourSqlDba script is run for the first time on a server --------------------------------------------------------------------------------------------- GO -- @@MARK: Install YourSqlDba Create Or Alter Proc Install.InitialSetupOfYourSqlDba @FullBackupPath nvarchar(512) = NULL , @LogBackupPath nvarchar(512) = NULL , @email nvarchar(512) , @sourceEmail nvarchar(512) = '' , @SmtpMailServer nvarchar(128) , @SmtpMailPort int = 25 , @SmtpMailEnableSSL bit = 0 , @EmailServerAccount nvarchar(512) = NULL , @EmailServerPassword nvarchar(512) = NULL , @ConsecutiveDaysOfFailedBackupsToPutDbOffline Int , @FullMaintenanceScript nvarchar(max) = '' Output , @LogBackupScript nvarchar(max) = '' Output As --Declare @FullBackupPath nvarchar(512) --Set @FullBackupPath = 'C:\SQL2005Backups\' -- Begin Set nocount on If @ConsecutiveDaysOfFailedBackupsToPutDbOffline < 1 Begin print 'YourSQLDba initial configuration failed' print '' print 'You must read the description of the @ConsecutiveDaysOfFailedBackupsToPutDbOffline' print 'parameter for the InitialSetupOfYourSqlDba procedure in the "YourSQLDba guide".' Return End Declare @oper sysname Set @oper = 'YourSQLDba_Operator' ------------------------------------------------------------- -- database mail setup for YourSQLDba ------------------------------------------------------------- If not Exists ( Select * From sys.configurations Where name = 'show advanced options' And value_in_use = 1 ) Begin EXEC sp_configure 'show advanced options', 1 Reconfigure End -- To enable the feature. If not Exists ( Select * From sys.configurations Where name = 'Database Mail XPs' And value_in_use = 1 ) Begin EXEC sp_configure 'Database Mail XPs', 1 Reconfigure End DECLARE @profile_name sysname , @account_name sysname , @SMTP_servername sysname , @email_address NVARCHAR(128) , @display_name NVARCHAR(128) , @rv INT -- Set profil name here SET @profile_name = 'YourSQLDba_EmailProfile'; SET @account_name = lower(replace(convert(sysname, Serverproperty('servername')), '\', '.')) -- Init email account name If @sourceEmail = '' Begin SET @email_address = lower(@account_name+'@YourSQLDba.com') SET @display_name = lower(convert(sysname, Serverproperty('servername'))+' : YourSQLDba ') End Else Begin SET @email_address = @sourceEmail SET @display_name = @sourceEmail End -- if account exists remove it If Exists (Select * From msdb.dbo.sysmail_account WHERE name = @account_name ) Begin Exec @rv = msdb.dbo.sysmail_delete_account_sp @account_name = @account_name If @rv <> 0 Begin Raiserror('Cannot remove existing database mail account (%s)', 16, 1, @account_Name); return End End; -- if profile exists remove it If Exists (Select * From msdb.dbo.sysmail_profile WHERE name = @profile_name) Begin Exec @rv = msdb.dbo.sysmail_delete_profile_sp @profile_name = @profile_name If @rv <> 0 Begin Raiserror('Cannot remove existing database mail profile (%s)', 16, 1, @profile_name); return End End -- Proceed email config in a single tx to leave nothing inconsistent Begin transaction ; declare @profileId Int -- Add the profile Exec @rv = msdb.dbo.sysmail_add_profile_sp @profile_name = @profile_name If @rv<>0 Begin Raiserror('Failure to create database mail profile (%s).', 16, 1, @profile_Name); Rollback transaction; return End; -- Grant access to the profile to the DBMailUsers role EXECUTE msdb.dbo.sysmail_add_principalprofile_sp @profile_name = @profile_name, @principal_name = 'public', @is_default = 1 ; -- Add the account Exec @rv = msdb.dbo.sysmail_add_account_sp @account_name = @account_name , @email_address = @email_address , @display_name = @display_name , @mailserver_name = @SmtpMailServer , @port = @SmtpMailPort , @enable_ssl = @SmtpMailEnableSSL , @username = @EmailServerAccount , @password = @EmailServerPassword; If @rv<>0 Begin Raiserror('Failure to create database mail account (%s).', 16, 1, @account_Name) ; Rollback transaction; return End -- Associate the account with the profile. Exec @rv = msdb.dbo.sysmail_add_profileaccount_sp @profile_name = @profile_name , @account_name = @account_name , @sequence_number = 1 ; If @rv<>0 Begin Raiserror('Failure when adding account (%s) to profile (%s).', 16, 1, @account_name, @profile_Name) ; Rollback transaction; return End; COMMIT transaction; EXEC msdb.dbo.sp_set_sqlagent_properties @email_save_in_sent_folder = 1 EXEC master.dbo.xp_instance_regwrite N'HKEY_LOCAL_MACHINE' , N'SOFTWARE\Microsoft\MSSQLServer\SQLServerAgent' , N'UseDatabaseMail' , N'REG_DWORD' , 1 EXEC master.dbo.xp_instance_regwrite N'HKEY_LOCAL_MACHINE' , N'SOFTWARE\Microsoft\MSSQLServer\SQLServerAgent' , N'DatabaseMailProfile' , N'REG_SZ' , @profile_Name Declare @NetStop sysname Declare @SqlAgentServiceName sysname Set @SqlAgentServiceName = convert(sysname, Serverproperty('instancename')) If @SqlAgentServiceName IS NOT NULL Set @NetStop = 'Net Stop "SQLAgent$'+@SqlAgentServiceName+'"' Else Set @NetStop = 'Net Stop SQLSERVERAGENT ' Declare @NetStart sysname If @SqlAgentServiceName IS NOT NULL Set @NetStart = 'Net Start "SQLAgent$'+@SqlAgentServiceName+'"' Else Set @NetStart = 'Net Start SQLSERVERAGENT ' -- If XP_cmdshell is activated temporary to restart automatically SQL Agent Exec yMaint.SaveXpCmdShellStateAndAllowItTemporary Begin try -- intercepte erreurs pour être sur que restore va se faire Select 'Review your job parameters if job already existed ' As Msg print @netstop EXEC xp_cmdShell @netStop, 'NO_OUTPUT' print @netstart EXEC xp_cmdShell @netStart, 'NO_OUTPUT' end try begin catch declare @error nvarchar(max) set @error = str(error_number()) + ERROR_MESSAGE () print @error end catch Exec yMaint.RestoreXpCmdShellState DECLARE @retval INT SELECT @retval = 0 while (1=1) Begin EXECUTE master.dbo.xp_sqlagent_is_starting @retval OUTPUT If @retval <> 1 Break print 'SQL Server Agent is starting. InitialSetupOfYourSqlDba is waiting for 1 second.' waitfor delay '00:00:01' end If exists(SELECT * FROM msdb.dbo.sysoperators Where name = @oper) Exec msdb.dbo.sp_delete_operator @name = @oper; Exec msdb.dbo.sp_add_operator @name = @oper, @email_address = @email Declare @pathBkp Nvarchar(512); Exec master.dbo.xp_instance_regread N'HKEY_LOCAL_MACHINE' , N'Software\Microsoft\MSSQLServer\MSSQLServer' , N'BackupDirectory' , @pathBkp OUTPUT , 'no_output' -- prendre répertoire SQL backup par défaut si pas complété Select @FullBackupPath = ISNULL(@FullBackupPath, @pathBkp), @LogBackupPath = ISNULL(@LogBackupPath, @pathBkp) Exec Install.AddOrReplaceMaintenance '', @FullBackupPath, @LogBackupPath, @ConsecutiveDaysOfFailedBackupsToPutDbOffline, @FullMaintenanceScript Output, @LogBackupScript Output Declare @Msg Nvarchar(max) Set @msg = '
Initial setup of YourSqlDba : Review YourSqlDba jobs step


Full maintenance job step:

#FullMaintenanceScript#


Log backups jobs:

#LogBackupScript#
' Set @msg = replace(@Msg, '#FullMaintenanceScript#', replace(replace(@FullMaintenanceScript, nchar(13), ''),nchar(10),'
')) Set @msg = replace(@Msg, '#LogBackupScript#', replace(replace(@LogBackupScript, nchar(13), ''),nchar(10),'
')) EXEC Msdb.dbo.sp_send_dbmail @profile_name = 'YourSQLDba_EmailProfile' , @recipients = @email , @importance = 'High' , @subject = 'YourSqlDba installed' , @body = @Msg , @body_format = 'HTML' End -- Install.InitialSetupOfYourSqlDba GO -- This procedure removes from SQL Server Agent's jobs steps commands strings -- which meets the selection criterias contained in -- @SelectSearchArg and @UnSelectSearchArg, -- the parameter string supplied by the "@prm" parameter. -- The parameter string must begin with a '@' character. -- The removal begins from the '@' character and ends before the next '@' character -- or at the end of the command string in the jobstep. -- @@MARK: TODO : Question for need Create Or Alter Proc yInstall.CleanUpParam @prm sysname , @SelectSearchArg nvarchar(1000) , @UnSelectSearchArg nvarchar(1000) = '' as Begin Set nocount on declare @sql nvarchar(max) declare @job_id uniqueidentifier declare @step_id int declare @pos int declare @PosDeb int declare @PosFin int While (1=1) -- while there is steps to correct Begin select @job_id=job_id, @step_id=step_id, @sql=command from msdb.dbo.sysjobsteps Where command like @SelectSearchArg And command not like @UnSelectSearchArg And command like '%'+@prm+'[^a-z0-9]%' If @@rowcount = 0 break set @pos = patindex('%'+@prm+'[^a-z0-9]%', @sql) -- assume the first parameter is always valid Set @PosDeb = @pos Set @PosFin = @pos+1 While (substring(@sql, @PosFin,1) <> '@') Begin --print substring(@sql, @PosFin,1) Set @PosFin = @PosFin +1 If @PosFin >= len(@Sql) Break End If substring(@sql, @PosFin,1) = '@' Set @PosFin = @PosFin -1 -- place the end position before the '@' -- if last param remove comma before if necessary If @PosFin >= len(@Sql) Begin While (substring(@sql, @Pos, 1) <> ',') Begin --print substring(@sql, @PosFin,1) Set @Pos = @Pos -1 If @pos = 1 Break End If @pos > 1 Set @PosDeb = @pos End Print '========================== Job step before update ============================' Print @sql Set @sql = Stuff(@sql, @PosDeb, @PosFin - @PosDeb + 1, '') Print '========================== Job step after update ============================' Print @sql Print '======================================================================' Update JS Set command = @sql from msdb.dbo.sysjobsteps JS Where @job_id=job_id and @step_id=step_id End End -- yInstall.CleanUpParam GO Exec yInstall.CleanUpParam @prm = '@genjour' , @SelectSearchArg = '%YourSQLDba%@DoBackup = ''F''%' Exec yInstall.CleanUpParam @prm = '@NotifyMandatoryFullDbBkpBeforeLogBkp' , @SelectSearchArg = '%YourSQLDba%@DoBackup = ''F''%' Exec yInstall.CleanUpParam @prm = '@genjour' , @SelectSearchArg = '%YourSQLDba%@DoBackup = ''L''%' Begin -- remove comments prededing the @jobId parameter from YourSQLDba_DoMaint calls in SQL Server Agent Update JS Set command = replace( command , '-- Agent job number to track step to retrieve step script in maintenance report' , '') from msdb.dbo.sysjobsteps JS Where command like '%YourSQLDba_DoMaint%-- Agent job number to track step to retrieve step script in maintenance report%' -- remove @jobId parameter from YourSQLDba_DoMaint calls in SQL Server Agent Exec yInstall.CleanUpParam @prm = '@jobId' , @SelectSearchArg = '%YourSQLDba_DoMaint%$(ESCAPE_NONE(JOBID))%' End -- remove @jobId parameter and comments from DeleteOldBackups calls in SQL Server Agent Exec yInstall.CleanUpParam @prm = '@jobId' , @SelectSearchArg = '%YourSQLDba.Maint.DeleteOldBackups%@JobId%=%$(ESCAPE_NONE(JOBID))%' Begin -- remove comments prededing the @StepId parameter from YourSQLDba_DoMaint calls in SQL Server Agent Update JS Set command = replace( command , '-- Agent job step number to track step to retrieve step script in maintenance report' , '') from msdb.dbo.sysjobsteps JS Where command like '%%YourSQLDba_DoMaint%-- Agent job step number to track step to retrieve step script in maintenance report%' -- remove @StepId parameter from YourSQLDba_DoMaint calls in SQL Server Agent Exec yInstall.CleanUpParam @prm = '@StepId' , @SelectSearchArg = '%YourSQLDba_DoMaint%$(ESCAPE_NONE(STEPID))%' End -- remove @StepId parameter and comments from DeleteOldBackups calls in SQL Server Agent Exec yInstall.CleanUpParam @prm = '@StepId' , @SelectSearchArg = '%YourSQLDba.Maint.DeleteOldBackups%@StepId%=%$(ESCAPE_NONE(STEPID))%' GO Create Or Alter Proc yInstall.AddUpEndParam @SelectSearchArg nvarchar(1000) , @UnSelectSearchArg nvarchar(1000) , @prm nvarchar(1000) as Begin Set nocount on declare @sql varchar(max) declare @job_id uniqueidentifier declare @step_id int declare @pos int declare @PosDeb int declare @PosFin int set @prm = yExecNLog.Unindent_TSQL(@prm) While (1=1) -- while there is steps to correct Begin select @job_id=job_id, @step_id=step_id, @sql=command from msdb.dbo.sysjobsteps Where command like @SelectSearchArg And command not like @UnSelectSearchArg If @@rowcount = 0 break Print '========================== Job step before update ============================' Print @sql -- suppress trailing spaces, tabs and carrige return at the end of the sql statement While (1=1) Begin If substring(@sql,len(@sql), 1) not in (' ', char(10), char(13), char(9)) Break Set @sql = substring(@sql, 1, len(@sql)-1) End -- add params Set @sql = yExecNLog.Unindent_TSQL(@sql + @prm) Print '========================== Job step after update ============================' Print @sql Print '======================================================================' Update JS Set command = @sql from msdb.dbo.sysjobsteps JS Where @job_id=job_id and @step_id=step_id End -- While there is steps to correct End -- yInstall.AddUpEndParam GO Exec yInstall.AddUpEndParam @SelectSearchArg = '%exec Maint.YourSqlDba_DoMaint%@DoBackup = ''F''%' , @UnSelectSearchArg = '%@ExcDbFromPolicy_CheckFullRecoveryModel%' , @prm = ' -- Each database exclusion filter must be on its own line between the following quote pair , @ExcDbFromPolicy_CheckFullRecoveryModel = '' '' ' GO print 'Existing installation, if any is updated to this version.' GO -- This function replace the parameters name in the "@command" string -- and subtract 1 day from the number of days of retention. Create Or Alter Function yInstall.ReplaceParamValue ( @command nvarchar(max) , @paramName sysname , @newParamValue nvarchar(max) ) returns nvarchar(max) as Begin Declare @ParamPos int Declare @equalPos int Declare @paramValuePos int Declare @paramValueEndPos int Declare @paramValueLength int Declare @paramFullLength int Declare @paramValue nvarchar(max) Declare @cmdRep nvarchar(max) Set @ParamPos = PATINDEX ('%'+@paramName+'[^a-z0-9_]%=%',@command) If @ParamPos = 0 -- the parameter is not in the command return (@command) Set @equalPos = @ParamPos + PATINDEX('%=%', Substring(@command, @ParamPos, 4000)) - 1 Set @paramValuePos = @equalPos + PATINDEX('%[0-9]%', Substring(@command, @equalPos, 4000)) - 1 Set @paramValueEndPos = @paramValuePos + PATINDEX('%[^0-9]%', Substring(@command, @paramValuePos, 4000)) - 2 Set @paramValueLength = @paramValueEndPos - @paramValuePos + 1 Set @paramValue= substring(@command, @paramValuePos, @paramValueLength) Set @paramFullLength = @paramValueEndPos - @ParamPos + 1 Set @cmdRep = Stuff ( @command , @ParamPos , @paramFullLength , @ParamName + ' = '+ @newParamValue ) return (@cmdRep) End -- yInstall.ReplaceParamValue GO -- This function replace the parameters name in the "@command" string -- and subtract 1 day from the number of days of retention. -- @@MARK: TODO : Check for use - Very old update Create Or Alter Function yInstall.ReplaceRetDays ( @command nvarchar(max) , @paramName sysname , @newParamName sysname ) returns nvarchar(max) as Begin Declare @ParamPos int Declare @equalPos int Declare @paramValuePos int Declare @paramValueEndPos int Declare @paramValueLength int Declare @paramFullLength int Declare @paramValue nvarchar(max) Declare @newParamValue nvarchar(max) Declare @cmdRep nvarchar(max) Set @ParamPos = PATINDEX ('%'+@paramName+'[^a-z0-9_]%=%',@command) If @ParamPos = 0 -- the parameter is not in the command return (@command) Set @equalPos = @ParamPos + PATINDEX('%=%', Substring(@command, @ParamPos, 4000)) - 1 Set @paramValuePos = @equalPos + PATINDEX('%[0-9nN]%', Substring(@command, @equalPos, 4000)) - 1 Set @paramValueEndPos = @paramValuePos + PATINDEX('%[^0-9nullNULL]%', Substring(@command, @paramValuePos, 4000)) - 2 Set @paramValueLength = @paramValueEndPos - @paramValuePos + 1 Set @paramValue= substring(@command, @paramValuePos, @paramValueLength) Set @paramFullLength = @paramValueEndPos - @ParamPos + 1 Set @newParamValue = CASE When @paramValue = 'null' Or @paramValue = 'NULL' Then 'NULL' When @paramValue = '0' Then 'NULL' Else convert(nvarchar(30) ,CONVERT(int, @paramValue) - 1 ) End Set @cmdRep = Stuff ( @command , @ParamPos , @paramFullLength , @newParamName + ' = '+ @newParamValue ) return (@cmdRep) End -- yInstall.ReplaceRetDays GO Create Or Alter Proc Install.UpdateMaintenanceTasksParam @paramName sysname , @paramValue nvarchar(max) as Begin Update msdb.dbo.sysjobsteps Set command = yInstall.ReplaceParamValue(command, @paramName, @paramValue ) Where command like '%'+'YourSQLDba_DoMaint%'+@paramName+'[^a-z0-9_]%' End -- Install.UpdateMaintenanceTasksParam go Create Or Alter Proc yInstall.DoReplacesInJobAndTasks @search nvarchar(1000) , @replace nvarchar(1000) = '' as Begin Set nocount on Set @search = replace(@search, '"', '''') Set @replace = replace(@replace, '"', '''') Update JS Set JS.step_name = replace (js.step_name, @search, @replace) from msdb.dbo.sysjobsteps JS Where step_name like '%'+@search+'%' Update JS Set command = replace(command, @search, @replace) from msdb.dbo.sysjobsteps JS Where command like '%'+@search+'%' Update JS Set JS.output_file_name = replace (js.output_file_name, @search, @replace) from msdb.dbo.sysjobsteps JS Where output_file_name like '%'+@search+'%' Update J Set J.name = replace(J.name, @search, @replace) from msdb.dbo.sysjobs J Where name like '%'+@search+'%' Update J Set description = replace(description, @search, @replace) from msdb.dbo.sysjobs J Where description like '%'+@search+'%' Update OP Set OP.name = replace(name, @search, @replace) From msdb.dbo.sysoperators OP Where name like '%'+@search+'%' End -- yInstall.DoReplacesInJobAndTasks GO -- script to migrate previous YourSQLDba database to YourSqlDba SET NOCOUNT ON --Select -- yInstall.ReplaceRetDays(command, '@FullBkpRet', '@FullBkpRetDays') --From msdb.dbo.sysjobsteps --Where command like '%'+'Maint.YourSqlDba_DoMaint%@FullBkpRet'+'[^a-z0-9_]%' -- version 4.0.10 Exec yInstall.DoReplacesInJobAndTasks '@UpdStatDaySpread', '@SpreadUpdStatRun' Update msdb.dbo.sysjobsteps Set command = yInstall.ReplaceRetDays(command, '@FullBkpRet', '@FullBkpRetDays') Where command like '%'+'Maint.YourSqlDba_DoMaint%@FullBkpRet'+'[^a-z0-9_]%' --Select -- yInstall.ReplaceRetDays(command, '@LogBkpRet', '@LogBkpRetDays') --From msdb.dbo.sysjobsteps --Where command like '%'+'Maint.YourSqlDba_DoMaint%@LogBkpRet'+'[^a-z0-9_]%' Update msdb.dbo.sysjobsteps Set command = yInstall.ReplaceRetDays(command, '@LogBkpRet', '@LogBkpRetDays') Where command like '%'+'Maint.YourSqlDba_DoMaint%@LogBkpRet'+'[^a-z0-9_]%' --Select -- yInstall.ReplaceRetDays(command, '@BackupRetentionDaysForSelectedDb', '@BkpRetDays') --From msdb.dbo.sysjobsteps --Where command like '%'+'Maint.DeleteOldBackups%@BackupRetentionDaysForSelectedDb'+'[^a-z0-9_]%' Update msdb.dbo.sysjobsteps Set command = yInstall.ReplaceRetDays(command, '@BackupRetentionDaysForSelectedDb', '@BkpRetDays') Where command like '%'+'Maint.DeleteOldBackups%@BackupRetentionDaysForSelectedDb'+'[^a-z0-9_]%' --Select -- yInstall.ReplaceRetDays(command, '@BackupRetentionDays', '@BkpRetDaysForUnSelectedDb') --From msdb.dbo.sysjobsteps --Where command like '%'+'Maint.DeleteOldBackups%@BackupRetentionDays'+'[^a-z0-9_]%' Update msdb.dbo.sysjobsteps Set command = yInstall.ReplaceRetDays(command, '@BackupRetentionDays', '@BkpRetDaysForUnselectedDb') Where command like '%'+'Maint.DeleteOldBackups%@BackupRetentionDays'+'[^a-z0-9_]%' Exec yInstall.DoReplacesInJobAndTasks '@MaxFailedBackupAttemptsToOffline', '@ConsecutiveFailedbackupsDaysToPutDbOffline' -- Version 6.2.3 Exec yInstall.DoReplacesInJobAndTasks '@ConsecutiveFailedbackupsDaysToPutDbOffline', '@ConsecutiveDaysOfFailedBackupsToPutDbOffline' GO -- ------------------------------------------------------------------------------ -- Create network map table -- ------------------------------------------------------------------------------ -- if the table doesn't exists create the latest version If object_id('Maint.NetworkDrivesToSetOnStartup') is null Begin Declare @sql nvarchar(max) Set @sql = ' Create table Maint.NetworkDrivesToSetOnStartup ( DriveLetter nchar(2) , Unc nvarchar(255) , constraint Pk_NetworkDrivesToSetOnStartup primary key clustered (DriveLetter) ) ' Exec (@sql) If Object_Id('tempdb..##NetworkDrivesToSetOnStartup') IS NOT NULL Exec ( ' Insert Into Maint.NetworkDrivesToSetOnStartup ([DriveLetter],[Unc]) Select [DriveLetter],[Unc] From ##NetworkDrivesToSetOnStartup Drop table if exists ##NetworkDrivesToSetOnStartup ' ) End GO -- -- @@MARK: TODO : Check for use -- ------------------------------------------------------------------------------ -- Stored procedure to define network drive on SQL Server startup -- ------------------------------------------------------------------------------ Create Or Alter Proc Maint.CreateNetworkDrives @DriveLetter nvarchar(2) , @unc nvarchar(255) as Begin Declare @errorN int Declare @cmd nvarchar(4000) Set nocount on Exec yMaint.SaveXpCmdShellStateAndAllowItTemporary Set @DriveLetter=rtrim(@driveLetter) Set @Unc=rtrim(@Unc) If Len(@DriveLetter) = 1 Set @DriveLetter = @DriveLetter + ':' If Len(@Unc) >= 1 Begin Set @Unc = yUtl.NormalizePath(@Unc) Set @Unc = Stuff(@Unc, len(@Unc), 1, '') End Set @cmd = 'net use /Delete' Set @cmd = Replace( @cmd, '', @DriveLetter) begin try Print @cmd exec xp_cmdshell @cmd, no_output end try begin catch end catch -- suppress previous network drive definition If exists(select * from Maint.NetworkDrivesToSetOnStartup Where DriveLetter = @driveLetter) Begin Delete from Maint.NetworkDrivesToSetOnStartup Where DriveLetter = @driveLetter End Begin Try Set @cmd = 'net use ' Set @cmd = Replace( @cmd, '', @DriveLetter ) Set @cmd = Replace( @cmd, '', @unc ) Print @cmd exec xp_cmdshell @cmd Insert Into Maint.NetworkDrivesToSetOnStartup (DriveLetter, Unc) Values (@DriveLetter, @unc) Exec yMaint.RestoreXpCmdShellState End Try Begin Catch Set @errorN = ERROR_NUMBER() -- return error code Print convert(nvarchar, @errorN) + ': ' + ERROR_MESSAGE() Exec yMaint.RestoreXpCmdShellState End Catch End -- Maint.CreateNetworkDrives GO -- @@MARK: TODO : Check for use Create Or Alter Proc Maint.DisconnectNetworkDrive @DriveLetterOrUNC nvarchar(255) As Begin Declare @errorN int Declare @DriveLetter nvarchar(255) Declare @cmd nvarchar(4000) Set nocount on If Len(@DriveLetterOrUNC) = 1 Set @DriveLetterOrUNC = @DriveLetterOrUNC + ':' Set @DriveLetterOrUNC = yUtl.NormalizePath(@DriveLetterOrUNC) -- because on past relaxed parameter validation the table may have variant of drive letter or unc format. -- make it uniform to make the rest of the code to work properly. ;With UpdateView as ( Select DriveLetter , Unc , left(yUtl.NormalizePath(left(rtrim(DriveLetter)+':', 2)),2) as NormalizedDriveLetter , Stuff(yUtl.NormalizePath(rtrim(Unc)), len(yUtl.NormalizePath(rtrim(Unc))), 1, '') as NormalizedUnc From Maint.NetworkDrivesToSetOnStartup ) Update UpdateView Set DriveLetter = NormalizedDriveLetter, Unc = NormalizedUnc -- no matter how drive letter or unc where stored with or without ending '\', make it work Set @DriveLetter = Null Select @DriveLetter = DriveLetter From Maint.NetworkDrivesToSetOnStartup Where DriveLetter = left(@DriveLetterOrUNC,2) Or Unc = @DriveLetterOrUNC If @DriveLetter Is Not Null Begin Begin Try Set @cmd = 'net use /DELETE' Set @cmd = Replace( @cmd, '', @DriveLetter ) Print @cmd exec yMaint.SaveXpCmdShellStateAndAllowItTemporary exec xp_cmdshell @cmd exec yMaint.RestoreXpCmdShellState Delete From Maint.NetworkDrivesToSetOnStartup Where DriveLetter = left(yUtl.NormalizePath(@DriveLetterOrUNC),2) Or Unc = yUtl.NormalizePath(@DriveLetterOrUNC) End Try Begin Catch Set @errorN = ERROR_NUMBER() -- return error code Print convert(nvarchar, @errorN) + ': ' + ERROR_MESSAGE() exec yMaint.RestoreXpCmdShellState End Catch End Else Begin Print 'No network drive match this criteria. ' + 'Run the following command to list existing network drives :' + ' «Exec YourSQLDba.Maint.ListNetworkDrives»' End End -- Maint.DisconnectNetworkDrive GO -- @@MARK: TODO : Check for use Create Or Alter Proc Maint.ListNetworkDrives As Begin Set nocount on Select DriveLetter, Unc From Maint.NetworkDrivesToSetOnStartup End -- Maint.ListNetworkDrives GO -- @@MARK: TODO : Check for use Create Or Alter Procedure yMirroring.InactivateYourSqlDbaJobs As Begin Select ROW_NUMBER() OVER (ORDER BY job_id) As Seq, job_id Into #jobs From msdb.dbo.sysjobsteps Where command like '%YourSQLDba_DoMaint%' Declare @job_id uniqueidentifier Declare @sql nvarchar(max) Declare @seq int Set @seq = 0 while 1=1 Begin Select Top 1 @Seq=Seq, @job_id=job_id From #jobs Where Seq > @Seq If @@rowcount = 0 break Set @sql = 'msdb.dbo.sp_update_job "", @enabled= 0' Set @sql = Replace( @sql, '', convert(nvarchar(36), @job_id)) Set @sql = Replace( @sql, '"', '''') --Print @sql Exec( @sql ) End Drop table if exists #jobs End -- yMirroring.InactivateYourSqlDbaJobs GO -- @@MARK: Mirroring DropServer Create Or Alter Procedure Mirroring.DropServer @MirrorServer sysname = '' , @silent int = 0 As Begin Declare @sql nvarchar(max) Declare @remoteServerYourSqlDbaVersion Nvarchar(40) Declare @ObjectId int Set NoCount on Exec yMirroring.ReportYourSqlDbaVersionOnTargetServers @MirrorServer = @MirrorServer , @remoteVersion = @remoteServerYourSqlDbaVersion Output , @LogToHistory = 0 , @silent = @silent If @remoteServerYourSqlDbaVersion IN ('Server undefined', 'Remote YourSqlDba is missing)' ) Return Delete From Mirroring.TargetServer Where MirrorServerName=@MirrorServer Declare @srvLogins Table (loginName sysname primary key clustered) Insert into @srvLogins select p.name from sys.servers S Join sys.Linked_logins L ON L.server_Id = S.server_id Join sys.server_principals P On P.principal_id = l.local_principal_id Where S.name = @MirrorServer And S.is_linked = 1 EXEC master.dbo.sp_droplinkedsrvlogin @rmtsrvname=@MirrorServer,@locallogin=NULL Declare @name sysname -- drop dependants logins for this linked server Set @name = '' -- otherwise the linked server is not removed While(1=1) Begin Select top 1 @name = loginName from @SrvLogins Where loginName > @name Order by loginName If @@ROWCOUNT = 0 break Print 'Remove existing linked server login '+@name Exec sp_droplinkedsrvlogin @MirrorServer, @name End -- Set options for the linked server to be able to do Exec ... AT EXEC master.dbo.sp_serveroption @server=@MirrorServer, @optname=N'rpc', @optvalue=N'true' EXEC master.dbo.sp_serveroption @server=@MirrorServer, @optname=N'rpc out', @optvalue=N'true' Print 'Remove existing Linked Server' + @mirrorServer Exec sp_dropServer @MirrorServer Exec Mirroring.HandleRestoreJobAsNecessary @MirrorServer=@MirrorServer, @DropJob=1 Print '-------------------------------------------------------------------' Print ' Mirror server succesfully uninstalled' Print '-------------------------------------------------------------------' End -- Mirroring.DropServer GO -- @@MARK: Mirroring Procedure to process restores Create Or Alter Procedure Mirroring.ProcessRestores As ------------------------------------------------------------------------------------- -- Sp called by SQL Agent Job in the context of a MirrorServer. -- This SQL Agent Job is managed properly by yMirroring.QueueRestoreToMirror ------------------------------------------------------------------------------------- Begin -- Restores are scheduled from a maintenance session that has its own parameters and JobNo. -- Before queuing a restore, this maintenance session records its current JobNo along with -- the SQL command to execute and the target MirrorServer. -- In YourSqlDba, many processes and logging operations rely on information linked to this JobNo. -- The JobNo is made globally available to the current session through a session context key -- named 'JobNoInSessCtx'. It is set by calling: -- EXEC sp_set_session_context @key = N'JobNoInSessCtx', @value = @JobNo -- Many processes obtain their parameters and JobNo via MainContextInfo(), -- which internally reads the session context value for 'JobNoInSessCtx' to get the JobNo, -- and then uses it to read Maint.JobHistory for all related parameters. -- Therefore, we retrieve @JobNo from Mirroring.RestoreQueue. After reading it, we set the -- session context for the current connection (the SQL Agent connection running -- Mirroring.ProcessRestores) with the same JobNo, so that dbo.MainContextInfo can return -- the correct originating JobNo and its parameters whenever needed. Set Nocount on Declare @WaitStart Datetime = Getdate() Declare @RestoreSeq Int Declare @errorN Int Declare @jobNo Int Declare @CallerProcess Nvarchar(4000) Select @CallerProcess = ISNULL(I.SqlAgentJobName, Prog + ' - Executed by :'+Who) From dbo.WhoCalledWhat as I Declare @JSonPrms Nvarchar(max) Declare @MsgToLog Nvarchar (2048) While (1=1) Begin BEGIN TRY Select Top 1 @RestoreSeq = RestoreSeq , @jobNo = Q.CallingJobNo , @JSonPrms = Q.JSonPrms From Mirroring.RestoreQueue as Q Where Q.ErrorN = 0 -- not processed yet. Order By RestoreSeq Asc If @@ROWCOUNT = 0 Begin If (DATEDIFF(mi, @WaitStart, Getdate()) > 5) OR NOT EXISTS -- If YourSqlDba.Do_Maint is no more active, no new backup will be queued, so I can stop ( SELECT * FROM sys.dm_tran_locks AS tl WHERE tl.resource_type = 'APPLICATION' AND tl.request_mode = 'S' AND tl.request_status = 'GRANT' AND resource_description Like '%YourSqlDba.Do_Maint%' ) Break -- Exit proc, it is waiting for 5 minutes and no new backups are queued into the table Else Begin Waitfor Delay '00:01:00'; Continue; -- retry a peek on the queue End End -- Set session context value for the jobNo to the same value of the connection that queue the restore -- It will make the restore command to be logged as if it was part of the same job. IF ISNULL(CAST(SESSION_CONTEXT(N'JobNoInSessCtx') AS int), -1) <> @JobNo EXEC sp_set_session_context @key = N'JobNoInSessCtx', @value = @JobNo, @read_only = 0; -- Report error flag if any (could be zero) Update Mirroring.RestoreQueue Set StartedAt = Getdate() Where RestoreSeq=@RestoreSeq Exec Dbo.LogEvent @MsgTemplate = 'JobNo:#JobNo# - Restoring Database backup (#bkpTyp#) #DbName# at server #MirrorServer#' , @JsonPrms=@JsonPrms EXECUTE AS LOGIN = 'YourSqlDba'; -- for our remote exec. setup Declare @Sql Nvarchar(max) Select @Sql=R.Code From --#PrmQueueRestoreToMirrorCmd (Select ThisProc=S#.FullObjName(@@PROCID)) as ThisProc CROSS APPLY S#.GetTemplateFromCmtAndReplaceTags ('===SqlTemplateForRestore===', ThisProc, @JsonPrms) as R /*===SqlTemplateForRestore=== DECLARE @BackupType char(1) = '#BkpTyp#' , @Filename Nvarchar(512) = '#Filename#' , @DbName Sysname = '#DbName#' , @MigrationTestMode Int = #MigrationTestMode# , @ReplaceSrcBkpPathToMatchingMirrorPath NVarchar(max) = '#ReplaceSrcBkpPathToMatchingMirrorPath#' , @ReplacePathsInDbFilenames NVarchar(max) = '#ReplacePathsInDbFilenames#' EXEC ( N' EXEC YourSqlDba.yMirroring.DoRestore @BackupType = ?, @Filename = ?, @DbName = ?, @MigrationTestMode = ?, @ReplaceSrcBkpPathToMatchingMirrorPath = ?, @ReplacePathsInDbFilenames = ?; ' , @BackupType , @Filename , @DbName , @MigrationTestMode , @ReplaceSrcBkpPathToMatchingMirrorPath , @ReplacePathsInDbFilenames ) AT [#MirrorServer#]; ===SqlTemplateForRestore===*/ EXEC yExecNLog.LogAndOrExec @context = 'Mirroring.ProcessRestores' , @Info = @CallerProcess , @sql = @sql , @errorN = @errorN OUTPUT REVERT; -- EXECUTE AS LOGIN = 'YourSqlDba'; Exec Dbo.LogEvent @MsgTemplate = 'JobNo:#JobNo# - Restore (#bkpTyp#) #DbName# processed at server #MirrorServer#' , @JsonPrms=@JsonPrms -- Report error flag if any (could be zero) Update Mirroring.RestoreQueue Set ErrorN = @ErrorN Where RestoreSeq=@RestoreSeq And ErrorN <> @ErrorN Update Mirroring.RestoreQueue Set EndedAt = Getdate() Where RestoreSeq=@RestoreSeq -- If done, delete successful restore Delete From Mirroring.RestoreQueue Where RestoreSeq=@RestoreSeq And ErrorN=0; Set @WaitStart = Getdate() END TRY BEGIN CATCH IF SUSER_SNAME() = 'YourSqlDba' BEGIN TRY REVERT; END TRY BEGIN CATCH END CATCH; THROW; END CATCH End -- While End Go ------------------------------------------------------------------------------------- -- When Adding a server ensure and or create specific SQL Agent Job for restore to MirrorServer ------------------------------------------------------------------------------------- -- @@MARK: Mirroring AddServer Create Or Alter Procedure Mirroring.AddServer @MirrorServer nvarchar(512) -- Mirror server (descriptive only if @MirrorServerDataSrc is different) , @remoteLogin nvarchar(512) -- sysadmin login , @remotePassword nvarchar(512) -- matchin sysadmin login password for @remoteLogin , @ExcSysAdminLoginsInSync int = 0 -- by default don't sync sysadmin , @ExcLoginsFilter nvarchar(max) = '' -- on here is a filter on which on to sync , @MirrorServerDataSrc nvarchar(512) = '' -- If specified this is real server name , @YourSqlDbaAccountForMirroringPwd nvarchar(512) = NULL As Begin Declare @sql nvarchar(max) Declare @Info nvarchar(2048) Declare @remoteServerYourSqlDbaVersion nvarchar(100) Set NoCount on -- Create a link server for the Mirror If Exists (Select * From sys.servers where name = @MirrorServer And is_linked = 1) Begin EXEC Mirroring.DropServer @MirrorServer End -- Get SqlAgent Login Account Declare @SqlAgentLoginAccount as sysname select @SqlAgentLoginAccount = login_name from sys.dm_exec_sessions Where program_name = 'SQLAgent - Generic Refresher' If @@rowcount = 0 Begin Raiserror('SqlAgent must be running in order to identify its starting account and authorize it to the remote server', 11, 1) Return End IF (LEN(@MirrorServerDataSrc) > 0) BEGIN EXEC master.dbo.sp_addlinkedserver @server = @MirrorServer, @srvproduct=N'', @provider=N'MSOLEDBSQL', @datasrc=@MirrorServerDataSrc END ELSE BEGIN EXEC master.dbo.sp_addlinkedserver @server = @MirrorServer, @srvproduct=N'', @provider=N'MSOLEDBSQL', @datasrc=@MirrorServer END EXEC master.dbo.sp_addlinkedsrvlogin @rmtsrvname = @MirrorServer, @locallogin = NULL , @useself = N'True' -- enable SQLAgent to run remotely something AT the mirror server Print 'Adding delegate account "'+@remoteLogin+ '" for SQL Agent Login account : "'+@SqlAgentLoginAccount+'"' If left(@SqlAgentLoginAccount, 2)= '.\' Begin Set @SqlAgentLoginAccount = STUFF(@SqlAgentLoginAccount, 1, 1, convert(sysname, serverproperty('machineName'))) End EXEC master.dbo.sp_addlinkedsrvlogin @rmtsrvname = @MirrorServer , @locallogin = @SqlAgentLoginAccount , @useself = N'False' , @rmtUser = @RemoteLogin , @rmtPassWord = @RemotePassWord -- add also yourself for debugging purposes (to check the linkServer access) -- unless already done Declare @localLogin sysname Set @localLogin = SUSER_SNAME() If @localLogin <> @SqlAgentLoginAccount And @localLogin <> 'YourSQLDba' Begin Print 'Adding delegate account "'+@remoteLogin+ '" for account "'+@localLogin+'"' EXEC master.dbo.sp_addlinkedsrvlogin @rmtsrvname = @MirrorServer , @locallogin = @localLogin , @useself = N'False' , @rmtUser = @RemoteLogin , @rmtPassWord = @RemotePassWord End -- Set options for the linked server EXEC master.dbo.sp_serveroption @server=@MirrorServer, @optname=N'rpc', @optvalue=N'true' EXEC master.dbo.sp_serveroption @server=@MirrorServer, @optname=N'rpc out', @optvalue=N'true' EXEC master.dbo.sp_serveroption @server=@MirrorServer, @optname=N'query timeout', @optvalue=N'86400' Insert Into Mirroring.TargetServer (MirrorServerName ) Select @MirrorServer Where Not Exists (Select * From Mirroring.TargetServer Where MirrorServerName=@MirrorServer) -- if user specify a YourSqlDba password, it is set locally and remotely to the specified value Exec Mirroring.SetYourSqlDbaAccountForMirroring @YourSqlDbaAccountForMirroringPwd; Exec yMirroring.ReportYourSqlDbaVersionOnTargetServers @MirrorServer = @MirrorServer , @remoteVersion = @remoteServerYourSqlDbaVersion Output , @LogToHistory = 0 If @remoteServerYourSqlDbaVersion <> (Select v.VersionNumber from Install.VersionInfo () as v) Begin Delete Mirroring.TargetServer Where MirrorServerName=@MirrorServer Print '************ AddServer Failure **********************' If @remoteServerYourSqlDbaVersion <> 'no remote mapping exists' Begin Print 'YourSqlDba.Mirror.AddServer : Problem occurred with linked server YourSqlDba Database version: '+@remoteServerYourSqlDbaVersion Print 'Install or Upgrade YourSqlDba on the remote server and run Mirroring.AddServer again' Print 'Rollbacking remote server addition because YourSqlDba version mismatch between local server and linked server' EXEC Mirroring.DropServer @MirrorServer, @silent = 1 End Else Begin Print 'YourSqlDba.Mirror.AddServer : Problem occurred: '+@remoteServerYourSqlDbaVersion Print 'Adjust Linked server remote logins mapping. Then test link server connection by browsing linked server databases ' Print 'You can also do YourSqlDba.Mirror.DropServer followed by YourSqlDba.Mirror.AddServer with checking for proper remote user and password ' End Print '*****************************************************' Raiserror ('See print text for Mirroring.Addserver failure',11,1) Return End Exec Mirroring.HandleRestoreJobAsNecessary @MirrorServer=@MirrorServer, @ForceRecreateJob=1 -- Synchronise logins on the mirror server Exec yMirroring.LaunchLoginSync Print '-------------------------------------------------------------------' Print ' Mirror server succesfully installed' Print '-------------------------------------------------------------------' End -- Mirroring.AddServer GO -- @@MARK: Mirroring DoRestore Create Or Alter Procedure yMirroring.DoRestore @BackupType nchar(1) , @Filename nvarchar(255) , @DbName nvarchar(255) -- In test migrationTesmode=1, only Full backup are restored -- there is a default param because FailOver call DoRestore but never in this mode , @MigrationTestMode Int = 0 , @ReplaceSrcBkpPathToMatchingMirrorPath nvarchar(max) = '' , @ReplacePathsInDbFileNames nvarchar(max) = '' As Begin Declare @pathData nvarchar(512) Declare @pathLog nvarchar(512) Declare @FileType char(1) Declare @sql nvarchar(max) Declare @sqlcmd nvarchar(max) Declare @FileId int Declare @NoSeq int Declare @LogicalName sysname Declare @PhysicalName sysname Declare @NewPhysicalName sysname Declare @ClauseMove nvarchar(max) Declare @Position smallint Declare @ErrorMessage nvarchar(2048) Declare @ReplSrch nvarchar(512) Declare @ReplBy nvarchar(512) Set nocount on If @ReplacePathsInDbFileNames = '' -- user did not specified any relocation parameter for DB files, so use default location Begin -- get default data and log location Exec master.dbo.xp_instance_regread N'HKEY_LOCAL_MACHINE' , N'Software\Microsoft\MSSQLServer\MSSQLServer' , N'DefaultData' , @pathData OUTPUT Exec master.dbo.xp_instance_regread N'HKEY_LOCAL_MACHINE' , N'Software\Microsoft\MSSQLServer\MSSQLServer' , N'DefaultLog' , @pathLog OUTPUT -- if default data and log locations are not specified in server properties, use model database files locations If @pathData Is Null Select Top 1 @pathData = Left( physical_name, Len(physical_name) - Charindex('\', Reverse(physical_name))) FROM model.sys.database_files Where type = 0 If @pathLog Is Null Select Top 1 @pathLog = Left( physical_name, Len(physical_name) - Charindex('\', Reverse(physical_name))) FROM model.sys.database_files Where type = 1 End Else -- user did specified replacement parameter for database files location Begin -- extract file path string replacement to do if any, because directories can be at a different places -- Parameter contains both search and replace parameter in the form /* 'stringToSearch1>StringToReplace1| stringToSearch2>StringToReplace2' */ -- where '>' is the divider of the pair. Each pair must be on its own line and ends with '|' -- Parameter can be empty Set @ReplacePathsInDbFileNames = replace(@ReplacePathsInDbFileNames, '>', '>') Declare @ReplacesOnFiles Table (seqRep int primary key, replSrch nvarchar(512) NULL, replBy nvarchar(512) NULL) ;With ReplacePairs as (Select * From yUtl.SplitParamInRows (@ReplacePathsInDbFileNames) as x) , Pairs(seq, pair, posSep) as (Select no, line, CHARINDEX ('>', line) From ReplacePairs) insert @ReplacesOnFiles (seqRep, replSrch, replBy) Select seq, left(pair, posSep-1), right(pair, len(pair)-posSep) From pairs Where posSep > 1 End -- If user specified a restore location remapping performs replacements on backup file name. If @ReplaceSrcBkpPathToMatchingMirrorPath <> '' Begin -- extract replacement information on backup location to restore location -- on one side it is a network path and on the other it is a local path -- or it could but 2 network path expressed differently. -- Parameter contains both search and replace parameter in the form /* 'stringToSearch1>StringToReplace1 stringToSearch2>StringToReplace2' */ -- where '>' is the divider of the pair. Each pair must be on its own line -- Parameter can be empty -- Since the all call of this command get through an xml message ">" becomes ">" -- so it must be turned back to > Set @ReplaceSrcBkpPathToMatchingMirrorPath = replace(@ReplaceSrcBkpPathToMatchingMirrorPath, '>', '>') Declare @ReplacesOnRestorePath Table (seqRep int primary key, replSrch nvarchar(512) NULL, replBy nvarchar(512) NULL) ;With ReplacePairs as (Select * From yUtl.SplitParamInRows (@ReplaceSrcBkpPathToMatchingMirrorPath) as x) , Pairs(seq, pair, posSep) as (Select no, line, charindex('>', Line) From ReplacePairs) Insert @ReplacesOnRestorePath (seqRep, replSrch, replBy) Select seq, left(pair, posSep-1), right(pair, len(pair)-posSep) From pairs Where posSep > 1 -- process replaces on backup file name and path location Declare @seqRep Int Set @SeqRep = 0 While (1=1) Begin Select top 1 @SeqRep = SeqRep, @ReplSrch = replSrch, @ReplBy = replBy From @ReplacesOnRestorePath Where seqRep > @seqRep Order by seqRep If @@rowcount = 0 Break Set @Filename = replace(@Filename, @replSrch, @replBy) End End Begin Try -- an internal raiserror to collectBackup... procedure makes unnecessary to test return code -- in case of error it will jump to the catch block to be reported Exec yMaint.CollectBackupHeaderInfoFromBackupFile @Filename Exec yMaint.CollectBackupFileListFromBackupFile @Filename -- recover database name from datase backup file Select @DbName = DatabaseName From Maint.TemporaryBackupHeaderInfo Where spid = @@spid If @BackupType='F' Begin -- If database already exists with a status different than «RESTORING» -- we must generate an error to prevent restoring over a good database -- the exception is that when this is a MigrationTestMode, we just don't restore over online database -- the goal when MigrationTestMode=1 it that if the user wants a new copy of the database, he just have to remove it If DATABASEPROPERTYEX(@DbName, 'Status' ) = 'ONLINE' And @MigrationTestMode = 1 Begin Return -- in MigrationTestMode=1 Online database are left as is there is no restore over them End If DATABASEPROPERTYEX(@DbName, 'Status' ) Is Not Null And DATABASEPROPERTYEX(@DbName, 'Status' ) <> 'RESTORING' Begin Raiserror (N'To restore a full backup to the mirror server the database %s must be in «RESTORING» state or not exists', 11, 1, @DbName) End -- previous database must be removed to have accurate information about its last_lsn in msdb -- Generate restore command Set @sql = ' If databasepropertyex("", "status")="RESTORING" Begin Exec ( " Restore Database [] with recovery; Drop DATABASE []; " ) End; RESTORE DATABASE [] FROM DISK="" WITH ,CHECKSUM ,REPLACE ,NORECOVERY ,bufferCount = 20 ,MAXTRANSFERSIZE = 4096000 ' Set @FileId = -1 Set @NoSeq = 0 -- generate the move command which is requiered where original location -- from db source server don't match with destination location on destination server While 1=1 Begin Select Top 1 @FileType = Type , @FileId = FileId , @LogicalName=LogicalName , @PhysicalName=PhysicalName From Maint.TemporaryBackupFileListInfo Where spid = @@spid And FileId > @FileId Order by FileId If @@rowcount = 0 break If @ReplacePathsInDbFileNames <> '' -- user specified relocation parameters Begin -- check for replaces to do to "relocate" location Set @SeqRep = 0 While (1=1) Begin Select top 1 @SeqRep = SeqRep, @ReplSrch = replSrch, @ReplBy = replBy From @ReplacesOnFiles Where seqRep > @seqRep Order by seqRep If @@rowcount = 0 Break -- perform replacements as long as there are Set @PhysicalName = replace(@PhysicalName, @replSrch, @replBy) End -- while there is replacements on file names to perform Set @ClauseMove = 'MOVE "" TO ""' Set @ClauseMove = Replace( @ClauseMove, '', @PhysicalName ) Set @ClauseMove = Replace( @ClauseMove, '', @LogicalName ) End Else Begin -- use default Db location parameters Set @ClauseMove = 'MOVE "" TO "\"' If @FileType = 'L' -- log location not usually the same that other files Set @ClauseMove = Replace( @ClauseMove, '', @pathLog ) --bug restore Else Set @ClauseMove = Replace( @ClauseMove, '', @pathData ) -- strip the path out of physical name that comes from Maint.TemporaryBackupFileListInfo -- which is produced by CollectBackupHeaderInfoFromBackupFile Set @PhysicalName=RIGHT(@PhysicalName, Charindex('\', Reverse(@PhysicalName))-1) Set @ClauseMove = Replace( @ClauseMove, '', @PhysicalName ) Set @ClauseMove = Replace( @ClauseMove, '', @LogicalName ) End Set @sql = Replace(@sql, '', @ClauseMove + nchar(13) + nchar(10) + ' ,') End Set @sql = Replace(@sql, ',', '') Set @sql = replace (@sql, '"', '''') Set @sql = replace (@sql, '', @DbName) Set @sql = replace (@sql, '', @Filename) Set @sql = yExecNLog.Unindent_TSQL(@sql) End -- In @MigrationTestMode =1 only full backup can be restored, because they are put online after restore -- so we skip request for this type of restore If @BackupType='D' And @MigrationTestMode = 0 Begin Set @sql = '' Set @Position = 0 -- To restore a log backup the database must exists and have the status «RESTORING» If DATABASEPROPERTYEX(@DbName, 'Status' ) Is Null Or DATABASEPROPERTYEX(@DbName, 'Status' ) <> 'RESTORING' Begin Raiserror (N'To restore a DIFFERENTIAL backup to the mirror server the database %s must be in «RESTORING» state', 11, 1, @DbName) End while 1=1 Begin -- check database state to see which file of the log backup has to be restored -- funny enough restore database appears also in msdb.dbo.backupSet -- this information (position) is obtained through the last_lsn restore versus -- last_lsn into the backup Select Top 1 @Position = H.Position --first position that match the last_lsn From ( Select database_name, Max(last_lsn) as last_lsn From msdb.dbo.backupset B Group By database_name ) X Join Maint.TemporaryBackupHeaderInfo H ON H.spid = @@spid And H.DatabaseName = X.database_name collate database_default AND H.LastLSN > X.last_lsn Where H.Position > @Position Order By H.Position If @@rowcount = 0 break -- Generate restore command. -- Do not handle move command. To acheive this there is an need to add column createLsn -- to Maint.TemporaryBackupFileListInfo and generate the move command only for the lsn -- that is between the backup lsn. Also proceeed to replace in the file name. -- Some job but not overwhelming Set @sqlcmd = 'RESTORE DATABASE [] FROM DISK="" WITH FILE=, NORECOVERY, CHECKSUM,bufferCount = 20,MAXTRANSFERSIZE = 4096000 ' Set @sqlcmd = replace (@sqlcmd, '"', '''') Set @sqlcmd = replace (@sqlcmd, '', @DbName) Set @sqlcmd = replace (@sqlcmd, '', @Filename) Set @sqlcmd = replace (@sqlcmd, '', CONVERT(nvarchar(10), @Position)) Set @sql = REPLACE( @sql, '', @sqlcmd) Set @sql = @sql + char(10) + '' End Set @sql = REPLACE( @sql, '', '') End -- In @MigrationTestMode =1 only full backup can be restored, because they are put online after restore -- so we skip request for this type of restore If @BackupType='L' And @MigrationTestMode = 0 Begin Set @sql = '' Set @Position = 0 -- To restore a log backup the database must exists and have the status «RESTORING» If DATABASEPROPERTYEX(@DbName, 'Status' ) Is Null Or DATABASEPROPERTYEX(@DbName, 'Status' ) <> 'RESTORING' Begin Raiserror (N'To restore a LOG backup to the mirror server the database %s must be in «RESTORING» state', 11, 1, @DbName) End -- check database state to see which file of the log backup has to be restored -- funny enough restore database appears also in msdb.dbo.backupSet -- this information (position) is obtained through the last_lsn restore versus -- last_lsn into the backup -- Generate restore log commands. -- Do not handle move command. To acheive this there is an need to add column createLsn -- to Maint.TemporaryBackupFileListInfo and generate the move command only for the lsn -- that is between the backup lsn. Also proceeed to replace in the file name. -- Some job but not overwhelming Select @Sql= STRING_AGG(Sql.Code, '') WITHIN GROUP (ORDER BY H.Position ASC) From ( Select database_name, Max(last_lsn) as last_lsn From msdb.dbo.backupset B Group By database_name ) X Join Maint.TemporaryBackupHeaderInfo H ON H.spid = @@spid And H.DatabaseName = X.database_name collate database_default AND H.LastLSN > X.last_lsn CROSS APPLY (Select DbName=DatabaseName, FileName=@FileName, Position=CONVERT(nvarchar(10), H.Position), Self=NULL) as Prm CROSS APPLY (Select jPrms=(Select Prm.* For JSON Path)) as jPrms CROSS APPLY (Select * From S#.GetTemplateFromCmtAndReplaceTags ('===DoRestoreTempl===', Self, jPrms) as C) as Sql /*===DoRestoreTempl=== Exec Sp_getapplock @Resource = 'SerializeLogShip_#DbName#', @LockOwner = 'Session', @LockMode = 'Exclusive', @LockTimeout = 60000; -- 60s RESTORE LOG [#DBName#] FROM DISK='#FileName#' WITH FILE=#Position#, NORECOVERY, CHECKSUM,bufferCount = 20,MAXTRANSFERSIZE = 4096000 EXEC sp_releaseapplock @Resource = 'SerializeLogShip_#DbName#', @LockOwner = 'Session'; ===DoRestoreTempl===*/ End If @sql <> '' -- could be still NULL which is also false. Begin Declare @maxSeverity Int Declare @Msgs nvarchar(max) Exec yExecNLog.ExecWithProfilerTrace @sql, @maxSeverity output, @Msgs Output If @maxseverity <=10 Begin Set @msgs = @sql + @msgs Exec yExecNLog.PrintSqlCode @msgs -- In @MigrationTestMode =1 only full backup can be restored -- and we do recover database after full restore If @MigrationTestMode = 1 -- we want to make Database available right away for test in MigrationTestMode Begin Set @Sql = 'Restore Database ['+@DbName+'] With recovery' Exec yExecNLog.ExecWithProfilerTrace @sql, @maxSeverity output, @Msgs Output End End Else Begin Raiserror (N'%s: %s %s', 11, 1, @@SERVERNAME, @Sql, @Msgs) End End End Try Begin Catch Select @ErrorMessage = ERROR_MESSAGE() Raiserror (N'yMirroring.DoRestore error / %s', 11, 1, @ErrorMessage ) End Catch End -- yMirroring.DoRestore GO If Db_name() <> 'Master' Use master GO -- previous version cleanup if object_id('dbo.CreateNetworkDrive') is not null exec sp_procoption N'dbo.CreateNetworkDrive', N'startup', N'false' If object_id('dbo.CreateNetworkDrive') is not null drop proc dbo.CreateNetworkDrive GO If object_id('YouSqlDbaAutostart_ReconnectNetworkDrive') is not null drop proc YouSqlDbaAutostart_ReconnectNetworkDrive go -- new version If object_id('YourSqlDbaAutostart_ReconnectNetworkDrive') is not null drop proc YourSqlDbaAutostart_ReconnectNetworkDrive go Create Or Alter Proc YourSqlDbaAutostart_ReconnectNetworkDrive As Begin ------------------------------------------------------------------- -- The "YouSqlDbaAutostart_ReconnectNetworkDrive" procedure is part of YourSQLDba. ------------------------------------------------------------------- Declare @DriveLetter nchar(2) Declare @unc nvarchar(255) Declare @cmd nvarchar(4000) Declare @sql nvarchar(4000) Set @DriveLetter = '' while 1=1 Begin Select Top 1 @DriveLetter=DriveLetter, @unc=Unc From YourSQLDba.Maint.NetworkDrivesToSetOnStartup Where DriveLetter > @DriveLetter if @@ROWCOUNT = 0 break Begin Try set @sql = ' If Db_name() <> "YourSqlDba" Use YourSqlDba Print @cmd exec YourSQLDba.yMaint.SaveXpCmdShellStateAndAllowItTemporary exec xp_cmdshell @cmd, NO_OUTPUT exec YourSQLDba.yMaint.RestoreXpCmdShellState ' Set @sql = Replace( @Sql, '"', '''') Set @cmd = 'net use /Delete' Set @cmd = Replace( @cmd, '', @DriveLetter ) Exec sp_executeSql @Sql, N'@cmd nvarchar(4000)', @cmd Set @cmd = 'net use ' Set @cmd = Replace( @cmd, '', @DriveLetter ) Set @cmd = Replace( @cmd, '', @unc ) Exec sp_executeSql @Sql, N'@cmd nvarchar(4000)', @cmd End Try Begin Catch declare @msg nvarchar(max) Set @msg = STR(error_number())+' '+ERROR_MESSAGE () print @msg exec YourSQLDba.yMaint.RestoreXpCmdShellState End Catch End End -- YourSqlDbaAutostart_ReconnectNetworkDrive GO exec sp_procoption N'YourSqlDbaAutostart_ReconnectNetworkDrive', N'startup', N'true' GO If DB_NAME()<> 'YourSqlDba' USE YourSqlDba GO -- ------------------------------------------------------------------------- -- Prepare databases for upgrade by changing their names and doing a backup -- before upgrade. Users are automatically kicked out of the database. -- The goal in changing names is prevent other users or applications -- or services to connect. The DBA needs to have a means to have exclusive -- access to new datasource definitions or connect strings -- reflecting the new databases names. -- Suffix supplied by @DbNameSuffixForMaintenance is -- added to the name of databases supplied by @dbList. The backup -- reflect the name of the new database name and is placed into the backup path. -- Backup can be bypassed by supplying empty string to @PathOfBackupBeforeMaintenance -- but it is obviously not recommanded. -- A little table with a long name : "RecoveryModelBeforePrepDbForMaintenanceMode" -- is used as its name implies to keep track of the database recovery model -- so if the upgrade process changes it, it will be brought to its original state -- after running ReturnDbToNormalUseFromMaintenanceMode -- To minimize the increase in size of the log file during the upgrade, it is possible to -- set @SetRecoveryModeToSimple to 1. Doing so will put the database into SIMPLE -- recovery mode until the «ReturnDbToNormalUseFromMaintenanceMode» is called. -- ------------------------------------------------------------------------- -- @@MARK: Tooling for database on application update Create Or Alter Procedure Maint.PrepDbForMaintenanceMode @DbList nVARCHAR(max) = '' -- @DbList : See comments later for further explanations , @DbNameSuffixForMaintenance nvarchar(128) , @PathOfBackupBeforeMaintenance nvarchar(512) = NULL , @SetRecoveryModeToSimple int = 0 as Begin Set nocount on Set @DbNameSuffixForMaintenance= isnull(@DbNameSuffixForMaintenance, '') Set @PathOfBackupBeforeMaintenance = ISNULL (@PathOfBackupBeforeMaintenance, '') If Right(@PathOfBackupBeforeMaintenance ,1) = '\' Set @PathOfBackupBeforeMaintenance = Left(@PathOfBackupBeforeMaintenance, len(@PathOfBackupBeforeMaintenance) - 1) Select d.name collate database_default as Dbname, bl.lastLogBkpFile, bl.EncryptionAlgorithm, bl.EncryptionCertificate, row_number() over (order by line) seq into #Tmp From yUtl.SplitParamInRows (@dbList) AS X join master.sys.databases d on d.name = x.line collate database_default left join Maint.JobLastBkpLocations bl on bl.dbName = d.name collate database_default Where d.name Not Like ('%[_]' + replace(@DbNameSuffixForMaintenance, '_', '[_]')); Declare @name sysname Declare @sql nvarchar(max) Declare @seq int Declare @lastLogBkpFile nvarchar(512) Declare @msgErr nvarchar(max) Declare @EncryptionAlgorithm nvarchar(10) Declare @EncryptionCertificate nvarchar(100) Set @seq = 0 While (1=1) Begin Select top 1 @name = DbName, @seq = seq, @lastLogBkpFile = lastLogBkpFile, @EncryptionAlgorithm = EncryptionAlgorithm, @EncryptionCertificate = EncryptionCertificate from #Tmp Where seq > @seq Order by seq If @@rowcount = 0 break Begin Try Set @sql = ' If Db_name() <> "Master" Use master; Update [YourSQLDba].[Maint].[JobLastBkpLocations] Set keepTrace=1 Where dbName="" If Not Exists (Select * From [].sys.tables Where name="RecoveryModelBeforePrepDbForMaintenanceMode") Select convert(sysname, DATABASEPROPERTYEX ("", "recovery")) as recovery_model_desc Into [].dbo.RecoveryModelBeforePrepDbForMaintenanceMode Alter database [] Set Single_User With Rollback Immediate Alter database [] MODIFY NAME = [_] Alter database [_] Set MULTI_USER With Rollback Immediate ' Set @sql = replace(@sql, '', @name) Set @sql = replace(@sql, '', @DbNameSuffixForMaintenance) Set @sql = replace(@sql, '"', '''') --print @sql exec(@sql) Set @sql = ' use [_]; Begin Transaction PrepDbForMaintenanceMode With mark "Mark to point in time restore for RestoreDbAtStartOfMaintenanceMode" Update dbo.RecoveryModelBeforePrepDbForMaintenanceMode Set recovery_model_desc = recovery_model_desc Commit Transaction PrepDbForMaintenanceMode ' Set @sql = replace(@sql, '', @name) Set @sql = replace(@sql, '', @DbNameSuffixForMaintenance) Set @sql = replace(@sql, '"', '''') --print @sql exec(@sql) Print @name + ' renamed to ' + @name + '_' + @DbNameSuffixForMaintenance + ' for maintenance' End Try Begin Catch Set @msgErr = @name + '> ' + ERROR_MESSAGE() Raiserror (N'%s', 11, 1, @msgErr) End Catch If @PathOfBackupBeforeMaintenance = '' And @lastLogBkpFile Is Null Begin Raiserror (N'The database has no log backups and you did not specified a value for parameter @PathOfBackupBeforeMaintenance so it will not be possible to restore the database state at the start of the maintenance in case of a failure of the maintenance process', 11, 1) End Else Begin Begin Try -- Always make a log backup if the database has a log backup file exists for this database If @lastLogBkpFile IS Not Null Begin Set @sql = yMaint.MakeBackupCmd( @name + '_' + @DbNameSuffixForMaintenance, 'L', @lastLogBkpFile, 0, Null, @EncryptionAlgorithm, @EncryptionCertificate) exec(@sql) End If @PathOfBackupBeforeMaintenance <> '' Begin Set @sql = 'EXECUTE [YourSQLDba].[Maint].[SaveDbCopyOnly] @dbname = "_",@PathAndFilename="\_.Bak"' Set @sql = replace(@sql, '', @name) Set @sql = replace(@sql, '', @DbNameSuffixForMaintenance) Set @sql = replace(@sql, '', @PathOfBackupBeforeMaintenance) Set @sql = replace(@sql, '"', '''') exec(@sql) End End Try Begin Catch Set @msgErr = @name + '_' + @DbNameSuffixForMaintenance + '> ' + ERROR_MESSAGE() Raiserror (N'%s', 11, 1, @msgErr) End Catch End -- If specified with the parameter @SetRecoveryModeToSimple, set the -- recovery model to simple during the maintenance if @SetRecoveryModeToSimple = 1 begin Set @sql = ' if DATABASEPROPERTYEX ("_", "recovery") <> "SIMPLE" Alter database [_] Set RECOVERY SIMPLE WITH NO_WAIT ' Set @sql = replace(@sql, '', @name) Set @sql = replace(@sql, '', @DbNameSuffixForMaintenance) Set @sql = replace(@sql, '"', '''') Begin Try --print @sql exec(@sql) Print @name + '_' + @DbNameSuffixForMaintenance + ' is in SIMPLE recovery model' End Try Begin Catch Set @msgErr = @name + '_' + @DbNameSuffixForMaintenance + '> ' + ERROR_MESSAGE() Raiserror (N'%s', 11, 1, @msgErr) End Catch end End End -- Maint.PrepDbForMaintenanceMode GO -- ------------------------------------------------------------------------- -- procedure you need to use ReturnDbToNormalUseFromMaintenanceMode -- ------------------------------------------------------------------------- Create Or Alter Procedure yMaint.PrepareRestoreDbToLogMarkCommand @DbName nVARCHAR(max) , @FullBkpFile nvarchar(512) , @LogMarkName nvarchar(32) , @SqlCmd nvarchar(max) output As Begin Declare @sql nvarchar(max) Declare @RestoreLog nvarchar(max) Declare @position smallint Declare @LogBkpFile nvarchar(512) Declare @MediaSetId int Set @SqlCmd = ' Restore Database [] From Disk = "" With NoRecovery ,stats=1, replace Restore Log [] With Recovery ' -- Find all log backups associated with the full backup Set @MediaSetId = 0 While 1=1 Begin Select Top 1 @MediaSetId= bm.media_set_id, @LogBkpFile = bm.physical_device_name From ( Select bs.database_name, bs.first_lsn From YourSQLDba.Maint.JobLastBkpLocations lb join msdb.dbo.backupset bs on bs.database_name = lb.dbName collate database_default And RIGHT( bs.name, Len(lb.lastFullBkpFile)) = lb.lastFullBkpFile collate database_default Where lb.lastFullBkpFile = @FullBkpFile And (bs.name like 'YourSqlDba%' or bs.name like 'SaveDbOnNewFileSet%') And bs.type = 'D' ) X Join msdb.dbo.backupset bs On bs.database_name = X.database_name And bs.database_backup_lsn = X.first_lsn Join msdb.dbo.backupmediafamily bm On bm.media_set_id = bs.media_set_id Where bs.type = 'L' And bm.media_set_id > @MediaSetId If @@ROWCOUNT = 0 Break Exec yMaint.CollectBackupHeaderInfoFromBackupFile @LogBkpFile -- Restore all log backup until the log mark Set @position = 0 while 1=1 Begin Select Top 1 @position = Position From Maint.TemporaryBackupHeaderInfo Where Spid = @@spid And BackupType = 2 And Position > @position Order by Position If @@rowcount= 0 break Set @RestoreLog = 'Restore Log [] From Disk="" With FILE=, NoRecovery, STOPATMARK=""' Set @RestoreLog = Replace(@RestoreLog, '', Convert(nvarchar(255), @position)) Set @SqlCmd = replace(@SqlCmd, '', @RestoreLog + Char(13) + Char(10) + '' ) Set @SqlCmd = replace(@SqlCmd, '', @LogBkpFile) End End Set @SqlCmd = replace(@SqlCmd, '', '') Set @SqlCmd = replace(@SqlCmd, '', @DbName) Set @SqlCmd = replace(@SqlCmd, '', @LogMarkName) Set @SqlCmd = replace(@SqlCmd, '', @FullBkpFile) Set @SqlCmd = replace(@SqlCmd, '"', '''') End -- yMaint.PrepareRestoreDbToLogMarkCommand GO -- ------------------------------------------------------------------------- -- Restore databases to their state before maintenance process is started. -- They are still in maintenance mode and original backup remains available -- for other maintenance attempts. -- Requires that PrepDbForMaintenanceMode was used in the way necessary to -- generate a backup (i.e. by supplying a valid backup path, not empty string). -- Your must supply in @bdlist each database you want to restore. -- Can be used to abort maintenance attempt and report it later, but after this -- procedure you need to use ReturnDbToNormalUseFromMaintenanceMode -- ------------------------------------------------------------------------- Create Or Alter Procedure Maint.RestoreDbAtStartOfMaintenanceMode @DbList nVARCHAR(max) , @DbNameSuffixForMaintenance nvarchar(128) , @PathOfBackupBeforeMaintenance nvarchar(512) = NULL as Begin Set nocount on Set @DbNameSuffixForMaintenance = isnull(@DbNameSuffixForMaintenance, '') Set @PathOfBackupBeforeMaintenance = ISNULL (@PathOfBackupBeforeMaintenance, '') If Right(@PathOfBackupBeforeMaintenance ,1) = '\' Set @PathOfBackupBeforeMaintenance= Left(@PathOfBackupBeforeMaintenance, len(@PathOfBackupBeforeMaintenance) - 1) Select d.name collate database_default as Dbname , lastLogBkpFile , lastFullBkpFile , row_number() over (order by line) seq into #Tmp From yUtl.SplitParamInRows (@dbList) AS X join master.sys.databases d on d.name = x.line + '_' + @DbNameSuffixForMaintenance collate database_default left join Maint.JobLastBkpLocations bl on bl.dbName = x.line collate database_default Declare @name sysname Declare @sql nvarchar(max) Declare @seq int Declare @lastLogBkpFile nvarchar(512) Declare @lastFullBkpFile nvarchar(512) Declare @msgErr nvarchar(max) Set @seq = 0 While (1=1) Begin Select top 1 @name = DbName , @seq = seq , @lastLogBkpFile = lastLogBkpFile , @lastFullBkpFile = lastFullBkpFile from #Tmp Where seq > @seq Order by seq If @@rowcount = 0 break If @PathOfBackupBeforeMaintenance = '' And @lastLogBkpFile Is Null Begin Raiserror (N'No backup for database %s', 11, 1, @name) --Print 'No backup for database «' + @name + '»' End Else Begin -- If a backup file is specified we restore form the Full backup in this path. -- Else we Restore the last Full Backup and all the log Backup until the start of the maintenance mode Begin Try If @PathOfBackupBeforeMaintenance <> '' Begin -- Kill all connection Before launching the RESTORE Command Set @sql = ' ALTER DATABASE [] SET Single_User WITH ROLLBACK IMMEDIATE ALTER DATABASE [] SET MULTI_USER WITH ROLLBACK IMMEDIATE Restore Database [] From Disk = "\.Bak" With stats=1, replace ' Set @sql = replace(@sql, '', @name) Set @sql = replace(@sql, '', @PathOfBackupBeforeMaintenance) Set @sql = replace(@sql, '"', '''') Set @sql = yExecNLog.Unindent_TSQL( @sql ) --print @sql exec(@sql) End Else Begin Exec yMaint.PrepareRestoreDbToLogMarkCommand @DbName=@name , @FullBkpFile=@lastFullBkpFile , @LogMarkName='PrepDbForMaintenanceMode' , @SqlCmd=@sql out -- Kill all connection Before launching the RESTORE Command Set @sql = ' ALTER DATABASE [] SET Single_user WITH ROLLBACK IMMEDIATE ALTER DATABASE [] SET MULTI_USER WITH ROLLBACK IMMEDIATE '+@Sql Set @sql = replace(@sql, '', @name) Set @sql = replace(@sql, '', @PathOfBackupBeforeMaintenance) Set @sql = replace(@sql, '"', '''') Set @sql = yExecNLog.Unindent_TSQL( @sql ) --print @sql exec(@sql) End Print @name + ' restored ' End Try Begin Catch Set @msgErr = @name + '> ' + ERROR_MESSAGE() Raiserror (N'%s', 11, 1, @msgErr) End Catch End End Drop Table If Exists #Tmp End -- Maint.RestoreDbAtStartOfMaintenanceMode GO -- -------------------------------------------------------------------------- -- Restore databases names to their original value and original recovery mode. -- Database list need to be supplied, and suffix use to rename the database. -- If the database is part of regular YourSqlDba full backups, then a new -- file backup set (a new total backup and a new log backup are directed to -- a new file set) so you keep backup before maintenance process. -- Backup files generated by PrepDbForMaintenanceMode at start of maintenance process -- are also left there and you must perform a manual cleanup of them. -- ------------------------------------------------------------------------- Create Or Alter Procedure Maint.ReturnDbToNormalUseFromMaintenanceMode @DbList nVARCHAR(max) = '' -- @DbList : See comments later for further explanations , @DbNameSuffixForMaintenance nvarchar(128) as Begin Set nocount on Set @DbNameSuffixForMaintenance= isnull(@DbNameSuffixForMaintenance, '') Select d.name as Dbname, row_number() over (order by line) seq into #Tmp From yUtl.SplitParamInRows (@dbList) AS X join master.sys.databases d on d.name collate database_default = x.line + '_' + @DbNameSuffixForMaintenance Declare @dbOrig sysname Declare @sql nvarchar(max) Declare @seq int Declare @msgErr nvarchar(max) Declare @DbAndSuffix sysname Declare @recovery_model_saved Int Declare @recovery_model sysname Set @seq = 0 Select * from #tmp Begin Try While (1=1) Begin Select top 1 @DbAndSuffix = DbName, @seq = seq from #Tmp Where seq > @seq Order by seq If @@rowcount = 0 break -- remove the suffix from the name Set @dbOrig = STUFF( @DbAndSuffix , len(@DbAndSuffix)-Len(@DbNameSuffixForMaintenance) , Len(@DbNameSuffixForMaintenance)+1, '' ) Set @sql = ' Use []; Set @recovery_model_saved = convert(int, objectpropertyex(object_id("dbo.RecoveryModelBeforePrepDbForMaintenanceMode"), "isUserTable")) ' Set @sql = replace(@sql, '', @DbAndSuffix) Set @sql = replace(@sql, '"', '''') Exec Sp_ExecuteSql @Sql, N'@recovery_model_saved int output', @recovery_model_saved Output If @Recovery_model_saved = 1 Begin Set @sql = 'select @recovery_model = recovery_model_desc From [].dbo.RecoveryModelBeforePrepDbForMaintenanceMode' Set @sql = replace(@sql, '', @DbAndSuffix) Exec Sp_ExecuteSql @Sql, N'@recovery_model sysname output', @recovery_model Output Set @sql = ' If Db_name() <> "Master" Use Master; Alter database [] SET Single_User WITH ROLLBACK IMMEDIATE Alter database [] SET Multi_User Alter Database [] Set RECOVERY Drop Table If Exists [].dbo.RecoveryModelBeforePrepDbForMaintenanceMode ' Set @sql = replace(@sql, '', @DbAndSuffix) Set @sql = replace(@sql, '', @recovery_model) Set @sql = replace(@sql, '"', '''') Exec (@sql) End Set @sql = ' If Db_name() <> "Master" Use Master; Alter database [] SET Single_user WITH ROLLBACK IMMEDIATE Alter database [] MODIFY NAME = [] Alter database [] Set MULTI_USER With Rollback Immediate ' Set @sql = replace(@sql, '', @DbAndSuffix) Set @sql = replace(@sql, '', @dbOrig) Set @sql = replace(@sql, '"', '''') Exec (@sql) If Exists(Select * From [YourSQLDba].[Maint].[JobLastBkpLocations] Where dbName=@DbOrig And lastFullBkpFile Is Not Null) Begin Exec [YourSQLDba].[Maint].[SaveDbOnNewFileSet] @DbName=@DbOrig Update [YourSQLDba].[Maint].[JobLastBkpLocations] Set keepTrace=0 Where dbName=@DbOrig End Print @dbOrig + ' returned to normal use' End End Try Begin Catch Set @msgErr = @DbOrig + '> ' + ERROR_MESSAGE() Raiserror (N'%s', 11, 1, @msgErr) End Catch End -- Maint.ReturnDbToNormalUseFromMaintenanceMode GO -- @@MARK: TODO : Check for use Create Or Alter Proc S#.CopyAllSqlModules @SrcDb Sysname, @DestDb Sysname as Begin Set nocount on --Drop Table If Exists #CopyAllSqlModules Select SrcDb = QUOTENAME(@SrcDb), DestDb = QUOTENAME(@DestDb) Into #CopyAllSqlModules --Drop Table If Exists #ModuleDefInSrcDb -- Select top 0 to template #ModuleDefInSrcDb from Sys.Sys_SqlModules + extra cols Select Top 0 M.*, Names.* Into #ModuleDefInSrcDb From Sys.sql_modules M Cross Join (select n=cast('' as sysname)) as n Cross Apply (Select sc=n, n, SrcObj=n, DestObj=n) as Names Declare @SqlDefMod Nvarchar(max) Select @SqlDefMod=SqlDefMod From (Select ReplacePairs=(Select SrcDb, DestDb From #CopyAllSqlModules For Json Path)) as ReplacePairs Cross Apply (Select SqlDefMod=R.code From S#.GetTemplateFromCmtAndReplaceTags('===GetModuleDefAndNames===', NULL, ReplacePairs) as R) as Sql /*===GetModuleDefAndNames=== --------------------------------------------------------------------------- -- S#.CopyAllSqlModules (copy object definitions) --------------------------------------------------------------------------- Use #SrcDb# Insert into #ModuleDefInSrcDb select M.*, sc, n, srcObj, DestObj From Sys.sql_modules as M Cross JOIN (Select d='.') as d CROSS APPLY (Select sc=Object_schema_name(object_id)) as sc CROSS APPLY (Select n=Object_name(object_id)) as n CROSS APPLY (Select SrcObj = QUOTENAME(sc)+d+QUOTENAME(n)) as SrcObj CROSS APPLY (Select DestObj = '#DestDb#'+d+SrcObj) as DestObj ===GetModuleDefAndNames===*/ Print @SqlDefMod Exec (@SqlDefMod) -- Select top 0 to template #ObjectsInSrcDb and #ObjectsInDestDb from Sys.Objects Drop Table If Exists #ObjectsInSrcDb; Select Top 0 * Into #ObjectsInSrcDb From Sys.objects Drop Table If Exists #ObjectsInDestDb; Select Top 0 * Into #ObjectsInDestDb From Sys.objects Declare @SqlGetObj Nvarchar(max)='' Select @SqlGetObj=@SqlGetObj+SqlGetObj -- concat because 2 rows of statements From (Select Db=SrcDb, Typ='Src' From #CopyAllSqlModules Union All Select Db=DestDb, Typ='Dest' From #CopyAllSqlModules) as Db Cross Apply (Select ReplacePairs=(Select Db, Typ For Json Path)) as ReplacePairs Cross Apply (Select SqlGetObj=R.code From S#.GetTemplateFromCmtAndReplaceTags('===GetObjects===', NULL, ReplacePairs) as R) as Sql /*===GetObjects=== --------------------------------------------------------------------------- -- S#.CopyAllSqlModules (copy object infos object_id and type) for each Db --------------------------------------------------------------------------- Use #Db# Insert into #ObjectsIn#Typ#Db select * From Sys.Objects Where objectpropertyEx(Object_id, 'isMsShipped') = 0 And Type IN ('V','IF','FN','TF','P','TR') ===GetObjects===*/ Print @SqlGetObj Exec (@SqlGetObj ) --Drop Table If Exists #DropsAndCreates -- #DropAndCreates is created by the select into below ;With MatchTypToDropTypAndTypSequence as ( Select * from (Values ('V', 'View' , '1VUEF') , ('IF', 'Function' , '1VUEF') , ('FN', 'Function' , '1VUEF') , ('TF', 'Function' , '1VUEF') , ('P', 'Procedure', '2PROC') , ('TR', 'Trigger' , '3TRIG') ) as T (Typ, DropTyp, ModuleSeq) ) Select Seq=row_number() OVER (ORDER BY SrcSeq.moduleSeq, DestDrop.DropTyp, sc, n) , sc , SrcObj , DestObj , DropStmt , CreateObj = M.definition , aNull = M.uses_ansi_nulls , qIden = M.uses_quoted_identifier , SrcSeq.ModuleSeq , Sql Into #DropsAndCreates from (Select DestDb, SrcDb From #CopyAllSqlModules) as Prm CROSS JOIN #ModuleDefInSrcDb M CROSS APPLY (Select aNull=IIF(M.uses_ansi_nulls=1,'ON', 'OFF')) aNull CROSS APPLY (Select qIden=IIF(M.uses_quoted_identifier=1,'ON', 'OFF')) qIden JOIN #ObjectsInSrcDb OB On OB.object_id = M.object_id -- Get module sequence type for processing in a correct creation order -- that increases the chance of success. Views and functions aren't stored proc -- invocation order dependent, but triggers are. So the most efficient -- processing order is views, functions, procedures, triggers. JOIN MatchTypToDropTypAndTypSequence as SrcSeq ON SrcSeq.Typ=Ob.Type -- get type at destination, just in case object with same name exists but with a different type -- at each new pass, the object is dropped and recreated, so the type should eventually match the source LEFT JOIN #ObjectsInDestDb OBE ON OBE.object_id = OBJECT_ID(M.DestObj) -- get matching type word for type of sys.objects LEFT JOIN MatchTypToDropTypAndTypSequence as destDrop ON destDrop.Typ=Obe.Type Cross Apply (Select DropStmt= ISNULL('Drop '+destDrop.DropTyp+' If Exists '+ SrcObj,'') ) as DropStmt /*===DropRecreateAtDest=== --------------------------------------------------------------------------- -- S#.CopyAllSqlModules (drop/recreate objects at destination) --------------------------------------------------------------------------- Use #SrcDb#; Declare @DropStmt nvarchar(max) = '#DropStmt#' -- destination drop statement If @DropStmt <> '' Exec sp_executesql N'Use #DestDb#; Exec sp_executeSql @Sql', N'@Sql nvarchar(max)', @Sql=@DropStmt Declare @CreateStmt nvarchar(max) Select @CreateStmt = Definition From #ModuleDefInSrcDb Where SrcObj = '#SrcObj#' -- source objectId Begin Try Exec sp_executesql N' Use #DestDb#; Set Ansi_NULLS #ANull# Set Quoted_identifier #Qiden# Exec sp_executeSql @Sql ' , N'@Sql nvarchar(max)' , @Sql=@CreateStmt Insert into #ObjectsCreated Values ('#SrcObj#') End Try Begin Catch Declare @msg nvarchar(4000)='Cannot create object #DestObj# because of error '+Str(error_number())+ ': ' +Error_Message() Print @Msg End Catch ===DropRecreateAtDest===*/ CROSS APPLY (Select ReplacePairs=(Select SrcDb, DestDb, Ob.Object_id, ANull, QIden, SrcObj, DestObj, DropStmt For Json Path)) as ReplacePairs Cross Apply (Select Sql=R.code From S#.GetTemplateFromCmtAndReplaceTags('===DropRecreateAtDest===', NULL, ReplacePairs) as R) as Sql ORDER BY SrcSeq.moduleSeq, destDrop.DropTyp, sc, n Declare @StopPass Int = 0 Declare @nbOfObjCreated Int Declare @Seq Int Declare @Sql Nvarchar(max) Declare @Sc Nvarchar(max) = '' Drop Table If Exists #ObjectsCreated Create table #ObjectsCreated(N sysname); Drop table If Exists #PreviousPassObjectCreated Create table #PreviousPassObjectCreated(N sysname); -- ensure destination schemas aren't missing While (1=1) Begin Select top 1 @Sc=Sc From #DropsAndCreates Where Sc > @Sc Order by Sc If @@ROWCOUNT = 0 Break Select @Sql=Sql From (select sc=@Sc, DestDb From #CopyAllSqlModules) as Sc Cross Apply (Select ReplacePairs=(Select DestDb, sc For Json Path)) as ReplacePairs Cross Apply (Select Sql=R.code From S#.GetTemplateFromCmtAndReplaceTags('===SchemaDup===', NULL, ReplacePairs) as R) as Sql /*===SchemaDup=== --------------------------------------------------------------------------- -- S#.CopyAllSqlModules (assert schema presence) --------------------------------------------------------------------------- Use #DestDb#; If Schema_id('#Sc#') IS NULL Exec('Create Schema [#Sc#] Authorization Dbo') ===SchemaDup===*/ print @sql Exec(@sql) End -- drop if exists and create objects in destination database as much that can be created -- loop drop if exists/create until nothing nothing yet created hasn't been created is previous pass -- the lass pass implicitely make all dependencies correct While (1=1) Begin Insert into #PreviousPassObjectCreated Select * From #ObjectsCreated Truncate table #ObjectsCreated -- elements are added in the exec (@Sql) in the loop -- a full pass of drop/create for each object Set @Seq=0 While (1=1) Begin -- get next seq > previous @seq in increasing order Select top 1 @Sql=Sql, @Seq=Seq From #DropsAndCreates Where Seq>@seq Order By Seq If @@ROWCOUNT = 0 Break Print @sql Exec(@Sql) End -- once an attempt is done at creating all objects, break the loop if number of objects created -- isnt't superior then the one at the previous pass If Not exists (Select * from #ObjectsCreated Except Select * from #PreviousPassObjectCreated) Break End End go -- test --Use tempdb --RESTORE DATABASE [YourSQLDba_Export] --FROM DISK = N'\\MAURICESQL\Sql2k19Backups\YourSQLDbaExp.bak' WITH FILE = 1 --, MOVE N'YourSQLDba' TO N'C:\Program Files\Microsoft SQL Server\SQL2K19\Data\YourSqlDba_Export.Mdf' --, MOVE N'YourSQLDba_Log' TO N'C:\Program Files\Microsoft SQL Server\SQL2K19\Logs\YourSqlDba_Export_log.ldf' --, NOUNLOAD, STATS = 5 --RESTORE DATABASE [YourSQLDba_Export2] --FROM DISK = N'\\MAURICESQL\Sql2k19Backups\YourSQLDbaExp.bak' WITH FILE = 1 --, MOVE N'YourSQLDba' TO N'C:\Program Files\Microsoft SQL Server\SQL2K19\Data\YourSqlDba_Export2.Mdf' --, MOVE N'YourSQLDba_Log' TO N'C:\Program Files\Microsoft SQL Server\SQL2K19\Logs\YourSqlDba_Export2_log.ldf' --, NOUNLOAD, STATS = 5 --Exec YourSqlDba.S#.CopyAllSqlModules 'YourSqlDba', 'YourSqlDba_Export' --Exec YourSqlDba.S#.CopyAllSqlModules 'YourSqlDba_Éxport', 'YourSqlDba_Export2' -- Reset of default database to YourSqlDba for YourSqlDba jobs steps. -- required since all objects are qualified by YourSqlDba -- declare @job_id UniqueIdentifier declare @step_id int declare @tmp table (job_id UniqueIdentifier, step_id int) Insert into @tmp Select job_id, step_id from msdb.dbo.sysjobsteps Where step_name like '%YourSqlDba%' And database_name not like '%YourSqlDba%' -- for an unknow reason a direct update of database_name on this column doesn't work. -- so we look using sp_update_jobstep while (1=1) Begin Select top 1 @job_id = job_id, @step_id = step_id from @tmp If @@rowcount = 0 break EXEC msdb.dbo.sp_update_jobstep @job_id = @job_id, @step_id = @step_id, @database_name = N'YourSQLDba' Delete from @tmp where job_id = @job_id And step_id = @step_id End go -- --------------------------------------------------------------------------------------- -- Proc to create database export -- --------------------------------------------------------------------------------------- -- @@MARK: TODO : Check for use Exporting database Create Or Alter Procedure yExport.CreateExportDatabase @dbName sysname , @collation sysname = NULL , @stopOnError Int = 1 , @jobNo int as Begin set nocount on declare @sql nvarchar(max); set @sql = '' declare @sqlM nvarchar(max); set @sqlM = '' declare @minSizeData Int declare @minSizeLog Int declare @rc int Declare @fgId Int Declare @type_desc sysname Declare @fSpec nvarchar(max) Declare @fgn sysname Declare @Name sysname Declare @PhysicalName nvarchar(512) Declare @DataSpaceid int Declare @fileGroupName sysname Declare @fileGroupNameAv sysname Declare @FileId int Declare @Size nvarchar(40) Declare @maxSize nvarchar(40) Declare @maxSizeUnit nvarchar(40) Declare @Growth nvarchar(40) Declare @GrowthMode nvarchar(2) Declare @context nvarchar(200) Declare @Info nvarchar(max) Declare @err nvarchar(max) If databasepropertyex(@dbName+'_Export','status') IS NOT NULL Begin Set @err = 'Error - Database "' + @dbName + '_Export" must be removed first as it is the destination name of exported database "' + @dbName + '"' Exec yExecNLog.LogAndOrExec @context = 'yExport.CreateExportDatabase' , @Info = 'Database for export must not be there' , @err = @err , @raiseError = @stopOnError return(1) End Set @sql = ' CREATE DATABASE [_Export] ON Log On Collate ' Set @sql = REPLACE(@Sql, '', @dbName) If @collation Is Null Set @sql = REPLACE(@Sql, '', convert(sysname, DatabasepropertyEx(@dbName, 'Collation'))) else Set @sql = REPLACE(@Sql, '', @collation) Set @fileId = 0 Set @DataSpaceid = 0 Set @fileGroupNameAv = '' Set @fSpec = ' ' While (1=1) begin Set @sqlM = -- fichiers data, du groupe primaire en premier, puis des autres ' use [] Select Top 1 @FileId = file_id , @dataSpaceId = data_space_id , @fileGroupName = Filegroup_Name (data_space_id) , @Name = name , @PhysicalName = physical_name , @Size = str(Case when size / 10 < 1024*200 Then 1024*200 else (size / 10) * 8 End)+"KB" -- translate to KB actually 8Kb pages , @MaxSize = Case When max_Size <= 0 Then "Unlimited" When max_Size = 268435456 Then "2" Else STR(max_size * 8,10) End , @maxSizeUnit = Case when max_Size = -1 Then "" when max_Size = 268435456 Then "TB" Else "KB" End , @Growth = Case When is_percent_growth = 1 then Str(Growth,10) Else 1024*200 End -- translate to KB 8KB pages , @GrowthMode = Case When is_percent_growth = 1 Then "%" Else "KB" End From sys.database_files d Where data_space_id > 0 And -- no log files Str(data_space_id)+Str(File_id) > Str(@dataSpaceId)+Str(@Fileid) And (convert(nvarchar, serverproperty("productversion")) not like "9.%" Or type_desc <> "FULLTEXT") -- exclude unusual file setup made when a sql2005 database with full text is restored to a version above And not exists(select * from sys.fulltext_Catalogs F where F.name = replace(d.name, "ftrow_", "")) Order by data_space_id, File_id ' Set @sqlM = replace (@sqlM, '', @dbName) Set @sqlM = replace (@sqlM, '"', '''') Exec sp_executeSql @SqlM ,N' @FileId int output , @dataSpaceId int output , @fileGroupName sysname output , @Name sysname Output , @PhysicalName nvarchar(512) Output , @Size nvarchar(40) Output , @MaxSize nvarchar(40) Output , @maxSizeUnit nvarchar(40) Output , @Growth nvarchar(40) Output , @GrowthMode nvarchar(2) Output ' , @FileId = @FileId Output , @dataSpaceId = @dataSpaceId Output , @fileGroupName = @fileGroupName Output , @Name = @name Output , @PhysicalName = @PhysicalName output , @Size = @Size Output , @maxSize = @maxSize Output , @maxSizeUnit = @maxSizeUnit Output , @Growth = @Growth Output , @GrowthMode = @GrowthMode Output if @@ROWCOUNT = 0 Break If @FileGroupNameAv <> @fileGroupName Begin Set @fSpec = REPLACE( @fSpec, '' , Case -- avoid use of primary keyword and add a comma if more that one file When @fileGroupName = 'Primary' Then '' Else 'FILEGROUP ' +@FileGroupName End) Set @FileGroupNameAv = @fileGroupName End Else Set @fSpec = REPLACE(@fSpec, ', ', ', ') Set @fSpec = REPLACE(@fSpec, '', ' ( NAME = , FILENAME = "" , SIZE = , MAXSIZE = , FILEGROWTH = ) , ' ) Set @fSpec = REPLACE(@fSpec, '', Replace (@name, @dbName, @dbName+'_Export')) Set @fSpec = REPLACE(@fSpec, '', Replace (@physicalName, @dbName, @dbName+'_Export') ) Set @fSpec = REPLACE(@fSpec, '', @Size) Set @fSpec = REPLACE(@fSpec, '', @maxSize + @maxSizeUnit) Set @fSpec = REPLACE(@fSpec, '', @Growth + @growthMode) End Set @fSpec = REPLACE(@fSpec, ', ', '') -- remove remaining tag Set @fSpec = REPLACE(@fSpec, '', '') -- remove remaining tag Set @Sql = REPLACE (@sql, '', @fSpec) -- insert it into create database stmt Set @fileId = 0 Set @fSpec = ' ' While (1=1) begin Set @sqlM = -- fichiers data, du groupe primaire en premier, puis des autres ' use [] Select Top 1 @FileId = file_id , @dataSpaceId = data_space_id , @fileGroupName = Filegroup_Name (data_space_id) , @Name = name , @PhysicalName = physical_name , @Size = str(Case when size / 10 < 1024*200 Then 1024*200 else (size / 10) * 8 End)+"KB" -- translate to KB actually 8Kb pages , @MaxSize = Case When max_Size <= 0 Then "Unlimited" When max_Size = 268435456 Then "2" Else STR(max_size * 8,10) End , @maxSizeUnit = Case when max_Size = -1 Then "" when max_Size = 268435456 Then "TB" Else "KB" End , @Growth = Case When is_percent_growth = 1 then Str(Growth,10) Else 1024*200 End -- translate to KB 8KB pages , @GrowthMode = Case When is_percent_growth = 1 Then "%" Else "KB" End From sys.database_files Where data_space_id = 0 And -- log files Str(File_id) > Str(@Fileid) Order by File_id ' Set @sqlM = replace (@sqlM, '', @dbName) Set @sqlM = replace (@sqlM, '"', '''') Exec sp_executeSql @SqlM ,N' @FileId int output , @dataSpaceId int output , @fileGroupName sysname output , @Name sysname Output , @PhysicalName nvarchar(512) Output , @Size nvarchar(40) Output , @MaxSize nvarchar(40) Output , @maxSizeUnit nvarchar(40) Output , @Growth nvarchar(40) Output , @GrowthMode nvarchar(2) Output ' , @FileId = @FileId Output , @dataSpaceId = @dataSpaceId Output , @fileGroupName = @fileGroupName Output , @Name = @name Output , @PhysicalName = @PhysicalName output , @Size = @Size Output , @maxSize = @maxSize Output , @maxSizeUnit = @maxSizeUnit Output , @Growth = @Growth Output , @GrowthMode = @GrowthMode Output if @@ROWCOUNT = 0 Break Set @fSpec = REPLACE(@fSpec, '', '( NAME = , FILENAME = "" , SIZE = , MAXSIZE = , FILEGROWTH = ) , ') Set @fSpec = REPLACE(@fSpec, '', Replace (@name, @dbName, @dbName+'_Export')) Set @fSpec = REPLACE(@fSpec, '', Replace (@physicalName, @dbName, @dbName+'_Export')) Set @fSpec = REPLACE(@fSpec, '', @size) Set @fSpec = REPLACE(@fSpec, '', @maxSize+@maxSizeUnit) Set @fSpec = REPLACE(@fSpec, '', @Growth+@GrowthMode) End Set @fSpec = REPLACE(@fSpec, ', ', '') -- remove remaining tag Set @Sql = REPLACE (@sql, '', @fSpec) -- put it into create database Set @sql = replace(@sql, '"', '''') Exec yExecNLog.LogAndOrExec @context = 'yExport.CreateExportDatabase' , @Info = 'Running create database for database export' , @sql = @sql , @raiseError = @stopOnError End -- yExport.CreateExportDatabase GO Create Or Alter Procedure yExport.ExportData @dbName sysname , @stopOnError Int = 1 , @jobNo Int as Begin Set nocount on declare @sql nvarchar(max) -- Declare @nbRow int declare @rc int declare @Ks sysname -- schéma de foreign key declare @USn sysname -- schéma de clé primaire référencée par foreign key declare @Kn sysname -- nom de foreign key declare @Sn sysname -- nom de schema d'index ou de table declare @Tn sysname -- nom de table declare @UTn sysname -- nom de table de clé primaire référencée par foreign key declare @mn sysname -- module name Declare @seqFk Int Declare @cn sysname -- nom de colonne Declare @iden int -- clause identity Declare @defIden sysname -- définition de la clause identity Declare @fgDef sysname -- définition du filegroup de table pour les LOB Declare @fgDat sysname -- définition de filegroup de table pour les rangées Declare @fgLob sysname -- définition du filegroup de table pour les LOB Declare @fgIdx sysname -- définition du filegroup de l'index Declare @is_computed int -- si colonne calculée Declare @computedColDef nvarchar(max) -- si colonne calculée, sa définition declare @Ucn sysname -- nom de colonne de clé primaire référencée par foreign key declare @In sysname -- nom d'index ou de clé primaire ou de contrainte unique declare @Pk Int -- indicateur clé primaire declare @typDesc Sysname -- type d'index clustered, nonclustered declare @dcn sysname -- nom de contrainte default declare @DefName sysname -- définition du default declare @typ sysname -- type d'une colonne ou type d'un objet selon le bout de code local declare @nouvTyp sysname -- nouveau type d'une colonne dans cas de char, varchar, text, image declare @lc nvarchar(8) -- définition de longueur d'une colonne declare @nullSpec nvarchar(8) -- spéc. NULL, NOT NULL Declare @seq Int -- sequence de traitement dans les tables Declare @seqT Int -- sequence des tables Declare @seqC Int -- séquence des colonnes Declare @Def nvarchar(max) -- définition de l'expression qui donne le defaut d'une colonne Declare @Cols nvarchar(max) -- liste des colonnes Declare @ligCols nvarchar(max) -- liste des colonnes sur une même ligne Declare @ColsSelect nvarchar(max) -- liste des colonnes d'une table pour select into, ou colonne d'un index selon usage Declare @colsCreateTable nvarchar(max) -- liste des colonnes d'une table pour alter constraint Declare @colsInsertInto nvarchar(max) -- liste alter des colonnes d'une table pour alter null not null Declare @uCols nvarchar(max) -- liste des colonnes de la clé primaire référencée par la clé unique Declare @ColIdxChar Int Declare @schemaAlt int -- says that a view nust be used to pump the data Declare @ko int -- ordre des colonnes dans la clé Declare @iUniq int -- index unique ou pas Declare @iUniqC Int -- contrainte unique mais pas nécessairement primary key Declare @FKOnClause NVARCHAR(255) Declare @is_not_trusted int -- indique si la contrainte de foreigh key est activée Declare @Info nvarchar(max) Declare @seqIx Int Declare @BigLig nvarchar(max) Declare @IndexOnView int Declare @Created int Declare @FunctionCreated int Declare @TableCreated int Declare @Anull int Declare @qIden int declare @err nvarchar(max) Begin try Create table #Schemas ( Sn sysname Not NULL primary key clustered ) -- table des définitions des types create table #UserDefTypes ( seq int primary key clustered , sn sysname NOT NULL -- nom schema , un sysname NOT NULL -- nom user type , typ sysname NOT NULL -- type , lc nvarchar(10) NULL -- longueur facultative , nullspec nvarchar(10) NULL -- ajouter null ou not null ) -- table des définitions de foreigh key -- if object_id('tempdb..#RefConstraints') is not null drop table #RefConstraints create table #RefConstraints ( seq int primary key clustered , Sn sysname not NULL , Tn sysname not NULL , Kn sysname not NULL , USn sysname not NULL , UTn Sysname Not NULL , UKn sysname not NULL , RefObjId Int not NULL , KeyId Int not NULL , MATCH_OPTION sysname not NULL , UPDATE_RULE sysname not NULL , DELETE_RULE sysname not NULL , Is_not_trusted int not null ) -- liste des colonnes impliquées dans contraintes d'intégrité référentielle des foreigh key -- if object_id('tempdb..#ColsRefConstraints') is not null drop table #ColsRefConstraints create table #ColsRefConstraints ( Sn sysname NOT NULL , Tn sysname NOT NULL , Kn sysname NOT NULL , ordCol Int NOT NULL , cn sysname NOT NULL , USn sysname NOT NULL , UKn sysname NOT NULL , UTn sysname NOT NULL , Ucn sysname NOT NULL ) Create unique clustered index iKC on #ColsRefConstraints (Sn, Tn, Kn, OrdCol) -- liste des tables d'une BD -- if object_id('tempdb..#TablesToExport') is not null drop table #TablesToExport create table #TablesToExport ( seq int primary key clustered , sn sysname NOT NULL -- nom schema , tn sysname NOT NULL -- nom table , Id int NOT NULL -- id de la table , Iden int NOT NULL -- a un identity , fgLob sysname NOT NULL Default '' -- filegroup pour LOB , fgDat sysname NOT NULL Default '' -- filegroup du Data , fgDef sysname NOT NULL Default '' -- default filegroup amoung filegroups ) -- --------------------------------------------------------------------------------------------------- -- table qui conserve les instructions pour rebâtir les statistiques d'origine crées par auto-stats -- --------------------------------------------------------------------------------------------------- -- informations pour regénérer des statistiques sur colonnes des statistiques auto-générées d'une BD -- if object_id('tempdb..#Stats') is not null drop table #Stats create table #Stats ( seq int primary key clustered , sn sysname NOT NULL -- nom schema , tn sysname NOT NULL -- nom table , cn sysname NOT NULL -- nom colonne ) -- liste des colonnes des tables -- if object_id('tempdb..#ColsTablesAMigr') is not null drop table #ColsTablesAMigr create table #ColsTablesAMigr ( sn sysname NOT NULL -- nom schema , tn sysname NOT NULL -- nom table , cn sysname NOT NULL -- nom colonne , typ sysname NOT NULL -- type , defIden sysname NOT NULL -- si elle a une définition identity , lc sysname NOT NULL -- nouvelle longueur comme dans définition de table , DefName sysname NOT NULL -- nom du défaut s'il existe , Def nvarchar(max) NOT NULL -- expression qui le représente s'il existe , nullSpec sysname NOT NULL -- signale si la colonne peut être mise à null , is_computed int not null -- signale si c'est une colonne calculée , computedColDef nvarchar(max) NULL -- expression de colonne calculée si c'est le cas , OrdCol int NOT NULL -- position relative croissante des colonnes, trous possibles dans séquence ) Create unique clustered index iTC on #ColsTablesAMigr (Sn, Tn, OrdCol) -- liste des index des tables -- if object_id('tempdb..#Indexes') is not null drop table #Indexes Create table #Indexes ( sn sysname NOT NULL , tn sysname NOT NULL , IdxName sysname NOT NULL , type_desc sysname NOT NULL , is_unique int NOT NULL , is_primary_key int NOT NULL , is_unique_constraint int NOT NULL , object_id Int NOT NULL , index_id Int NOT NULL , fgIdx sysname NOT NULL Default '' , IndexOnView int null ) Create unique clustered index iIX on #Indexes (Sn, Tn, IdxName) -- liste des colonnes des index des tables Declare @seqIxc Int -- if object_id('tempdb..#IndexesCols') is not null drop table #IndexesCols Create table #IndexesCols ( sn sysname NOT NULL , tn sysname NOT NULL , IdxName sysname NOT NULL , cn sysname NOT NULL , Seq Int Identity NOT NULL -- pour rendre la clé ci dessous unique, pas uilisée ailleurs , Ko int NOT NULL , ColIdxChar int not NULL -- pour signaler si type Char ou pas ) Create unique clustered index iIXC on #IndexesCols (sn, tn, IdxName, seq, ko) print '===========================================================================================' print '-- ['+@dbName+'] Data export ' print '===========================================================================================' -- optimiser l'insertion massive, plus tard remettre les options en place Set @sql= ' Alter database [_export] Set recovery Simple ' Set @sql = replace(@sql, '', @dbName) Exec yExecNLog.LogAndOrExec @context = 'yExport.ExportData' , @Info = 'Put Export Db is simple recovery' , @sql = @sql , @raiseError = @stopOnError -- --------------------------------------------------------------------------------------------------- -- generate stmt to rebuild stats -- --------------------------------------------------------------------------------------------------- Set @sql = ' use [] ;With ColWithStats as ( select Distinct Schema_name(OB.schema_id) as sn , Ob.name as Tn , c.name as Cn From sys.stats Ixs join sys.objects OB ON OB.object_id = Ixs.Object_id Join sys.stats_columns Ixc ON Ixc.object_id = Ixs.object_id And Ixc.stats_id = Ixs.stats_id join sys.columns C On c.object_id = Ixc.object_id And c.column_id = Ixc.column_id Where objectpropertyEx(ixs.object_id, "IsUserTable") = 1 And not exists(Select * from sys.indexes I where I.name = Ixs.name And I.IS_Disabled=0) And Schema_name (OB.schema_id) NOT IN ("sys") And objectpropertyEx(OB.object_id, "isMsShipped") = 0 -- on veut pas toucher aux objets -- systèmes ex: Dt% And not (ob.name = "sysdiagrams" and Schema_name(OB.schema_id)="dbo") ) Insert into #Stats (seq, sn, tn, cn) Select row_number() over (order by sn, tn, cn) as Seq , sn , Tn , Cn From ColWithStats ' set @sql = replace (@sql, '', @dbName) Set @sql = replace(@sql, '"', '''') Exec yExecNLog.LogAndOrExec @context = 'yExport.ExportData' , @Info = 'Recording info to rebuild original stats' , @sql = @sql , @raiseError = @stopOnError -- --------------------------------------------------------------------------------------------------- -- List existing schema to recreate them -- --------------------------------------------------------------------------------------------------- Set @sql = ' Use [] truncate table #Schemas Insert into #Schemas (sn) select name as Sn from (select distinct schema_id from sys.objects) as Ob join sys.schemas S on S.schema_id = Ob.schema_id where name not in ("dbo", "sys") ' set @sql = replace (@sql, '', @dbName) set @sql = replace (@sql, '', convert(sysname, Serverproperty('Collation'))) Set @sql = replace(@sql, '"', '''') Exec yExecNLog.LogAndOrExec @context = 'yExport.ExportData' , @Info = 'Recording info to rebuild original schema' , @sql = @sql , @raiseError = @stopOnError -- --------------------------------------------------------------------------------------------------- -- save definitions of foreign key, primary key, index clustered (non primaire), and index -- to recreate them after data load -- --------------------------------------------------------------------------------------------------- Set @sql = ' Use [] truncate table #RefConstraints Insert into #RefConstraints (Seq, Sn, Tn, Kn, USn, UTn, RefObjId, KeyId, UKn, MATCH_OPTION, UPDATE_RULE, DELETE_RULE, is_not_trusted) select row_number() Over (Order by TU.constraint_catalog, TU.constraint_schema, TU.Table_name) , TU.constraint_schema , TU.Table_name , TU.constraint_name , isnull(rc.UNIQUE_CONSTRAINT_SCHEMA,"") , isnull(object_name(FK.referenced_object_id), "") , FK.referenced_object_id , FK.Key_index_id , isnull(rc.UNIQUE_CONSTRAINT_NAME,"") , isnull(RC.MATCH_OPTION,"") , isnull(RC.UPDATE_RULE,"") , isnull(RC.DELETE_RULE,"") , fk.is_not_trusted from information_schema.constraint_table_usage TU Join sys.foreign_keys FK On FK.name = TU.Constraint_name join INFORMATION_SCHEMA.REFERENTIAL_CONSTRAINTS RC on rc.CONSTRAINT_NAME = TU.CONSTRAINT_NAME ' set @sql = replace (@sql, '', @dbName) set @sql = replace (@sql, '', convert(sysname, Serverproperty('Collation'))) Set @sql = replace(@sql, '"', '''') Exec yExecNLog.LogAndOrExec @context = 'yExport.ExportData' , @Info = 'Recording referential constrains info to rebuild them' , @sql = @sql , @raiseError = @stopOnError -- query that extract relations between foreign key and their columns matching with primary key and their columns Set @sql = ' truncate table #ColsRefConstraints Insert into #ColsRefConstraints (Sn, Tn, Kn, OrdCol, Cn, USn, UKn, UTn, UCn) Select K.Sn, K.Tn, K.Kn, cu.Ordinal_position, cu.Column_name , ISNULL (S.name, "") , ISNULL (Ix.name, "") , ISNULL (Ob.name, "") , ISNULL (c.name, "") From #RefConstraints K JOIN [].INFORMATION_SCHEMA.KEY_COLUMN_USAGE CU ON CU.CONSTRAINT_SCHEMA = K.Sn Collate And CU.CONSTRAINT_NAME = K.Kn Collate left JOIN -- index clé primaire ou index unique référencé (IX.name) [].sys.indexes IX on Ix.object_id = K.RefObjId And Ix.index_id = K.KeyId left JOIN -- table référencé (Ob.name) [].sys.objects Ob on ob.Object_Id = K.RefObjId left JOIN -- schema si index unique référencé (S.name) [].sys.schemas S on S.Schema_id = Ob.Schema_id LEFT JOIN -- id colonnes de la clé primaire ou de l"index unique référencé [].sys.index_columns Ixc ON Ixc.object_id = K.refObjId And Ixc.index_id = K.KeyId And Ixc.key_Ordinal = CU.ordinal_position LEFT JOIN -- colonnes de la clé primaire ou de l"index unique référencé (C.name) [].sys.columns c ON c.object_id = Ixc.object_id And c.column_id = Ixc.column_id ' set @sql = replace (@sql, '', convert(sysname, Serverproperty('Collation'))) set @sql = replace (@sql, '', @dbName) Set @sql = replace(@sql, '"', '''') Exec yExecNLog.LogAndOrExec @context = 'yExport.ExportData' , @Info = 'Recording referential constrains relations between columns ' , @sql = @sql , @raiseError = @stopOnError -- --------------------------------------------------------------------------------------------------- -- rebuild table one at the time -- tablelock hint on insert allows minimally logged operation in version above sql2005 -- when table is empty and still a heap -- --------------------------------------------------------------------------------------------------- -- make table list Set @sql = ' Use [] Truncate table #TablesToExport Insert into #TablesToExport(seq, sn, tn, Id, iden, fgLob, fgDat, fgDef) Select ROW_NUMBER() OVER (Order by S.name, T.name) , S.name as sn , T.name as tn , T.object_id as Id , Case When II.object_id is Not NULL Then 1 Else 0 End as Iden , Case -- on remplace DATA par PRIMARY car on laisse tomber le fichier DATA When T.lob_data_space_id >= 1 AND T.lob_data_space_id <= 2 Then "Primary" Else isnull(filegroup_name(T.lob_data_space_id), "") End As FgLob , Case When I.data_space_id <= 2 Then "Primary" Else isnull(filegroup_name(I.data_space_id), "") End As FgDat , (select top 1 name from sys.filegroups where is_default =1) as fgDef from sys.tables T join sys.indexes I on I.object_id = T.object_id And index_id in (0,1) join sys.Schemas S On S.schema_id = T.schema_id left join sys.identity_columns II On II.object_id = T.object_id Where objectpropertyEx(I.object_id, "isMsShipped") = 0 -- avoid system tables Dt% And not (T.name = "sysdiagrams" And S.name = "dbo") -- special case to handle ' set @sql = replace (@sql, '', convert(sysname, Serverproperty('Collation'))) Set @sql = replace(@sql, '', @dbName) Set @sql = replace(@sql, '"', '''') Exec yExecNLog.LogAndOrExec @context = 'yExport.ExportData' , @Info = 'Recording tables to migrate ' , @sql = @sql , @raiseError = @stopOnError --------------------------------------------------------------------------------------- -- collect useful column info that will allow to inactivate and reactivate -- défaults --------------------------------------------------------------------------------------- Set @sql = ' use [] truncate table #ColsTablesAMigr ;With TabList as ( /* (Select "dbo" as sn, name as tn, object_id as id, 0 as iden, * from [].sys.tables ) as T */ Select * From #TablesToExport as T ) , colNamesTypesCharLenPrecScale as ( Select object_id , sc.name collate Latin1_General_CI_AI as column_name , st.name collate Latin1_General_CI_AI as data_type , column_id , case when sc.max_length> 1 And type_name(sc.system_type_id) like "N%CHAR" then sc.max_length / 2 else sc.max_length End character_maximum_length -- longueur en caractères pas en byte , sc.precision as numeric_precision , sc.scale as numeric_scale From sys.columns sc left join sys.types ST -- ne plus utiliser type_name() en dehors du contexte de Bd On ST.user_type_id = Sc.user_type_id ) , CompleteBaseInfoOnTable as ( Select T.Sn As Sn , T.Tn As Tn , T.Iden As Iden , sc.name As Cn , Cn.data_type As Typ , Cn.column_id As OrdCol , Cn.character_maximum_Length as CharMaxLen , convert(nvarchar(30), Cn.character_maximum_Length) as StrCharMaxLen , Cn.numeric_precision , Cn.numeric_scale , isnull(d.definition,"") As def , Sc.is_nullable as is_nullable , Sc.is_computed as is_computed , Scc.definition as computedColDef , Coalesce ( Case When d.name is NULL Then "[DF_"+sn+"_"+tn+"_"+cn.column_name+"]" End , Case When M.name is NULL Then "[DF_Bind_"+sn+"_"+tn+"_"+cn.column_name+"]" End , "" ) As DefName , II.column_id as IdenColumn_id , II.Seed_Value as IdenSeed_Value , II.Increment_value as IdenIncrement_Value From TabList as T Join colNamesTypesCharLenPrecScale as Cn On Cn.object_id = T.Id Join sys.columns Sc On Sc.object_id = T.Id And Sc.name = Cn.column_name left join sys.computed_columns scc -- pour obtenir définition de la colonne calculée on sc.is_computed = 1 And -- optimiser avant joindre scc.object_id = Sc.object_id And scc.column_id = sc.column_id left join sys.objects m on m.object_id = Sc.default_object_id left Join -- un seule par table sys.identity_columns II On T.iden = 1 And -- join pas si pa siden sur table II.object_id = Sc.object_id And II.column_id = Sc.column_id left Join -- pas nécessairement de défaut sys.default_constraints d On d.parent_object_id = T.Id And d.parent_column_id = sc.column_id ) Insert into #ColsTablesAMigr (sn, tn, cn, typ, lc, defIden, DefName, def, nullSpec, is_computed, computedColDef, ordCol) Select Sn , Tn , Cn as cn , Typ as Typ , Case -- définition / susbtitution de la longueur When Typ like "%CHAR%" or Typ like "%BINARY%" Then Case When StrCharMaxLen = "-1" Then "(Max)" Else "(" + StrCharMaxLen +")" End When Typ IN ("Decimal", "Numeric") Then "("+ convert(nvarchar, numeric_precision)+"," + convert(nvarchar, numeric_scale)+ ")" Else "" -- pas de longueur spécifiable pour ce type End as lc , Case -- clause identity à mettre ? When IdenColumn_Id is NULL Then "" Else "Identity ("+convert(varchar(40), IdenSeed_value)+","+convert(varchar(40), IdenIncrement_value)+")" End as DefIden , DefName , Def , Case -- retiennent atribut nullable orginal When is_nullable = 0 Then "NOT NULL" Else "NULL" End as nullSpec , is_computed , computedColDef , OrdCol -- ordre de la colonne dans la table From CompleteBaseInfoOnTable ' set @sql = replace (@sql, '', convert(sysname, Serverproperty('Collation'))) Set @sql = replace(@sql, '', @dbName) Set @sql = replace(@sql, '"', '''') Exec yExecNLog.LogAndOrExec @context = 'yExport.ExportData' , @Info = 'Recording column info. of tables to migrate' , @sql = @sql , @raiseError = @stopOnError ------------------------------------------------------------- -- info to recreate user defined types ------------------------------------------------------------- Set @sql = ' use [] ;With moreConvientUserTypesNameInfo as ( Select Sh.name as Schema_name , U.name as Uname , S.name as Typ , U.max_length as charMaxLen , convert(varchar, U.max_length) as StrcharMaxLen , U.precision as numeric_precision , U.scale as numeric_scale , Case When U.is_Nullable = 0 Then "NOT NULL" Else "NULL" End as NullSPec From sys.types U join sys.schemas SH On SH.schema_id = U.Schema_id join sys.types S On S.user_type_id = U.system_type_id where U.is_user_defined = 1 ) Insert into #UserDefTypes ( seq -- no seq pour traitement seq , sn -- nom schema , un -- nom user type , typ -- type , lc -- longueur facultative , nullspec -- ajouter null ou not null ) Select Row_number() Over (Order by Schema_name, Uname) as Seq , Schema_name , Uname , Typ , Case -- définition / susbtitution de la longueur When Typ like "%char%" or Typ like "%binary%" Then Case When StrCharMaxLen = "-1" Then "(Max)" Else "("+StrCharMaxLen+")" End When Typ IN ("Decimal", "Numeric") Then "("+ convert(nvarchar, numeric_precision)+"," + convert(nvarchar, numeric_scale)+ ")" Else "" -- pas de longueur spécifiable pour ce type End as lc , NullSPec from moreConvientUserTypesNameInfo ' Set @sql = replace(@sql, '', @dbName) Set @sql = replace(@sql, '"', '''') Exec yExecNLog.LogAndOrExec @context = 'yExport.ExportData' , @Info = 'Recording data type info. of columns of tables to migrate' , @sql = @sql , @raiseError = @stopOnError -- interesting trace ---select * from #TablesToExport ---select * from #ColsTablesAMigr ------------------------------ -- keep tables index info ------------------------------ Set @sql = ' Use [] truncate table #Indexes insert into #Indexes select T.sn , T.tn , Ix.name as IdxName , Ix.type_desc , Ix.is_unique , Ix.is_primary_key , Ix.is_unique_constraint , Ix.Object_id , Ix.Index_id , Case When Ix.data_space_id <= 2 Then "Primary" Else isnull(filegroup_name(Ix.data_space_id), "") End As FgDat , convert(int, objectpropertyex(object_id, "isView")) as IndexonView From #TablesToExport T join sys.indexes Ix ON Ix.object_id = T.Id And Ix.is_hypothetical = 0 And Ix.type_desc NOT IN ("HEAP", "XML") Order By T.Sn, T.Tn, Ix.name ' set @sql = replace (@sql, '', convert(sysname, Serverproperty('Collation'))) Set @sql = replace(@sql, '', @dbName) Set @sql = replace(@sql, '"', '''') Exec yExecNLog.LogAndOrExec @context = 'yExport.ExportData' , @Info = 'Recording index info.' , @sql = @sql , @raiseError = @stopOnError Set @sql = ' Use [] truncate table #IndexesCols insert into #IndexesCols (sn, tn, IdxName, cn, ko, ColIdxChar) select Ix.Sn, Ix.Tn, Ix.IdxName, c.name as nomCol, ixc.key_ordinal, Case When st.name in ("char", "nchar", "varchar", "nvarchar", "sysname", "Text") Then 1 Else 0 End From #Indexes Ix join sys.index_columns Ixc ON Ixc.object_id = Ix.object_id And Ixc.index_id = Ix.index_id join sys.columns C On c.object_id = Ixc.object_id And c.column_id = Ixc.column_id join master.sys.types st On st.system_type_id = C.system_type_id Order by sn, tn, IdxName, ixc.key_ordinal ' set @sql = replace (@sql, '', convert(sysname, Serverproperty('Collation'))) Set @sql = replace(@sql, '', @dbName) Set @sql = replace(@sql, '"', '''') Exec yExecNLog.LogAndOrExec @context = 'yExport.ExportData' , @Info = 'Recording columns'' indexes info.' , @sql = @sql , @raiseError = @stopOnError Declare @iType sysname Declare @fk int Declare @PremCol int -- ----------------------------------------------------------------------------------------------------- -- create schema -- ----------------------------------------------------------------------------------------------------- Set @sn = '' While (1=1) Begin Select top 1 @sn = sn -- next schema From #Schemas Where sn > @Sn Order by sn If @@rowcount = 0 Break -- no more exit Set @sql = ' use [_export] exec("Create Schema [] authorization dbo") ' Set @sql = replace(@sql, '"', '''') Set @sql = replace(@sql, '', @dbName) Set @sql = replace(@sql, '', @sn) Exec yExecNLog.LogAndOrExec @context = 'yExport.ExportData' , @Info = 'Create schema' , @sql = @sql , @raiseError = @stopOnError End -- ----------------------------------------------------------------------------------------------------- -- rebuild user datatypes -- ----------------------------------------------------------------------------------------------------- Set @seq = 0 While (1=1) Begin Select top 1 @sn = sn , @nouvTyp = un , @typ = typ , @lc = ISNULL(lc, '') , @nullSpec = ISNULL(nullSpec, '') , @seq = seq From #UserDefTypes Where seq > @seq Order by seq If @@rowcount = 0 Break -- no more, exit Set @sql = ' use [_export] exec("Create Type [].[] From ") ' Set @sql = replace(@sql, '"', '''') Set @sql = replace(@sql, '', @dbName) Set @sql = replace(@sql, '', @sn) Set @sql = replace(@sql, '', @nouvTyp) Set @sql = replace(@sql, '', @typ) Set @sql = replace(@sql, '', @lc) Set @sql = replace(@sql, '', @nullSpec) Exec yExecNLog.LogAndOrExec @context = 'yExport.ExportData' , @Info = 'Create data types' , @sql = @sql , @raiseError = @stopOnError End create table #defFunc ( seq int primary key , sn sysname , ModuleName sysname , def nvarchar(max) , anull int , qIden int ) Set @sql = ' use [] Truncate table #defFunc Insert into #defFunc Select ROW_NUMBER() Over (order by object_name(object_id)) , schema_name(convert(int, objectpropertyex(object_id, "schemaId"))) as Sn , object_name(object_id) , definition , uses_ansi_nulls as aNull , uses_quoted_identifier as qIden From Sys.sql_modules Where OBJECTPROPERTYEX (object_id, "IsScalarFunction") = 1 ' Set @sql = replace(@sql, '"', '''') Set @sql = replace(@sql, '', @dbName) Exec yExecNLog.LogAndOrExec @jobNo = @JobNo , @context = 'yExport.ExportData' , @Info = 'Recreate scalar function and/or views that may be recreated' , @sql = @sql , @raiseError = @stopOnError While (1=1) -- at least one scalar function view or table is created Begin -- --------------------------------------------------------------------------------------------- -- Recreate scalar udf that can be used as default -- try a blind recreate ignoring what can't be created -- only do the attempt for schema bound objects -- --------------------------------------------------------------------------------------------- Set @Created = 0 -- flag as if nothing was created for the loop below Set @seq = 0 Set @FunctionCreated = 0 -- to know if this pass has created at least a function While(1=1) Begin Select top 1 @seq = seq , @sn = sn , @mn = ModuleName , @def = def , @ANull = Anull , @qIden = qIden from #defFunc Where seq > @seq Order by seq If @@rowcount = 0 -- end of table If @Created = 1 -- at least one function could be created Begin Set @seq = 0 -- retry another pass in case some other fonction depends on one just created set @Created = 0 -- just to know if the next pass has created nothing continue End Else Break set @sql = ' Use [_export] set ansi_nulls ; Set quoted_identifier ; If object_id("[].[]") is null Begin begin try Execute sp_executeSql @def end try begin catch Print error_number() Print error_message() end catch End -- not all errors are caught If object_id("[].[]") is not null Set @created = 1 Else Set @created = 0 ' Print 'try create ['+@sn+'].['+@mn+']' print '------------------------------------------' Set @sql = replace(@sql, '"', '''') Set @sql = replace(@sql, '', @dbName) Set @sql = replace(@sql, '', @sn) Set @sql = replace(@sql, '', @mn) Set @Sql = replace(@Sql, '', case When @aNull = 1 Then 'On' Else 'Off' End) Set @Sql = replace(@Sql, '', case When @qIden = 1 Then 'On' Else 'Off' End) Exec sp_executeSql @Sql, N'@Def nvarchar(max), @created int output', @def, @created output If @created = 1 -- certains cas d'erreur ne sont pas capturés Begin Set @FunctionCreated = @FunctionCreated + 1 Print 'object '+ @sn+'.'+@mn+ ' created' print '------------------------------------------' Delete From #defFunc Where seq = @seq -- remove already created function End Else begin print @sql Exec yExecNLog.PrintSqlCode @sql = @def, @numberingRequired = 1 Print 'object '+ @sn+'.'+@mn+ ' not created' print '------------------------------------------' end End -- --------------------------------------------------------------------------------------------- -- Export data one table at the time -- --------------------------------------------------------------------------------------------- -- Select * from #TablesToExport Set @TableCreated = 0 -- to know if this pass has created at least a table Set @seqT = 0 While (1=1) -- process all tables Begin Select top 1 @sn = sn , @tn = tn , @seqT = seq , @iden = Iden , @fgLob = fgLob , @fgDat = fgDat , @fgDef = fgDef From #TablesToExport Where seq > @SeqT Order by seq If @@rowcount = 0 Break Set @Info = ' --Export to [_export].[].[] à partir de [].[].[] ' Set @sql = replace(@sql, '"', '''') Set @sql = replace(@sql, '', @dbName) Set @sql = replace(@sql, '', @sn) Set @sql = replace(@sql, '', @tn) Exec yExecNLog.LogAndOrExec @jobNo = @JobNo , @context = 'yExport.ExportData' , @Info = @Info , @raiseError = @stopOnError ----------------------------------------------------- -- build column list ----------------------------------------------------- Set @ColsSelect = '' Set @colsCreateTable = '' Set @colsInsertInto = '' Set @seqc = 0 While (1=1) Begin Select Top 1 @cn = cn , @typ = typ , @lc = lc , @defIden = defIden , @DefName = DefName , @def = Def , @nullSpec = nullSpec , @is_computed = is_computed , @computedColDef = computedColDef , @seqc = OrdCol From #ColsTablesAMigr Where sn = @Sn And tn = @Tn and ordCol > @seqc Order by ordCol If @@rowcount = 0 Break -- select list, remove data that can be copied If (@typ <> 'timestamp' And @is_computed = 0) Begin Set @ColsSelect = @ColsSelect + case when @ColsSelect = '' Then ' []\n' Else ' ,[]\n' End End Set @ColsSelect = Replace(@ColsSelect, '', @cn) Set @ColsSelect = Replace(@ColsSelect, '', @lc) -- liste de colonnes pour into du Insert, dans mode insert, on évite les colonnes timestamp, et les calculées If (@typ <> 'timestamp' And @is_computed <> 1) Begin Set @colsInsertInto = @colsInsertInto + case when @colsInsertInto = '' Then ' ' Else ' ,' End + '['+@cn +']\n' End -- liste de colonnes du create table -- Les champs n'ont pas tous un défaut, mais ils doivent tous avoir une spec NULL ou Not NULL Set @colsCreateTable = @colsCreateTable + case when @colsCreateTable = '' Then ' ' Else ' ,' End + '[] [] \n' -- si défaut pas spécifié, ôte repère de marqueur de la définition du défaut -- sinon ajouter la définition syntaxique If @Def = '' Set @colsCreateTable = Replace(@colsCreateTable, '', '') Else Set @colsCreateTable = Replace( @colsCreateTable , '' , 'CONSTRAINT [DF__] Default ') -- remplace tous les marqueurs Set @colsCreateTable = Replace(@colsCreateTable, '', @tn) Set @colsCreateTable = Replace(@colsCreateTable, '', @cn) If @is_computed <> 1 Begin Set @colsCreateTable = Replace(@colsCreateTable, '', @Typ) Set @colsCreateTable = Replace(@colsCreateTable, '', @lc) Set @colsCreateTable = Replace(@colsCreateTable, '', isnull(@defIden,'')) Set @colsCreateTable = Replace(@colsCreateTable, '', isnull(@def,'')) Set @colsCreateTable = Replace(@colsCreateTable, '', @nullSpec) End Else Begin Set @colsCreateTable = Replace(@colsCreateTable, '[]', 'as '+@computedColDef) Set @colsCreateTable = Replace(@colsCreateTable, '', '') Set @colsCreateTable = Replace(@colsCreateTable, '', '') Set @colsCreateTable = Replace(@colsCreateTable, '', '') Set @colsCreateTable = Replace(@colsCreateTable, '', '') End End -- While une colonne de la table ------------------------------------------------------------------------------------- -- Executer le create de la table ------------------------------------------------------------------------------------- Set @Sql = ' Use [_export] create Table [].[] ( ) ON [] TEXTIMAGE_ON [] ' Set @sql = replace(@sql, '', @dbName) Set @sql = replace(@sql, '', @sn) Set @sql = replace(@sql, '', @tn) Set @sql = replace(@sql, '', @colsCreateTable) If @FgDat = '' Set @sql = replace(@sql, 'ON [] ', '') Else Set @sql = replace(@sql, '', @FgDat) If @FgLob = '' Or (@fgLob = @fgDef) Set @sql = replace(@sql, 'TEXTIMAGE_ON []', '') Else Set @sql = replace(@sql, '', @FgLob) Set @sql = replace(@sql, '"', '''') Set @sql = Replace(@sql, '\n', nchar(10)) -- il y en a dans Begin Try Exec yExecNLog.LogAndOrExec @jobNo = @JobNo , @context = 'yExport.ExportData' , @Info = 'Table creation' , @sql = @Sql , @raiseError = 1 -- must catch the error Set @TableCreated = @TableCreated + 1 Delete From #TablesToExport Where seq = @SeqT End try Begin catch print error_message() Continue -- Jump to next table creation End catch -- decide if data is going to be pipelined through a view Set @sql = ' Use If exists ( select * from sys.views where name = "" And Schema_name(schema_id) = "yExport_" ) Set @schemaAlt = 1 Else Set @schemaAlt = 0 ' Set @sql = replace(@sql, '', @dbName) Set @sql = replace(@sql, '', @sn) Set @sql = replace(@sql, '', @tn) Set @sql = replace(@sql, '"', '''') Exec sp_executeSql @sql, N'@schemaAlt int output', @schemaAlt = @schemaAlt output ------------------------------------------------------------------------------------- -- Executer le insert / select ------------------------------------------------------------------------------------- Set @sql = ' declare @d nvarchar(25); set @d = convert(nvarchar(25), getdate(), 121) raiserror ("Start export to [_export].[].[] at %s",10,1, @d) with nowait -- force output no error ' Set @sql = replace(@sql, '', @dbName) Set @sql = replace(@sql, '', @sn) Set @sql = replace(@sql, '', @tn) Set @sql = replace(@sql, '"', '''') Exec sp_executeSql @sql -- progress report only Set @sql = -- prépare un insert /select complet ' Declare @nb Int ------------------------------------------------------------ Set identity_insert [_export].[].[] on Insert into [_export].[].[] with (tablock) ( ) Select from [].[].[] Set @nb = @@rowcount Set identity_insert [_export].[].[] off checkpoint -- Empty the log in simple recovery ------------------------------------------------------------ declare @d nvarchar(25); set @d = convert(nvarchar(25), getdate(), 121) declare @s nvarchar(25); set @s = convert(nvarchar(25), @nb) raiserror ("End export at %s. %s rows exported to [_export].[].[] ",10,1, @d, @s) with nowait -- force output no error ' -- pipeline data through a pre-defined view in a predefined schema If @schemaAlt = 1 Set @sql = REPLACE(@sql, '[].[].[]', '[].[yExport_].[]') If @iden = 0 -- si pas de définition de colonne identity enlève, mise là juste en mode Insert Select Begin -- ôte instructions relatives à la gestion de l'insertion de la clause identity Set @sql = replace(@sql, 'Set identity_insert [_export].[].[] on', '') Set @sql = replace(@sql, 'Set identity_insert [_export].[].[] off', '') End Set @sql = replace(@sql, '', @ColsInsertInto) Set @sql = replace(@sql, '', @ColsSelect) Set @sql = replace(@sql, '', @dbName) Set @sql = replace(@sql, '', @sn) Set @sql = replace(@sql, '', @tn) Set @sql = replace(@sql, '"', '''') Set @sql = Replace(@sql, '\n', nchar(10)) -- il y en a dans Exec yExecNLog.LogAndOrExec @jobNo = @JobNo , @context = 'yExport.ExportData' , @Info = 'Table load by insert/select' , @sql = @Sql , @raiseError = @stopOnError Set @typDesc = 'Clustered' -- traite en ordre clustered, puis après non clustered Set @in = '' Set @ColIdxChar = 0 While (1=1) -- un index de la table à recréer Begin Select Top 1 @in = IdxName, @iUniq = is_unique, @pk = Is_primary_key, @IUniqC = is_unique_constraint, @fgIdx=FgIdx, @IndexOnView = IndexOnView From #Indexes Where sn = @Sn And tn = @Tn and type_desc = @typDesc and IdxName > @in And IndexOnView = 0 Order by sn, tn, IdxName If @@rowcount = 0 Begin If @typDesc = 'Clustered' Begin Set @typDesc = 'NonClustered' -- traite ensuite nonclustered Set @in = '' -- recommence à parcourir les index en ordre continue End Else Break End -- détermine si l'index a des colonnes de type charactère pour éviter -- de vérifier l'unicité de l'index à cause de la cédille si elle n'a -- pas de champ texte Select top 1 @ColIdxChar = ColIdxChar from #IndexesCols Where sn = @Sn And tn = @Tn And IdxName = @In And ColIdxChar = 1 If @@rowcount = 0 Set @ColIdxChar = 0 -- fabriquer liste de colonnes de l'index Set @Cols = '' Set @LigCols = '' Set @seqc = 0 While (1=1) -- une colonne de l'index à attribuer Begin Select Top 1 @cn = cn, @seqc = Ko From #IndexesCols Where sn = @Sn And tn = @Tn And IdxName = @In and Ko > @seqc Order by ko If @@rowcount = 0 Break Set @Cols = @Cols + case when @Cols = '' Then '' Else ' ,' End + '[' + @cn + ']' Set @ligCols = @ligCols + case When @ligCols = '' Then '' Else ' ,' End+'[' + @cn + ']' End -- While une colonne Set @sql = ' Use [_export] ' + Case When @Pk = 1 Then ' Alter table [].[] add constraint [] primary key () With (FILLFACTOR = 90) ON [] ' When @iUniqC = 1 Then ' Alter table [].[] add constraint [] unique () With (FILLFACTOR = 90) ON [] ' Else ' Create Index [] On [].[] () With (FILLFACTOR = 90) ON [] ' End -- case Set @sql = replace(@sql, '', @dbName) Set @sql = replace(@sql, '', @sn) Set @sql = replace (@sql, '', @tn) Set @sql = replace (@sql, '', @In) Set @sql = replace (@sql, '', @TypDesc) Set @sql = replace (@sql, '', case When @iUniq = 1 Then 'Unique' Else '' End) Set @sql = replace (@sql, '', @Cols) Set @sql = replace (@sql, '', @LigCols) Set @sql = replace (@sql, '', Str(@pk,1)) Set @sql = replace (@sql, '', Str(@iUniqC,1)) If @FgIdx = '' Set @sql = replace(@sql, 'ON [] ', '') Else Set @sql = replace(@sql, '', @FgIdx) Set @sql = replace (@sql, '"', '''') Set @sql = @sql + NCHAR(10)+ 'checkpoint' Exec yExecNLog.LogAndOrExec @jobNo = @JobNo , @context = 'yExport.ExportData' , @Info = 'Recreate table indexes' , @sql = @Sql , @raiseError = @stopOnError End -- While un index End -- While Une table à traiter If exists(Select * from #TablesToExport) -- no table or scalar function could be created this -- must stop because this is not due to a cross dependencies problem between both Begin Select sn, tn from #TablesToExport raiserror ('Some tables/index could be exported',11,1) End Else Break -- no more tables End -- Creation d'une table et/ou au moins une fonction scalaire fonctionne ---------------------------------------------------------------------------------- -- recreate auto-generated stats for all tables ---------------------------------------------------------------------------------- Set @seq = 0 While (1=1) -- un index de la table à recréer Begin Select Top 1 @seq = seq, @sn = sn, @tn = tn, @cn = cn From #Stats Where seq > @seq Order by seq If @@ROWCOUNT = 0 break Set @sql = ' Use [_export] declare @c int Select @c = count(distinct []) From (Select [].[].[] From [].[] tablesample(10 percent)) as x ' Set @sql = Replace(@sql, '', @dbName) Set @sql = Replace(@sql, '', @Sn) Set @sql = Replace(@sql, '', @tn) Set @sql = Replace(@sql, '', @cn) Set @sql = Replace(@sql, '"', '''') Exec yExecNLog.LogAndOrExec @jobNo = @JobNo , @context = 'yExport.ExportData' , @Info = 'Optimizer stats recreate' , @sql = @Sql , @raiseError = @stopOnError End -- While ---------------------------------------------------------------------------------- -- rebatir les clés étrangères des tables s'il y a lieu ---------------------------------------------------------------------------------- Set @seqFk = 0 While (1=1) Begin Select Top 1 @Ks = Sn, @tn = Tn, @Kn = Kn, @seqFk = Seq, @FKOnClause = Case When UPDATE_RULE <> 'NO ACTION' Then ' ON UPDATE '+UPDATE_RULE ELSE '' END + Case When DELETE_RULE <> 'NO ACTION' Then ' ON DELETE '+DELETE_RULE ELSE '' END , @is_not_trusted = is_not_trusted From #RefConstraints Where seq > @seqFk order by seq If @@rowcount = 0 Break -- fabriquer liste de colonnes de la reference aux colonnes Set @Cols = '' Set @ucols = '' Set @seqc = 0 While (1=1) -- une colonne pour la reference de cle etrangere Begin Select Top 1 @cn = cn, @seqc = OrdCol, @Usn = Usn, @Utn = Utn, @Ucn = Ucn From #ColsRefConstraints Where sn = @Ks And tn = @Tn and @Kn=Kn and OrdCol > @seqC Order by sn, tn, ordCol If @@rowcount = 0 Break Set @Cols = @Cols + case when @Cols = '' Then ' ' Else ' , ' End + '['+ @cn + ']' Set @uCols = @uCols + case when @uCols = '' Then ' ' Else ' , ' End + '['+ @uCn + ']' End -- While une colonne Set @sql = ' Use [_export] Alter table [].[] add constraint [] FOREIGN KEY () REFERENCES [].[] () ; Alter table [].[] NoCheck Constraint []; Checkpoint ' If @is_not_trusted = 0 Begin Set @sql = replace (@sql, '', '') Set @sql = replace (@sql, 'Alter table [].[] NOCHECK Constraint [];', '') End Else Set @sql = replace (@sql, '', 'WITH NOCHECK') Set @sql = replace(@sql, '', @dbName) Set @sql = replace (@sql, '', @ks) Set @sql = replace (@sql, '', @tn) Set @sql = replace (@sql, '', @Kn) Set @sql = replace (@sql, '', @Cols) Set @sql = replace (@sql, '', @uSn) Set @sql = replace (@sql, '', @utn) Set @sql = replace (@sql, '', @uCols) Set @sql = replace (@sql, '', @FkOnClause) Exec yExecNLog.LogAndOrExec @jobNo = @JobNo , @context = 'yExport.ExportData' , @Info = 'Recreate referential constraints' , @sql = @Sql , @raiseError = @stopOnError End -- While une clé étrangère à traiter raiserror('Shrint the log',10,1) declare @logN sysname Set @sql = ' use [_Export] Select @logn = name from sys.database_files where type_desc = "LOGN" ' Set @sql = replace(@sql, '', @dbName) Set @sql = replace(@sql, '"', '''') Exec sp_executeSql @Sql, N'@logN sysname output', @logn = @logn output Set @sql = ' use [_Export] dbcc shrinkfile("") with NO_INFOMSGS ' Set @sql = replace(@sql, '', @dbName) Set @sql = replace(@sql, '"', '''') Set @sql = replace(@sql, '', @logn) Exec yExecNLog.LogAndOrExec @jobNo = @JobNo , @context = 'yExport.ExportData' , @Info = 'Reset log file' , @sql = @Sql , @raiseError = @stopOnError End try begin catch set @Info = ERROR_MESSAGE() + ' (ExportData)' raiserror(@Info, 11, 1) Exec yExecNLog.LogAndOrExec @jobNo = @JobNo , @context = 'yExport.ExportData' , @err = 'Failure to complete ExportData' end catch End -- Export.ExportData GO Create Or Alter Proc yExport.ExportCode @dbName sysname , @stopOnError Int = 1 , @jobNo Int as Begin Declare @dbNameExport sysname =@DbName+'_Export' Exec S#.CopyAllSqlModules @DbName, @DbNameExport End -- yExport.ExportCode GO -------------------------------------------------------------------------------------------------------- -- Procedure maitresse qui migre les code utilisateurs et les droits -------------------------------------------------------------------------------------------------------- Create Or Alter Procedure yExport.ExportSecur @dbName sysname , @stopOnError Int = 1 , @jobNo Int as Begin Set nocount on declare @etp nvarchar(4) Set @etp = 'Secu' -- simplifie appel de la proc de logExec et logErr declare @sql nvarchar(max) , @suffixe sysname -- var to recreate ysers , @Usr sysname , @Uid smallInt , @logN sysname , @default_schema sysname , @owning_principal_id int -- var to recreate perms to users and roles , @Seq int , @action nvarchar(50) , @perms nvarchar(256) , @TypObj Char(1) , @obj sysname , @col sysname , @SomeUsers nvarchar(max) -- var to create roles and roles members , @r sysname , @rM sysname , @seqM Int , @Info nvarchar(max) Begin try -- get actual users and corresponding login name Create Table #princ -- users list ( Usr sysname collate database_default Not NULL , uid int , logN sysname collate database_default NULL , default_schema sysname collate database_default null , owning_principal_id int null ) Set @sql = ' Use [] insert into #princ Select p.name collate LATIN1_GENERAL_CI_AI as UserName , p.principal_id , SUSER_SNAME(sid) , p.default_schema_name , p.owning_principal_id From [].sys.database_principals P Where p.type_desc = "SQL_USER" And p.default_schema_name is not null And not exists(Select * from [_export].sys.database_principals as E Where E.name = P.Name collate Latin1_general_ci_ai) ' Set @sql = replace(@sql,'', @dbName) Set @sql = replace(@sql,'"', '''') Exec yExecNLog.LogAndOrExec @jobNo = @JobNo , @context = 'yExport.ExportSecur' , @Info = 'Get users names' , @sql = @Sql , @raiseError = @stopOnError --------------------------------------------------------------------------------------------------------- -- enum database roles and their members --------------------------------------------------------------------------------------------------------- Create Table #Roles ( RoleName Sysname primary key clustered , Principal_id Int ) Create Table #RoleMembers ( Seq int primary key clustered , RoleName Sysname , RoleMember sysname ) Set @sql= ' Use [] Insert into #roles select PSrc.Name as RoleName, PSrc.principal_Id from sys.database_principals PSrc where type_desc = "database_role" And not exists(Select * from [_export].sys.database_principals as E Where E.name = PSrc.Name collate Latin1_general_ci_ai) Insert into #RoleMembers Select ROW_NUMBER() OVER(Order By R.RoleName, M.Name) as Seq , R.RoleName collate Latin1_general_ci_ai as RoleName , M.Name collate Latin1_general_ci_ai as Name From #roles R JOIN sys.database_role_members RM ON RM.Role_Principal_id = R.Principal_id JOIN sys.database_principals M ON M.Principal_id = RM.Member_Principal_id And M.type_desc <> "application_role" ' Set @sql = replace (@sql, '', @dbName) Set @sql = replace (@sql, '"', '''') Exec yExecNLog.LogAndOrExec @jobNo = @JobNo , @context = 'yExport.ExportSecur' , @Info = 'Keep roleMembership' , @sql = @Sql , @raiseError = @stopOnError ---------------------------------------------------------------------------------------------------------- -- Make a compressed list on rights to make less GRANT/DENY instructions ---------------------------------------------------------------------------------------------------------- Create Table #RightsToApply ( Seq int primary key clustered , action nvarchar(50) NOT NULL , perms nvarchar(256) NOT NULL , TypObj Char(1) , obj sysname NULL , col sysname NULL , SomeUsers nvarchar(max) NOT NULL ) Create Table #Privs ( Seq int primary key clustered , action nvarchar(50) NOT NULL , perm nvarchar(256) NOT NULL , TypObj Char(1) , obj sysname NULL , col sysname NULL , ToWho nvarchar(max) NOT NULL ) Set @sql= ' Use [] ;With ObjectIds (ObjId) as ( Select object_Id(name) as ObjId From sys.tables union all Select object_Id -- exclure les vues retournées aussi par information_schema.table From sys.sql_modules Where objectpropertyEx(object_id, "IsView") = 0 ) , AllPrivs as ( select -- lire les droits qui ne sont pas spécifique à la colonne state_desc collate LATIN1_GENERAL_CI_AI as Action , permission_name collate LATIN1_GENERAL_CI_AI as Perm , Case When objectpropertyex(major_Id, "isUserTable") =1 Or objectpropertyex(major_Id, "isView") =1 Then "Q" Else "M" End collate LATIN1_GENERAL_CI_AI as TypObj , object_name(major_Id) collate LATIN1_GENERAL_CI_AI as Obj , NULL as Col -- pas une colonne , user_name(grantee_principal_id) collate LATIN1_GENERAL_CI_AI as toWho from ObjectIds as Objs Join sys.database_permissions P ON P.major_id = Objs.ObjId Where minor_id = 0 And -- zéro if no column specific privileges class_desc = "OBJECT_OR_COLUMN" UNION ALL select -- add specific columns rights state_desc , permission_name , Case When objectpropertyex(major_Id, "isUserTable") =1 Or objectpropertyex(major_Id, "isView") =1 Then "Q" Else "M" End , object_name(major_Id) , COL_NAME (Object_id, Column_id) , user_name(grantee_principal_id) from sys.columns TC Join sys.database_permissions P ON P.major_id = Tc.Object_id And P.minor_id = Tc.Column_id ) Insert into #Privs (Seq, action, perm, TypObj, obj, col, toWho) Select Row_number() over (order by Obj) , Action , perm , typObj , obj , col , toWho From AllPrivs ' Set @sql = replace(@sql,'', @dbName) Set @sql = replace(@sql,'"', '''') Exec yExecNLog.LogAndOrExec @jobNo = @JobNo , @context = 'yExport.ExportSecur' , @Info = 'Keep user/role permissions' , @sql = @Sql , @raiseError = @stopOnError Set @Uid = 0 -- to get to #1 which is dbo and which must be processed first While (1=1) Begin Select -- read next user Top 1 @Usr = Usr , @logN = LogN , @Uid = Uid , @default_schema = default_schema , @owning_principal_id = owning_principal_id From #princ Where Uid > @Uid Order by Uid If @@rowcount = 0 Break -- no more to read If @usr = 'dbo' -- ordre de traitement des users fait que celui-ci est traité en premier Begin Set @sql = ' use [_export]; ALTER AUTHORIZATION ON Database::[_export] To []; ' Set @sql = replace(@sql,'', @dbName) Set @sql = replace(@sql,'', ISNULL(@LogN, 'YourSQLDba')) Set @sql = replace(@sql,'"', '''') Exec yExecNLog.LogAndOrExec @jobNo = @JobNo , @context = 'yExport.ExportSecur' , @Info = 'Set Db Owner' , @sql = @Sql , @raiseError = @stopOnError Continue End -- if here user is not dbo Set @sql = '' If @Usr <> 'Guest' Begin -- user not aliased to dbo If @logN <> '' Set @sql = ' use [_export]; Create user [] For Login [] with default_schema = ' Else Set @sql = ' -- user is orphaned or aliased to dbo use [_export]; Create user [] Without Login -- user is aliased to dbo recreate it without login ' End else Begin If exists(Select * from Sys.database_permissions where grantee_principal_id = user_id('guest') and type = 'co' and state = 'G') Begin Set @sql = ' use [_export]; Grant connect to guest ' End End Set @sql = replace(@sql,'', isnull(@dbName, '')) Set @sql = replace(@sql,'', isnull(@Usr, '')) Set @sql = replace(@sql,'', isnull(@logN, '')) If @default_schema is null Set @sql = replace (@sql, 'with default_schema = ', '') Else Set @sql = replace (@sql, '', @default_schema) Set @sql = replace(@sql,'"', '''') if @sql <> '' Exec yExecNLog.LogAndOrExec @jobNo = @JobNo , @context = 'yExport.ExportSecur' , @Info = 'create user ' , @sql = @Sql , @raiseError = @stopOnError End -- While user to create Set @r = '' -- read first role While (1=1) Begin Select top 1 @r = RoleName From #Roles Where RoleName > @r Order by RoleName If @@rowcount = 0 Break -- si plus rien quitter Set @sql = ' use [_export]; exec ("Create ROLE AUTHORIZATION dbo") ' Set @sql = replace (@sql, '', @r) Set @sql = replace (@sql, '', @dbName) Set @sql = replace (@sql, '"', '''') Exec yExecNLog.LogAndOrExec @jobNo = @JobNo , @context = 'yExport.ExportSecur' , @Info = 'create role' , @sql = @Sql , @raiseError = @stopOnError End -- while Set @SeqM = 0 -- amorce lecture des membres de roles While (1=1) Begin Select top 1 @r = RoleName, @rM = RoleMember, @SeqM = Seq From #RoleMembers Where Seq > @SeqM Order by Seq If @@rowcount = 0 Break Set @sql = ' use [_export]; If "" <> "dbo" Exec sp_addrolemember "", "" ' Set @sql = replace (@sql, '', @dbName) Set @sql = replace (@sql, '', @r) Set @sql = replace (@sql, '', @rM) Set @sql = replace(@sql,'"', '''') Exec yExecNLog.LogAndOrExec @jobNo = @JobNo , @context = 'yExport.ExportSecur' , @Info = 'Add role member' , @sql = @Sql , @raiseError = @stopOnError End -- While il y a un membre de role à ajouter à un role -- ***************************************************************** --declare @sql nvarchar(max) --declare @dbName sysname --set @dbName=db_name() --drop table #RightsToApply ---------------------------------------------------------------------------------------------------------- -- Réattribuer les droits, lister, compresser en moins d'instructions GRANT/DENY, ré-exécuter GRANT / DENY -- Droits lus de la Bd à migrer -- C'est un ensemble de requêtes avec tables temporaires assez compliqué -- On ne peut pas couper la batch en moins car les tables #temporaire sont créées par select into -- et cesseraient d'exister après ---------------------------------------------------------------------------------------------------------- Set @sql= ' use [] ;With TabAndProcPriv as ( Select Action , Convert ( nvarchar(128) , Stuff -- remove starting comma from the list ( -- put together perms by column, user Max(Case When Perm = "Select " Then ", Select " Else "" End) + Max(Case When Perm = "Insert " Then ", Insert " Else "" End) + Max(Case When Perm = "Update " Then ", Update " Else "" End) + Max(Case When Perm = "Delete " Then ", Delete " Else "" End) + Max(Case When Perm = "Execute " Then ", Execute " Else "" End) + Max(Case When Perm = "References " Then ", References " Else "" End) , 1 , 1 , "" ) ) as Perms , TypObj , Obj , Col , toWho From #Privs group by Action, TypObj, Obj, Col, toWho ) Select Action , Perms , TypObj , Obj , Col , toWho -- give unique sequence number group to every 15 users , ROW_NUMBER() OVER(partition By Action, perms, typObj, Obj, Col Order by toWho) / 15 AS "PermGroup" into #Pa From TabAndProcPriv create clustered index iPa on #Pa (action, perms, obj, permGroup) -- compact rights statement by keeping the same set of rights on one or more users -- use premGroup generated previously to do that Truncate table #RightsToApply Insert into #RightsToApply ( Seq , action , perms , TypObj , obj , col , SomeUsers ) Select -- order rights so that grants are performed before deny so action column order is set descending ROW_NUMBER() OVER(Order by Action Desc, perms, Obj, Col) , Action , Perms , TypObj , Obj , col -- put together users that receive the same set of rights on the same objects , stuff ( ( -- nom colonne interprétée par XPATH, -- ici text() spécifie que c"est le texte de l"élément et pas son nom ex: nomElem -- truc SQL2005 pour fusionner data de plusieurs lignes select ", ["+cast(D.toWho as varchar(max))+"]" as [text()] from #Pa D Where D.Action = Pa.Action And D.perms = Pa.Perms And D.Obj = Pa.Obj And ISNULL(D.col, "") = ISNULL(Pa.Col,"") And D.PermGroup = Pa.PermGroup ORDER By D.toWho FOR XML PATH("") ) , 1 , 2 , "" ) as SomeUsers From ( Select Distinct Action, Perms, TypObj, Obj, Col, PermGroup From #Pa ) as PA ' Set @sql = replace(@sql,'', @dbName) Set @sql = replace(@sql,'"', '''') Exec yExecNLog.LogAndOrExec @jobNo = @JobNo , @context = 'yExport.ExportSecur' , @Info = 'Get rights info' , @sql = @Sql , @raiseError = @stopOnError -- Select * from #RightsToApply Set @seq = 0 -- amorce pour lecture des attribution de droits While (1=1) Begin Select TOP 1 @seq = seq , @action = action , @perms = perms , @typObj = typObj , @obj = obj , @col = col , @SomeUsers = SomeUsers From #RightsToApply Where seq > @seq Order By seq if @@rowcount = 0 BreaK -- applique sur BD de destination les droits Set @sql = ' Use [_export]; If object_Id("[]") IS NOT NULL ON OBJECT::[] ([]) To ' Set @Sql = replace(@sql, '', case When @action <> 'GRANT_WITH_GRANT_OPTION' Then @action Else 'GRANT' End ) Set @Sql = replace(@sql, '', case When @action <> 'GRANT_WITH_GRANT_OPTION' Then '' -- efface tag option Else 'WITH GRANT OPTION' -- sinon la met End ) Set @Sql = replace(@sql, '"', '''') Set @Sql = replace(@sql, '', @perms) Set @Sql = replace(@sql, '', @obj) Set @Sql = replace(@sql, '', @SomeUsers) If @col is NULL Set @Sql = replace(@sql, '([])', '') Else Set @Sql = replace(@sql, '', @col) Set @sql = replace(@sql,'', @dbName) Exec yExecNLog.LogAndOrExec @jobNo = @JobNo , @context = 'yExport.ExportSecur' , @Info = 'Apply privileges' , @sql = @Sql , @raiseError = @stopOnError End -- tant que des droits à appliquer End Try begin catch set @Info = ERROR_MESSAGE() + ' (ExportSecur)' raiserror(@Info, 11, 1) end catch End -- yExport.ExportSecur GO if objectpropertyEx(object_id('Export.ExportDb'), 'isProcedure') = 1 Drop proc Export.ExportDb GO Create Or Alter Proc Export.ExportDb @dbName sysname , @collation sysname = NULL , @stopOnError Int = 1 , @jobName sysname as Begin Set nocount on declare @rc int declare @Info nvarchar(max) declare @jobNo Int Select 'Look at messages TAB, to see work progress messages' as ReadThisPlease Declare @sql nvarchar(max) Set @jobName = 'Export data from '+@dbName+' to '+@dbName+'_Export' Print '====================================================' Print @jobName Print '====================================================' Begin try --Exec yExecNLog.AddJobEntry -- @jobName = @jobName --, @jobNo = @jobNo output Exec @rc = yExport.CreateExportDatabase @dbname, @collation, @stopOnError, @jobNo If @rc = 0 Begin Exec yExport.ExportData @dbName, @stopOnError, @jobNo Exec yExport.ExportCode @dbName, @stopOnError, @jobNo Exec yExport.ExportSecur @dbName, @stopOnError, @jobNo End End try Begin Catch set @Info = error_message() raiserror (@Info ,11,1) End Catch end -- Export.ExportDb GO Create Or Alter Procedure yMirroring.DatabaseRecovery @DbName sysname As Begin Declare @sql nvarchar(max) Set @sql = 'RESTORE DATABASE [] WITH RECOVERY, REPLACE' Set @sql = REPLACE(@sql, '', @DbName) print @sql Exec(@sql) End -- yMirroring.DatabaseRecovery GO Create Or Alter Procedure Mirroring.DoRecovery @IncDb nVARCHAR(max) = '' , @ExcDb nVARCHAR(max) = '' As Begin Declare @sql nvarchar(max) Declare @dbname sysname Declare @err nvarchar(max) Set @err = '' Set NOCOUNT ON -- The function udf_YourSQLDba_ApplyFilterDb apply filter parameters on this list Create table #Db ( DbName sysname primary key clustered , ErrorMsg nvarchar(max) default 'This is the database state before before putting it ''ONLINE''.' ) Insert into #Db (DbName) Select X.DbName from yUtl.YourSQLDba_ApplyFilterDb (@IncDb, @ExcDb) X Left Join master.sys.databases D On X.DbName = D.name Collate Latin1_general_ci_ai -- Exclude non-RESTORING databases Delete Db From #Db Db Where Exists( Select * From sys.databases d Where d.name = db.DbName and DatabasepropertyEx(d.name, 'Status') <> 'RESTORING' ) -- For each database to process Set @dbname = '' While 1=1 Begin Select Top 1 @DbName=DbName From #Db Where DbName > @dbname Order By DbName If @@rowcount = 0 break -- Switch the database from state "restoring" to available and recovered Set @sql = 'RESTORE DATABASE [] WITH RECOVERY' Set @sql = REPLACE(@sql, '', @DbName) Print '' Print '> ' + @sql Begin Try Exec sp_executeSql @sql End Try Begin Catch Set @err = ERROR_MESSAGE() Print '' Print 'Error. The database '''+ @DbName +''' has not changed to ''ONLINE''. Msg: ' + @err Update Db Set ErrorMsg = 'Msg: ' + @err From #Db Db Where Db.DbName = @DbName End Catch End --While 1=1 (DB list) -- Show results status for the selected databases Update Db Set ErrorMsg = Case When X.name Is Not Null Then 'The database is now ''ONLINE''.' Else 'Error. The database has not changed to ''ONLINE''. ' + ErrorMsg End From #Db Db Left Join master.sys.databases X On Db.DbName = X.name Collate Latin1_general_ci_ai And X.state_desc = 'ONLINE' Select DbName , ErrorMsg As N'Results ………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………' From #Db End -- Mirroring.DoRecovery GO Create Or Alter Procedure yUpgrade.UpgradeFulltextCatalogsFromSql2005 As Begin declare @sql nvarchar(max) declare @colspec nvarchar(max) declare @object_id int declare @name sysname declare @CatalogName sysname declare @IndexName sysname declare @is_accent_sensitivity_on bit declare @Colname sysname declare @TypeColname sysname declare @language_id int declare @UniqueIndexName sysname declare @FileSize int declare @PhysicalPath nvarchar(max) Set NOCOUNT ON -- ************************************************************** -- Conserver les définition d'index pour les recréer plus tard -- ************************************************************** Select Case When c.name = 'eg_files_catalog' Then 'eg_files_catalog' -- EDU_GROUPE When c.name like '%FILES[_]CATALOG' Then 'GED_FILES_CATALOG' -- Clé de Voute When c.name like '%[_]CATALOG' Then 'GED_CATALOG' -- Clé de Voute Else c.name End as CatalogName , is_accent_sensitivity_on , FULLTEXTCATALOGPROPERTY(c.name,'IndexSize') as Size , ui.name as UniqueIndexName , object_name(i.object_id) as IndexName , c1.name As ColName , c2.name as TypeColName , ic.language_id Into #FulltextIndexes From sys.fulltext_catalogs c Join sys.fulltext_indexes i On i.fulltext_catalog_id = c.fulltext_catalog_id join sys.indexes ui On ui.object_id = i.object_id And ui.index_id = i.unique_index_id join sys.fulltext_index_columns ic On ic.object_id = i.object_id join sys.columns c1 On c1.object_id = ic.object_id And c1.column_id = ic.column_id left join sys.columns c2 On c2.object_id = ic.object_id And c2.column_id = ic.type_column_id -- ************************************************************ -- Si aucun index plein texte trouver on a plus rien à faire -- ************************************************************ If Not Exists( Select * From #FulltextIndexes) Begin Print 'Aucun index plein texte non-migré trouvé' return End -- ********************************************************** -- Supprimer tous ce qui se rapporte aux indexes Fulltext -- . Indexes -- . Catalogues -- . Fichiers *.ndf -- . FileGroup -- ********************************************************** -- *************************************** -- Supprimer tous les index plein texte -- *************************************** Set @object_id = 0 while 1=1 Begin Select Top 1 @object_id = object_id From sys.fulltext_indexes Where object_id > @object_id Order by object_id If @@ROWCOUNT = 0 break Set @sql = 'DROP FULLTEXT INDEX ON []' Set @sql = Replace(@sql, '', object_name(@object_id) ) Set @sql = Replace(@sql, '"', '''') Print @sql Exec(@sql) End -- ******************************* -- Supprimer tous les catalogue -- ******************************* Set @name = '' while 1=1 Begin Select Top 1 @name = name From sys.fulltext_catalogs Where name > @name Order by name If @@ROWCOUNT = 0 break Set @sql = 'DROP FULLTEXT CATALOG []' Set @sql = Replace(@sql, '', @name ) Set @sql = Replace(@sql, '"', '''') Print @sql Exec(@sql) End -- ****************************************************************************** -- Supprimer tous les fichiers de donnée qui contiennent des index plein texte -- ****************************************************************************** Set @name = '' while 1=1 Begin Select Top 1 @name = name From sys.database_files Where name like 'ftrow_%' And name > @name Order by name If @@ROWCOUNT = 0 break Set @sql = 'ALTER DATABASE [] REMOVE FILE []' Set @sql = Replace(@sql, '', db_name()) Set @sql = Replace(@sql, '', @name ) Set @sql = Replace(@sql, '"', '''') Print @sql Exec(@sql) End -- ********************************************************************************** -- Supprimer tous les FileGroup qui contenait des fichier d'indexation plein texte -- ********************************************************************************** Set @name = '' while 1=1 Begin Select Top 1 @name = name From sys.data_spaces Where name like 'ftfg_%' And name > @name Order by name If @@ROWCOUNT = 0 break Set @sql = 'ALTER DATABASE [] REMOVE FILEGROUP []' Set @sql = Replace(@sql, '', db_name()) Set @sql = Replace(@sql, '', @name ) Set @sql = Replace(@sql, '"', '''') Print @sql Exec(@sql) End -- ************************************************************** -- Créer un FileGroup pour le ou les catalogues s'il n'existe pas déjà -- ************************************************************** If Exists (Select * From #FulltextIndexes) And Not Exists (Select * From sys.filegroups where name = 'CATALOGS') Begin Set @sql = 'ALTER DATABASE [] ADD FILEGROUP [CATALOGS]' Set @sql = Replace(@sql, '', db_name()) Set @sql = Replace(@sql, '"', '''') Print @sql Exec(@sql) End -- ************************************************************** -- Ajouter un fichier de données dans le FILEGROUP Catalogs -- ************************************************************** If Not Exists (Select * From sys.filegroups fg join sys.database_files dbf On dbf.data_space_id = fg.data_space_id Where fg.name = 'CATALOGS' And dbf.state_desc = 'ONLINE') Begin Select @FileSize = SUM(Size) * 1.25 From ( Select Distinct CatalogName, Size From #FulltextIndexes ) X Set @FileSize = Case When @FileSize = 0 Then 50 Else @FileSize End Select @PhysicalPath = Left(physical_name, Len(physical_name) - CharIndex('\', Reverse(physical_name)) + 1) From sys.database_files Where file_id = 1 Set @sql = 'ALTER DATABASE [] ADD FILE (NAME="_CATALOGS", FILENAME="_CATALOGS.ndf", SIZE=, FILEGROWTH= 10 %) TO FILEGROUP [CATALOGS]' Set @sql = Replace(@sql, '', db_name()) Set @sql = Replace(@sql, '', @PhysicalPath) Set @sql = Replace(@sql, '', convert(nvarchar(25), @FileSize)) Set @sql = Replace(@sql, '"', '''') Print @sql Exec(@sql) End -- ************************************************************** -- Recréer les catalogues -- ************************************************************** Set @CatalogName = '' while 1=1 Begin Select Distinct Top 1 @CatalogName = CatalogName, @is_accent_sensitivity_on = is_accent_sensitivity_on From #FulltextIndexes Where CatalogName > @CatalogName Order by CatalogName If @@ROWCOUNT = 0 break Set @sql = 'CREATE FULLTEXT CATALOG [] WITH ACCENT_SENSITIVITY = ' Set @sql = Replace(@sql, '', @CatalogName ) Set @sql = Replace(@sql, '', Case @is_accent_sensitivity_on When 1 Then 'ON' Else 'OFF' End ) Set @sql = Replace(@sql, '"', '''') Print @sql Exec(@sql) End -- ************************************************************** -- Recréer les indexes fulltext -- ************************************************************** Set @IndexName = '' while 1=1 Begin Select Distinct Top 1 @IndexName = IndexName, @UniqueIndexName = UniqueIndexName, @CatalogName = CatalogName From #FulltextIndexes Where IndexName > @IndexName Order by IndexName If @@ROWCOUNT = 0 break Set @sql = ' CREATE FULLTEXT INDEX ON [] ( ) KEY INDEX [] ON ([], FILEGROUP CATALOGS) WITH CHANGE_TRACKING = AUTO ' Set @ColName = '' while 1=1 Begin Select Top 1 @ColName = ColName, @TypeColName = TypeColName, @language_id = language_id From #FulltextIndexes Where IndexName = @IndexName And ColName > @ColName Order by ColName If @@ROWCOUNT = 0 break Set @colspec = ' LANGUAGE ' Set @colspec = Replace(@colspec, '', @ColName) Set @colspec = Replace(@colspec, '', Case When @TypeColName Is Null Then '' Else 'TYPE COLUMN ' + @TypeColName End) Set @colspec = Replace(@colspec, '', @language_id) Set @colspec = @colspec + ',' Set @sql = Replace(@sql, '', @colspec) End Set @sql = Replace(@sql, ',', '') Set @sql = Replace(@sql, '', @IndexName ) Set @sql = Replace(@sql, '', @UniqueIndexName ) Set @sql = Replace(@sql, '', @CatalogName ) Set @sql = Replace(@sql, '"', '''') Print @sql Exec(@sql) End End -- yUpgrade.UpgradeFulltextCatalogsFromSql2005 GO -- @@MARK: Mirroring : Upgrade SQL using failover Create Or Alter Procedure Upgrade.MakeDbCompatibleToTarget @DbName sysname As Begin Declare @sql nvarchar(max) -- Put the database in multi-user Set @sql = 'ALTER DATABASE [] SET MULTI_USER WITH ROLLBACK IMMEDIATE' Set @sql = REPLACE(@sql, '', @DbName) print @sql Exec(@sql) Declare @dbCompatLevel Int select @dbCompatLevel = compatibility_level From Sys.databases Where name = @DbName Select @sql = 'ALTER DATABASE[] SET COMPATIBILITY_LEVEL = ' + convert(nvarchar, yInstall.SqlVersionNumber ()) Set @sql = REPLACE(@sql, '', @DbName) print @sql Exec(@sql) End -- Upgrade.MakeDbCompatibleToTarget -------------------------------------------------------------------------------------- ------------------------------------------------------------------------------------------ GO -- @@MARK: Mirroring : failover Create Or Alter Procedure [Mirroring].[Failover] @IncDb nVARCHAR(max) = '' , @ExcDb nVARCHAR(max) = '' , @Simulation int = 0 , @LastDataSync Int = 1 -- for those who use other data sync solution like CommVault SQL Live Synce (similar to YourSqlDba Mirror) As Begin Declare @sql nvarchar(max) Declare @MirrorServer sysname Declare @dbname sysname Declare @dbOwner sysname Declare @FullRecoveryMode int Declare @lastLogBkpFile nvarchar(512) Declare @lastFullBkpFile nvarchar(512) Declare @lastDiffBkpFile nvarchar(512) Declare @fileName nvarchar(512) Declare @migrationTestMode Int Declare @bkpTyp nchar(1) Declare @OverwriteBackup int Declare @ListBDMigre nvarchar(max) Declare @TargetProductVersion Int Declare @ReplaceSrcBkpPathToMatchingMirrorPath nvarchar(max) Declare @ReplacePathsInDbFilenames nvarchar(max) Declare @maxSeverity int Declare @msgs Nvarchar(max) Declare @EncryptionAlgorithm nvarchar(10) Declare @EncryptionCertificate nvarchar(100) Set NOCOUNT ON -- The function udf_YourSQLDba_ApplyFilterDb apply filter parameters on this list Create table #Db ( DbName sysname primary key clustered , DbOwner sysname NULL -- because actual owner may be invalid after a restore , FullRecoveryMode int -- If = 1 log backup allowed , lastLogBkpFile nvarchar(512) null , lastFullBkpFile nvarchar(512) null , lastDiffBkpFile nvarchar(512) null , MirrorServer sysname null , ReplaceSrcBkpPathToMatchingMirrorPath nvarchar(max) , ReplacePathsInDbFilenames nvarchar(max) , ErrorMsg nvarchar(max) default 'Succès' , EncryptionAlgorithm nvarchar(10) , EncryptionCertificate nvarchar(100) , MigrationTestMode Int ) Insert into #Db ( DbName, DbOwner, FullRecoveryMode, lastLogBkpFile, lastFullBkpFile, lastDiffBkpFile, MirrorServer , ReplaceSrcBkpPathToMatchingMirrorPath, ReplacePathsInDbFilenames,EncryptionAlgorithm,EncryptionCertificate , MigrationTestMode) Select X.DbName , X.DbOwner , X.FullRecoveryMode , L.lastLogBkpFile , L.lastFullBkpFile , L.lastDiffBkpFile , L.MirrorServer , L.ReplaceSrcBkpPathToMatchingMirrorPath , L.ReplacePathsInDbFilenames , L.EncryptionAlgorithm , L.EncryptionCertificate , L.MigrationTestMode from yUtl.YourSQLDba_ApplyFilterDb (@IncDb, @ExcDb) X join Maint.JobLastBkpLocations L On L.dbName = X.DbName --And X.DbName Not IN ('master', 'model', 'msdb') -- to avoid messages about them -- Remove Snapshot databases Delete Db From #Db Db Where Exists(Select * From sys.databases d Where d.name = db.DbName and source_database_Id is not null ) If @Simulation = 0 Begin -- Synchroniser Logins on all «MirrorServer» Set @MirrorServer = '' While 1=1 Begin Select DISTINCT Top 1 @MirrorServer=MirrorServer From #Db Where MirrorServer > @MirrorServer Order by MirrorServer If @@rowcount = 0 break Print '' Print '' Print '-- *************************************************' Print '-- Synchronisation des logins sur ' + @MirrorServer Print '-- *************************************************' Exec yMirroring.LaunchLoginSync End --for each BD to process Set @dbname = '' While 1=1 Begin declare @pathBkp nvarchar(512), @language sysname Select Top 1 @dbname=DbName , @dbOwner=DbOwner , @FullRecoveryMode=FullRecoveryMode , @lastLogBkpFile=lastLogBkpFile , @lastFullBkpFile=lastFullBkpFile , @lastDiffBkpFile=lastDiffBkpFile , @MirrorServer=MirrorServer , @migrationTestMode=migrationTestMode , @ReplaceSrcBkpPathToMatchingMirrorPath=Isnull(ReplaceSrcBkpPathToMatchingMirrorPath, '') , @ReplacePathsInDbFilenames=IsNull(ReplacePathsInDbFilenames, '') , @EncryptionAlgorithm=IsNull(EncryptionAlgorithm,'') , @EncryptionCertificate=IsNull(EncryptionCertificate,'') From #Db Where DbName > @dbname Order By DbName If @@rowcount = 0 break -- Assert that the database is online If @migrationTestMode=1 Begin Update #Db Set ErrorMsg = 'Failover won''t process database ['+@dbname+'] because it is in migrationTestMode=1' Where DbName = @dbname continue End -- Assert that the database is online If DatabasepropertyEx(@dbname, 'Status') <> 'Online' Begin Update #Db Set ErrorMsg = 'Database ['+@dbname+'] is not ONLINE and cannot be processed' Where DbName = @dbname continue End -- Assert that the Database is setup for mirroring If @MirrorServer = '' Begin Update #Db Set ErrorMsg = 'Database ['+@dbname+'] is not mirrored and won''t be failed over' Where DbName = @dbname continue End -- Assert that the database is in RESTORING state Declare @DbStatus sysname Set @sql = 'SELECT @DbStatus=DbStatus FROM openquery ([], "SELECT Convert(sysname, DatabasepropertyEx("""", ""Status"")) as DbStatus")' Set @sql = REPLACE(@sql, '', @MirrorServer) Set @sql = REPLACE(@sql, '', @DbName) Set @sql = REPLACE(@sql, '"', '''') Exec sp_executesql @sql, N'@DbStatus Sysname OUTPUT', @DbStatus=@DbStatus OUTPUT If @DbStatus IS NOT NULL And @DbStatus <> 'RESTORING' Begin Update #Db Set ErrorMsg = 'Database ['+@dbname+'] is not in "RESTORING" state and won''t be failed over' Where DbName = @dbname Continue End -- Assert that the mirror server is as least the same version or higher than the source server Set @sql = 'SELECT @TargetProductVersion=Version FROM openquery ([], "SELECT YourSqlDba.yInstall.SqlVersionNumber() AS [Version]")' Set @sql = REPLACE(@sql, '', @MirrorServer) Set @sql = REPLACE(@sql, '"', '''') Exec sp_executesql @sql, N'@TargetProductVersion Int OUTPUT', @TargetProductVersion=@TargetProductVersion OUTPUT If @TargetProductVersion < yInstall.SqlVersionNumber () Begin Update #Db Set ErrorMsg = 'The target server ['+@MirrorServer+'] must be a version equal or above the source server' Where DbName = @dbname continue End -- Signal that databases in simple recovery mode are going to backuped again, and that it will -- make maintenance longer If @FullRecoveryMode = 0 Begin Update #Db Set ErrorMsg = 'Database ['+@dbname+'] is in simple recovery. A differential backup will be performed making upgrade possibly a bit slower.' Where DbName = @dbname -- End If @Simulation = 0 Begin Print '' Print '-- *************************************************' Print '-- Processing database ' + @dbname Print '-- *************************************************' -- efficient kill for all connections Set @sql = ' ALTER DATABASE [] SET Single_user WITH ROLLBACK IMMEDIATE ALTER DATABASE [] SET Multi_User ' Set @sql = REPLACE(@sql, '', @dbname) Print '' print '> ' + @sql Exec(@sql) If @LastDataSync = 1 Begin -- If the database is in full recovery mode, proceed to a last log backup -- oterwise a differential backup will be necessary If @FullRecoveryMode=1 Begin -- Do the last log backup Set @bkpTyp = 'L' Set @fileName = @lastLogBkpFile Set @OverwriteBackup = 0 End Else Begin -- Do a differential backup Set @bkpTyp = 'D' If @lastDiffBkpFile IS NOT NULL Set @fileName = @lastFullBkpFile -- overwrite existing differential backup Else Begin -- extract backup location from last full backup location to build the new filename Set @fileName = reverse(@lastFullBkpFile) Set @pathBkp = Reverse(Stuff(@filename, 1, charindex('\', @filename), '')) Exec yInstall.InstallationLanguage @Language output -- with differential backup timestamp naming is not useful in a failover context Select @filename = YourSqlDba.yMaint.MakeBackupFileName (@dbname, 'D', @pathBkp, @Language, 'Bak', 0); End Set @OverwriteBackup = 1 End If ISNULL(@EncryptionAlgorithm, '') <> '' AND ISNULL(@EncryptionCertificate, '') <> '' Begin -- backups on the same file are not supported for encrypted backups -- extract backup location from last full backup location to build the new filename Set @fileName = reverse(@lastFullBkpFile) Set @pathBkp = Reverse(Stuff(@filename, 1, charindex('\', @filename), '')) Exec yInstall.InstallationLanguage @Language output -- with differential backup timestamp naming is not useful in a failover context Select @filename = YourSqlDba.yMaint.MakeBackupFileName (@dbname, 'D', @pathBkp, @Language, 'Bak', 0); End Set @sql = yMaint.MakeBackupCmd( @dbname, @bkpTyp, @fileName, @OverwriteBackup, Null, @EncryptionAlgorithm, @EncryptionCertificate) Print '' print '> ' + @sql Exec(@sql) -- Restore backup to mirroir Set @sql = ' Exec [].YourSqlDba.yMirroring.DoRestore @BackupType="" , @Filename="" , @DbName="" , @ReplaceSrcBkpPathToMatchingMirrorPath = "" , @ReplacePathsInDbFilenames = "" ' Set @sql = REPLACE(@sql, '', @bkpTyp) Set @sql = REPLACE(@sql, '', @fileName) Set @sql = REPLACE(@sql, '', @DbName) Set @sql = REPLACE(@sql, '', @MirrorServer) Set @sql = REPLACE(@sql, '', @ReplaceSrcBkpPathToMatchingMirrorPath) Set @sql = REPLACE(@sql, '', @ReplacePathsInDbFilenames) Set @sql = REPLACE(@sql, '"', '''') Exec yExecNLog.ExecWithProfilerTrace @sql, @MaxSeverity output, @Msgs output If @maxSeverity > 10 Begin Raiserror (N'Mirroring.Failover error %s: %s %s', 11, 1, @@SERVERNAME, @Sql, @Msgs) Return (1) End End -- Set BD in Multi-User before to go offline -- it is complicated to come back multi-user when we go from offline and single_user Set @sql = 'ALTER DATABASE [] SET MULTI_USER WITH ROLLBACK IMMEDIATE' Set @sql = REPLACE(@sql, '', @dbname) Print '' print '> ' + @sql Exec(@sql) -- Put the database Offline, so no more updates and reads are possible to the old Db Set @sql = 'ALTER DATABASE [] SET OFFLINE WITH ROLLBACK IMMEDIATE' Set @sql = REPLACE(@sql, '', @DbName) Print '' print '> ' + @sql Exec(@sql) -- Switch the database on remote server, from state "restoring" to available and recovered Set @sql = 'Exec [].YourSqlDba.yMirroring.DatabaseRecovery @DbName=""' Set @sql = REPLACE(@sql, '', @DbName) Set @sql = REPLACE(@sql, '', @MirrorServer) Set @sql = REPLACE(@sql, '"', '''') Print '' print '> ' + @sql Exec(@sql) -- set the database to the original owner now it is no more in restoring state If @DbOwner IS NOT NULL -- because actual owner may already be invalid after a previous restore Begin Set @sql = 'Exec ("Alter Authorization On database::[] To []") at []' Set @sql = REPLACE(@sql, '', @DbName) Set @sql = REPLACE(@sql, '', @DbOwner) Set @sql = REPLACE(@sql, '', @MirrorServer) Set @sql = REPLACE(@sql, '"', '''') End Exec yExecNLog.ExecWithProfilerTrace @sql, @MaxSeverity output, @Msgs output If @maxSeverity > 10 Begin Raiserror (N'Mirroring.Failover error %s: %s %s', 11, 1, @@SERVERNAME, @Sql, @Msgs) Return (1) End -- Finalize the migration Set @sql = 'Exec [].YourSqlDba.Upgrade.MakeDbCompatibleToTarget @DbName=""' Set @sql = REPLACE(@sql, '', @DbName) Set @sql = REPLACE(@sql, '', @MirrorServer) Set @sql = REPLACE(@sql, '"', '''') Print '' print '> ' + @sql Exec(@sql) Set @sql = ' Update Db Set ErrorMsg = Case When X.name Is Not Null Then "Success" Else "Unexpected error. Compatibility level should""ve been set to be equal to server version. Check ShowHistoryErrors to get some details about this problem" End From #Db Db Left Join [].master.sys.databases X On Db.DbName = X.name Collate Latin1_general_ci_ai And X.compatibility_level = And X.user_access_desc = "MULTI_USER" And X.state_desc = "ONLINE" Where Db.DbName = "" ' Set @sql = REPLACE(@sql, '', @DbName) Set @sql = REPLACE(@sql, '', @MirrorServer) Set @sql = REPLACE(@sql, '', convert(nvarchar, @TargetProductVersion) ) Set @sql = REPLACE(@sql, '"', '''') Print '' print '> ' + @sql Exec(@sql) End --If @Simulation = 0 End --While 1=1 (liste des BD) End --If @Simulation = 0 -- Show maintenance status for all databases Select DbName, ErrorMsg As Statut From #Db End -- Mirroring.Failover GO -- @@MARK: TODO: Broker left over to cleanup until everybody use a mandatory version -- remove broker stuff from version previous to 7.1 version IF EXISTS (SELECT * FROM sys.services WHERE name = N'//YourSQLDba/MirrorRestore/TargetService') DROP SERVICE [//YourSQLDba/MirrorRestore/TargetService]; GO IF EXISTS (SELECT * FROM sys.service_queues WHERE name = N'YourSQLDbaTargetQueueMirrorRestore') DROP QUEUE YourSQLDbaTargetQueueMirrorRestore; GO IF EXISTS (SELECT * FROM sys.services WHERE name = N'//YourSQLDba/MirrorRestore/InitiatorService') DROP SERVICE [//YourSQLDba/MirrorRestore/InitiatorService]; GO IF EXISTS (SELECT * FROM sys.service_queues WHERE name = N'YourSQLDbaInitiatorQueueMirrorRestore') DROP QUEUE YourSQLDbaInitiatorQueueMirrorRestore; GO IF EXISTS (SELECT * FROM sys.service_contracts WHERE name = N'//YourSQLDba/MirrorRestore/Contract') DROP CONTRACT [//YourSQLDba/MirrorRestore/Contract]; GO IF EXISTS (SELECT * FROM sys.service_message_types WHERE name = N'//YourSQLDba/MirrorRestore/Request') DROP MESSAGE TYPE [//YourSQLDba/MirrorRestore/Request]; GO IF EXISTS (SELECT * FROM sys.service_message_types WHERE name = N'//YourSQLDba/MirrorRestore/Reply') DROP MESSAGE TYPE [//YourSQLDba/MirrorRestore/Reply]; GO IF EXISTS (SELECT * FROM sys.service_message_types WHERE name = N'//YourSQLDba/MirrorRestore/End') DROP MESSAGE TYPE [//YourSQLDba/MirrorRestore/End]; GO -- @@MARK: TODO : Soo old need to be removed -- the sole purpose of these synonyms is to avoid -- breaking scripts that could used the previous name with dbo. schema -- Ia also ease calling of procedures not prefixed by the schema maint. If OBJECT_ID ('dbo.DiagDbMail') IS NULL create synonym dbo.DiagDbMail For Maint.DiagDbMail If OBJECT_ID ('dbo.BringBackOnlineAllOfflineDb') IS NULL create synonym dbo.BringBackOnlineAllOfflineDb For Maint.BringBackOnlineAllOfflineDb If OBJECT_ID ('dbo.DeleteOldBackups') IS NULL create synonym dbo.DeleteOldBackups For Maint.DeleteOldBackups If OBJECT_ID ('dbo.YourSqlDba_DoMaint') IS NULL create synonym dbo.YourSqlDba_DoMaint For Maint.YourSqlDba_DoMaint If OBJECT_ID ('dbo.SaveDbOnNewFileSet') IS NULL create synonym dbo.SaveDbOnNewFileSet For Maint.SaveDbOnNewFileSet If OBJECT_ID ('dbo.SaveDbCopyOnly') IS NULL create synonym dbo.SaveDbCopyOnly For Maint.SaveDbCopyOnly If OBJECT_ID ('dbo.DuplicateDb') IS NULL create synonym dbo.DuplicateDb For Maint.DuplicateDb If OBJECT_ID ('dbo.DuplicateDbFromBackupHistory') IS NULL create synonym dbo.DuplicateDbFromBackupHistory For Maint.DuplicateDbFromBackupHistory If OBJECT_ID ('dbo.RestoreDb') IS NULL create synonym dbo.RestoreDb For Maint.RestoreDb If OBJECT_ID ('dbo.ShowHistory') IS NULL create synonym dbo.ShowHistory For Maint.ShowHistory If OBJECT_ID ('dbo.CreateNetworkDrives') IS NULL create synonym dbo.CreateNetworkDrives For Maint.CreateNetworkDrives If OBJECT_ID ('dbo.DisconnectNetworkDrive') IS NULL create synonym dbo.DisconnectNetworkDrive For Maint.DisconnectNetworkDrive If OBJECT_ID ('dbo.ListNetworkDrives') IS NULL create synonym dbo.ListNetworkDrives For Maint.ListNetworkDrives If OBJECT_ID ('dbo.PrepDbForMaintenanceMode') IS NULL create synonym dbo.PrepDbForMaintenanceMode For Maint.PrepDbForMaintenanceMode If OBJECT_ID ('dbo.RestoreDbAtStartOfMaintenanceMode') IS NULL create synonym dbo.RestoreDbAtStartOfMaintenanceMode For Maint.RestoreDbAtStartOfMaintenanceMode If OBJECT_ID ('dbo.ReturnDbToNormalUseFromMaintenanceMode') IS NULL create synonym dbo.ReturnDbToNormalUseFromMaintenanceMode For Maint.ReturnDbToNormalUseFromMaintenanceMode go grant execute on dbo.SaveDbOnNewFileSet to guest GO grant execute on dbo.SaveDbCopyOnly to guest GO grant execute on dbo.DuplicateDb to guest GO grant execute on dbo.DuplicateDbFromBackupHistory to guest GO grant execute on dbo.RestoreDb to guest GO -- check YourSqlDba account access through mirror server, and correct it if necessary. If failure to do it send a e-mail -- @@MARK: Mirroring : Verify and clean up mirroring setups Exec yMirroring.CleanMirrorServerForMissingServerAndCheckServerAccessAsYourSqlDbaAccount GO -- changing parameter name ConsecutiveDaysOfFailedBackupsToPutDbOffline to ConsecutiveFailedbackupsDaysToPutDbOffline -- ================================================================================== -- ** SAMPLE** SAMPLE** SAMPLE** SAMPLE** SAMPLE** SAMPLE** SAMPLE -- ** CONFIG of MAINTENANCE -- ================================================================================== --If Db_name() <> 'YourSqlDba' Use YourSqlDba --Exec Install.InitialSetupOfYourSqlDba -- @FullBackupPath = 'c:\iSql2005Backups' -- full backup directory --, @LogBackupPath = 'c:\iSql2005Backups' -- log backup directory --, @email = 'me@myDomain' -- log maintenance --, @SmtpMailServer = 'myEmailServer' -- email server which allow incoming smtp request from SQL Server --, @ConsecutiveDaysOfFailedBackupsToPutDbOffline = 9999 -- ================================================================================== -- ** End Of SAMPLE** ** End Of SAMPLE** ** End Of SAMPLE** -- ================================================================================== GO If Schema_id('utl') is NOT NULL drop schema utl GO -- @@MARK: Version Message Exec Install.PrintVersionInfo /*====== to follow a runnning job Select cmdStartTime, JobNo, seq, Typ, line, Txt, MaintJobName, MainSqlCmd, Who, Prog, Host, SqlAgentJobName, JobId, H.JobStart, JobEnd From (select top 1 JobStart from Maint.JobHistory order by JobNo desc) as S -- set of constants for the function below (& precomputed date range constants) cross join YourSQLDba.Maint.MaintenanceEnums as E -- HV$Now, HV$FromMidnight, HV$FromYesterdayMidnight, HV$Since12Hours, HV$Since10Min, HV$Since1Hour cross apply YourSQLDba.Maint.HistoryView(S.jobStart, HV$Now, E.HV$ShowAll) as H -- E.HV$ShowErrOnly=1, E.HV$ShowAll=0 Order By cmdStartTime, Seq, TypSeq, Typ, Line */