﻿-- Copyright 2008 Maurice Pelchat  
-- YourSQLDba : Auto-maintenance tools for SQL Server Databases
-- Author : Maurice Pelchat

-- 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.1', convert(datetime, '2025-11-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
-- ****************************************************************************************************

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 
   
Set @sql =
'
Create Database YourSQLDba
ON PRIMARY 
(
 Name = "YourSQLDba", 
 FILENAME = "<pathData>\YourSQLDba.MDF", 
 SIZE = 60MB, 
 FILEGROWTH = 20MB 
)
LOG ON     
(
 Name = "YourSQLDba_Log",
 FILENAME = "<pathLog>\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>', @pathData)
Set @sql = Replace (@sql, '<pathLog>', @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
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
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 TxtInCmt=Coalesce(InvalidDelimMsg, InvalidModuleMsg, NotFoundMsg, 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
Create Or Alter Function S#.ConcatFromJson(@InitSeparator nvarchar(10), @Separator nvarchar(10), @json nvarchar(max)) -- makes concat easier to do
Returns Table
As
Return
-----------------------------------------------------------------------------------------------------
-- this fonction accept muti-row of a single column values put in a Json Auto with optional order 

-- expression to concat extract them with openjson and concat them using an XML expression.
-- some escaped char have to be removed because they are created by the XML concat expression
--
-- There is 3 parameters, Initial separator which replace separator on the first row value
-- If initial separator is NULL, separator is used everywhere, initial separator is the same as separator
-- if initial separator is non-null, it replace first separator by its value
-----------------------------------------------------------------------------------------------------
(
  Select result
  From
    (Select Separator=ISNULL(@Separator,''), J=@json) as Prm0
    -- no initSeparator, take same value as separator
    CROSS APPLY (Select InitSeparator=ISNULL(@InitSeparator, Prm0.Separator), Prm0.J) as Prm
    -- SQL do not mesure trailings spaces in LEN function, so add an extra non space char, mesure this len minus one.
    CROSS APPLY (Select LenInitSeparator=LEN(InitSeparator+'|')-1) as LenInitSeparator
    CROSS APPLY (Select lenSeparator=LEN(Separator+'|')-1) LenSeparator
    -- concat everything with separator, so initSeparator is not there yet, separator is at its place
    CROSS APPLY
    (
    Select 
      allConcat=
      (
      Select isnull(Separator,'') +Conc.Col as [text()]
      From  (Select * From openJson(Prm0.j) with (Col nvarchar(max))) as Conc
      For XML Path(''),TYPE).value -- ,TYPE).value('.','NVARCHAR(MAX)') allow to make no escapes for ' " < > & in result
      ('.','NVARCHAR(MAX)') 
    ) As allConcat
    -- cases to deal with

    -- separator is empty string if @separator is null, and initSeparator is the same as separator, if InitSeparator is null
    -- InitSeparator isn't null, remove start of string of the len of Separator, and put InitSeparator instead as first separator
    -- Shortcut job work, if there is no sep, job done
    Outer Apply (Select ConcatAllNoSep=allConcat Where InitSeparator = '' And Separator = '') as ConcatAllNoSep
    -- otherwise Handle initialSeparator
    OUTER Apply (Select ConcatAllButFirst=STUFF(allConcat, 1, LenSeparator, InitSeparator) Where ConcatAllNoSep Is NULL) as ConcatAllButFirst
    CROSS APPLY (Select Result=COALESCE(ConcatAllNoSep, ConcatAllButFirst)) as Result
/*-- some test
Select J1.result, ExpectedRes, ok, Test=IIf(ok=1, 'Success', 'Fail')+' for '+TestName, ColsTbInJson, InitSep, Sep, Expected
From 
  (Values ('Set1')) as S(SetN)
  cross join (Select Nl=char(13)+CHAR(10)) as Nl
  cross apply
  (  
  Select 
    ColsTbInJson=
    (
    Select Col=item+NL
    From (Values ('Set1', 'Select item1&', 1), ('Set1', 'Select [item2]<', 2), ('Set1', 'Select {item3}"', 3)) as t(setN, item, ord) 
    Where t.setN=S.setN
    order by ord
    FOR Json auto
    )
  ) as x
  CROSS APPLY -- parameter for cases to test, and expected result
  (
  Select *
  From
    (
    Values 
         (NULL       , NULL   , 'Select item1&\nSelect [item2]<\nSelect {item3}"\n'          , 'Null separators, so no sep between them')
    ,    (NULL       , ', '   , ', Select item1&\n, Select [item2]<\n, Select {item3}"\n'    , 'NULL Initial separator, separator initial separator = separator')
    ,    ('          '      , 'Union all '  , '          Select item1&\nUnion all Select [item2]<\nUnion all Select {item3}"\n' , 'some initial separator to substitute to first occurence of separator' )
    ) as t(initSep, Sep    , Expected                             , TestName)
    CROSS APPLY (Select expectedRes=REPLACE(expected, '\n', NL)) as ExpectedRes
  ) TestData
  Cross apply S#.ConcatFromJson(initSep, Sep,  colsTbInJson) as J1
  Outer apply (select Ok=1 Where result=ExpectedRes) as Ok
-- end of test cases
*/

/*===KeyWords===
Scripting
===KeyWords===*/
) -- S#.ConcatFromJson
GO
-- --------------------------------------------------------------------------
-- very useful function for turning templates into real code
-- json tags are expressed as #JsonTagName# and replaced by their json values
-- --------------------------------------------------------------------------
Create Or Alter Function S#.MultipleReplaces (@Template nvarchar(max), @JsonDataSource Nvarchar(max))
Returns Table
as
Return 
---
--- JSON support makes Dbo.ReplaceTagsMatchingXMLAttributesNamesByTheirValue obsolete.
--- This version does the same thing faster.
---
-- keep this commented code below to test inner working of the query
--
--declare @JsonDataSource as nvarchar(max) =
--'
--[
--    {
--        "TbName": "[S#].[RealRestoreFileListOnly]",
--        "Cols": "  [spid] int NULL Default (@@spid)\n, [LogicalName] nvarchar(128) NULL\n, [PhysicalName] nvarchar(260) NULL\n, [Type] nchar(1) NULL\n, [FileGroupName] nvarchar(128) NULL\n, [Size] numeric(20, 0) NULL\n, [MaxSize] numeric(20, 0) NULL\n, [FileID] bigint NULL\n, [CreateLSN] numeric(25, 0) NULL\n, [DropLSN] numeric(25, 0) NULL\n, [UniqueID] uniqueidentifier NULL\n, [ReadOnlyLSN] numeric(25, 0) NULL\n, [ReadWriteLSN] numeric(25, 0) NULL\n, [BackupSizeInBytes] bigint NULL\n, [SourceBlockSize] int NULL\n, [FileGroupID] int NULL\n, [LogGroupGUID] uniqueidentifier NULL\n, [DifferentialBaseLSN] numeric(25, 0) NULL\n, [DifferentialBaseGUID] uniqueidentifier NULL\n, [IsReadOnly] bit NULL\n, [IsPresent] bit NULL\n, [TDEThumbprint] varbinary(32) NULL\n, [SnapshotURL] nvarchar(36) NULL\n"
--    },
--    {
--        "TbName": "[S#].[RealRestoreHeaderOnly]",
--        "Cols": "  [spid] int NULL Default (@@spid)\n, [BackupName] nvarchar(128) NULL\n, [BackupDescription] nvarchar(255) NULL\n, [BackupType] smallint NULL\n, [ExpirationDate] datetime NULL\n, [Compressed] tinyint NULL\n, [Position] smallint NULL\n, [DeviceType] tinyint NULL\n, [UserName] nvarchar(128) NULL\n, [ServerName] nvarchar(128) NULL\n, [DatabaseName] nvarchar(128) NULL\n, [DatabaseVersion] int NULL\n, [DatabaseCreationDate] datetime NULL\n, [BackupSize] numeric(20, 0) NULL\n, [FirstLSN] numeric(25, 0) NULL\n, [LastLSN] numeric(25, 0) NULL\n, [CheckpointLSN] numeric(25, 0) NULL\n, [DatabaseBackupLSN] numeric(25, 0) NULL\n, [BackupStartDate] datetime NULL\n, [BackupFinishDate] datetime NULL\n, [SortOrder] smallint NULL\n, [CodePage] smallint NULL\n, [UnicodeLocaleId] int NULL\n, [UnicodeComparisonStyle] int NULL\n, [CompatibilityLevel] tinyint NULL\n, [SoftwareVendorId] int NULL\n, [SoftwareVersionMajor] int NULL\n, [SoftwareVersionMinor] int NULL\n, [SoftwareVersionBuild] int NULL\n, [MachineName] nvarchar(128) NULL\n, [Flags] int NULL\n, [BindingID] uniqueidentifier NULL\n, [RecoveryForkID] uniqueidentifier NULL\n, [Collation] nvarchar(128) NULL\n, [FamilyGUID] uniqueidentifier NULL\n, [HasBulkLoggedData] bit NULL\n, [IsSnapshot] bit NULL\n, [IsReadOnly] bit NULL\n, [IsSingleUser] bit NULL\n, [HasBackupChecksums] bit NULL\n, [IsDamaged] bit NULL\n, [BeginsLogChain] bit NULL\n, [HasIncompleteMetaData] bit NULL\n, [IsForceOffline] bit NULL\n, [IsCopyOnly] bit NULL\n, [FirstRecoveryForkID] uniqueidentifier NULL\n, [ForkPointLSN] numeric(25, 0) NULL\n, [RecoveryModel] nvarchar(60) NULL\n, [DifferentialBaseLSN] numeric(25, 0) NULL\n, [DifferentialBaseGUID] uniqueidentifier NULL\n, [BackupTypeDescription] nvarchar(60) NULL\n, [BackupSetGUID] uniqueidentifier NULL\n, [CompressedBackupSize] bigint NULL\n, [Containment] tinyint NULL\n, [KeyAlgorithm] nvarchar(32) NULL\n, [EncryptorThumbprint] varbinary(20) NULL\n, [EncryptorType] nvarchar(32) NULL\n"
--    },
--    {
--        "TbName": "[S#].[InstallYourSqlDba]",
--        "Cols": "  [Version] varchar(7) Not NULL\n, [RevisionDate] date NULL\n"
--    },
--    {
--        "TbName": "[S#].[RealScriptToRun]",
--        "Cols": "  [spid] int Not NULL Default (@@spid)\n, [nestLevel] int Not NULL Default (@@nestlevel)\n, [seq] int Not NULL\n, [eventTime] datetime2(7) NULL Default (sysdatetime())\n, [Sql] nvarchar(Max) NULL\n, [label] nvarchar(Max) NULL\n, [db] sysname NULL\n"
--    },
--    {
--        "TbName": "[S#].[ProcessMaintenancePrm]",
--        "Cols": "  [PrmSetInJson] nvarchar(Max) NULL\n, [JsonJobStates] nvarchar(Max) NULL\n"
--    }
--]
--'
--Declare @template nvarchar(max)=
--'
--Create Table #TbName#
--(
--#Cols#)
--TEXTIMAGE_ON 
--'
--;
  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
    From 
      (Select JsonDataSource=@JsonDataSource) as JsonDataSource
      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 (@template, 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
/*======= some test example by calling the function itself ===============
  Select replacedTxt
  From 
    (
    Select 
      jMain =
      (
      Select --- ********** if complex expressions are used it is important to put all columns 
             --- ********** at the same select level otherwise json will not be properly formatted by Auto option
        TbName
      , Cols
      From 
        ( -- choose table
        Select TbName = Dbo.FullObjName (object_id) 
        From sys.tables
        --Where Dbo.FullObjName (object_id)  like '%realRestore%'
        ) as TbName
        cross apply
        (  
          Select 
            ColsTbInJson=
            ( -- get cols list and express it in json
            Select Col=CI.ColDef+nchar(10) 
            From Dbo.ColInfo(TbName, NULL) as CI 
            order by Ci.ColOrd 
            FOR Json auto
            )
        ) as x
        -- concat cols content from Json expression, and make put comma in front of each except the first one
        --cross apply (Select top 1 cols=j.CAT From Dbo.ConcatColFromJson('', ', ',  colsTbInJson) as J) as cols
        cross apply (Select Cols=convert(nvarchar(max),result) From S#.ConcatFromJson('', ', ',  colsTbInJson)) as cols
      For json Auto
      )
    ) as V
    cross apply
    (
    SELECT MainSyntaxTb.replacedTxt
    From 
      (Select CreateTbTemplate=TxtInCmt From S#.GetCmtBetweenDelim ('===temp===', null)) as CreateTbTemplate
/*===temp===
Create Table #TbName#
(
#Cols#)
TEXTIMAGE_ON 
===temp===*/
      Cross Apply S#.MultipleReplaces(CreateTbTemplate, jMain) as MainSyntaxTb
   ) MainSyntaxTb
=================== end sample test =================================*/
/*===KeyWords===
Scripting
===KeyWords===*/
GO
-------------------------------------------------------------------------------------
-- 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
-------------------------------------------------------------------------------------
Create Or Alter Function S#.GetTemplateFromCmtAndReplaceTags (@delim sysname, @CmtSource Sql_Variant, @JSonDataSource Nvarchar(max))
Returns table
As
Return
  Select Code=Code.replacedTxt, Template.moduleName 
  From 
    S#.GetCmtBetweenDelim (@delim, @CmtSource) as Template
    CROSS APPLY S#.MultipleReplaces (Template.TxtInCmt, @JsonDataSource) 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
-- -------------------------------------------------------------------------------------------
-- 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
-- -------------------------------------------------------------------------------------------
-- 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
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
-- -------------------------------------------------------------------------------------------
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
------------------------------------------------------------------------
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=@error_message) as ErrMessage
  CROSS APPLY (SeLect ErrNumber=@error_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#', CAST(ErrNumber as nvarchar)) ) 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
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
--------------------------------------------------------------------------------------------------------
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
------------------------------------------------------------------------------------------------
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
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)
-- ------------------------------------------------------------------------------------------
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
, nestLevel Int constraint DF_RealScriptToRun_nestLevel default @@NESTLEVEL -- allow reentrancy when working with this S#.ScriptToRun and S#.RunScript
, 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
Create View S#.ScriptToRun as 
Select Sql, db, label, Seq, nestLevel, Context, info, raiseErrorFlag, forDiagOnly
From S#.RealScriptToRun 
Where spid = @@spid
GO
-- ------------------------------------------------------------------------------------------
-- When inserting through this view, previous rows inserted are automatically cleanuped
-- ------------------------------------------------------------------------------------------
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
Create View S#.AppendToScriptToRun  as 
Select Sql, db, label, Seq, nestLevel, Context, info, raiseErrorFlag, forDiagOnly 
From S#.RealScriptToRun 
Where spid = @@spid
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
-- ------------------------------------------------------------------------------------------
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
------------------------------------------------------------------------------
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
-- ------------------------------------------------------------------------------------------
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
------------------------------------------------------------------------------------------------

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 '#pathAssemblyDll#'
          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
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 '#pathAssemblyDll#'
          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 = "<ERROR>";
                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
-- --------------------------------------------------------------------
--
-- 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
--
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<srutzky@gmail.com> 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 
    ( -- get errorLog parameter
    Select *
    From 
      sys.dm_server_registry 
      CROSS APPLY (Select InstancePathPrms=convert(nvarchar(max),value_data) ) as InstancePathPrms
      -- Remove NUL char that wreak avoc in SQL string functions
      CROSS APPLY (Select InstanceErrorLogPrm=Left(InstancePathPrms, Len(InstancePathPrms)-1) Where InstancePathPrms Like '-e%') as InstanceErrorLogPrm 
      CROSS APPLY (Select RevInstanceErrorLogPrm=REVERSE(InstanceErrorLogPrm)) as RevInstanceErrorLogPrm
      CROSS APPLY (Select lastBkSlashPos=CHARINDEX('\', RevInstanceErrorLogPrm)-1) as lastBkSlashPos
      -- remove the file name after the path and remove -e parameter identifier in front of it
      CROSS APPLY (Select pathErrLog=Stuff(LEFT(InstanceErrorLogPrm, LEN(InstanceErrorLogPrm)-lastBkSlashPos),1,2,'')) as pathErrLog
    Where value_name Like 'SqlArg%' And InstanceErrorLogPrm Like '-e%'
    ) as pathErrorLog
    CROSS APPLY (Select pathAssemblySourceCode=pathErrLog+aName+'.cs') as pathAssemblySourceCode
    CROSS APPLY (Select pathAssemblyDll=pathErrLog+aName+'.Dll') as pathAssemblyDll
    CROSS APPLY (Select pathAssemblyDirAll=pathErrLog+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, Substring(SqlDotNetDirValue , 1, 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
      Exec S#.RunScript @printOnly=0, @Silent=#Silent#
    End 
  End
  Delete #xp_cmdShellOutput 
  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

  -- 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=Code, Seq 
  From #AssemblySourceCodeView#('#pathAssemblyDll#') 
  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 = "2099-12-31";
   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
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
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
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
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
-- =========================================================================
Declare @silent Int = 1
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
---------------------------------------------------------------------------------------------------------
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
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
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
-- -------------------------------------------------------------------------------------------------------------
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
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
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 + '<br>' + r3.s +  '<Br>Reconfigure<Br>GO<Br>'
  as MsgLines
From 
  ( Select (Select CONVERT(nvarchar(max), '<br>exec Sp_Configure '''+opt+''', '+convert(nvarchar, val) + '<Br>GO') From optionVal for Xml path('')) as Msgs ) as r0 
  CROSS APPLY (Select REPLACE (r0.Msgs, '&gt;', '>') ) as r1(s)
  CROSS APPLY (Select REPLACE (r1.s, '&Lt;', '<') ) as r2(s)
  CROSS APPLY (Select REPLACE (r2.s, '&#x0D;', '<Br>')) as r3(s)
)
GO
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 'pelchatm@grics.ca'
End
GO
Create Or Alter Procedure yExecNLog.QryReplace -- do multiple replace on dynamic SQL generation
  @sql nvarchar(max) Output
, @srch1 nvarchar(1000) 
, @by1 nvarchar(1000)
, @srch2 nvarchar(1000) = ''
, @by2 nvarchar(1000) = ''
, @srch3 nvarchar(1000) = ''
, @by3 nvarchar(1000) = ''
, @srch4 nvarchar(1000) = ''
, @by4 nvarchar(1000) = ''
, @srch5 nvarchar(1000) = ''
, @by5 nvarchar(1000) = ''
, @srch6 nvarchar(1000) = ''
, @by6 nvarchar(1000) = ''
as
Begin
  set @sql = replace (@sql, @srch1, @by1)
  If isnull(@srch2,'') <> '' Set @sql = replace (@sql, @srch2, @by2)
  If isnull(@srch3,'') <> '' Set @sql = replace (@sql, @srch3, @by3)
  If isnull(@srch4,'') <> '' Set @sql = replace (@sql, @srch4, @by4)
  If isnull(@srch5,'') <> '' Set @sql = replace (@sql, @srch5, @by5)
  If isnull(@srch6,'') <> '' Set @sql = replace (@sql, @srch6, @by6)
End -- yExecNLog.QryReplace
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
Create Or Alter Function yExecNLog.FormatBasicBeginCatchErrMsg ()
returns nvarchar(max) 
as
Begin
  Return
  (
    'err :' 
  + convert(nvarchar(10), ISNULL(error_number(),0)) + ' ' 
  + ISNULL(ERROR_MESSAGE (),'No err Msg') + ' ' 
  + case when error_procedure() is not null Then ' In procedure ' + error_procedure() + ':' End
  + case when error_line() is not null Then ' at line ' + CONVERT(nvarchar, ERROR_LINE()) End
  )
End
go
-- ------------------------------------------------------------------------------
-- 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
-- ------------------------------------------------------------------------------
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
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
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
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
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
-- ------------------------------------------------------------------------------
-- under construction
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

-- 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 yExecNLog.QryReplace @sql output, '"', ''''
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
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
-- 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
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 yExecNLog.QryReplace @sql output, '"', ''''
  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 yExecNLog.QryReplace @sql output, '"', ''''
  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.
-- --------------------------------------------------------------------------------------------
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 
    "<version>" As VersionNumber
  , "<VerDate>" as VersionDate
  , "<UpdateReminderDate>" as UpdateReminderDate
  , Replicate ("=", 40) + nchar(10)+ "YourSQLDba version: <version> <verdate>" + nchar(10)+ Replicate ("=", 40) as Msg
  ) -- Install.VersionInfo
'
Set @createFct = REPLACE(@createFct, '<version>', (select version from #Version))
Set @createFct = REPLACE(@createFct, '<verDate>', (select convert(nvarchar(10), versionDate, 120) from #Version))
Set @createFct = REPLACE(@createFct, '<UpdateReminderDate>', (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
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
-- -----------------------------------------------------------------------------------------------------------
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
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
-- ------------------------------------------------------------------------------
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
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  
-----------------------------
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
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
-- -------------------------------------------------------------------------------------------
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
-- ----------------------------------------------------------------------------------------------------------
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
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
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 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) NULL
  , context nvarchar(4000) NULL
  , sql nvarchar(max) NULL
  , Info nvarchar(max)  NULL
  , err nvarchar(max)  NULL 
  , 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, @errorN, @raiseError, @forDiagOnly, @seq
    From dbo.MainContextInfo(null) as M

    -- 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')>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=yExecNLog.FormatBasicBeginCatchErrMsg () 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
    declare @msg  nvarchar(4000)
    Select @Msg=E.Err 
    From 
      (Select Err=yExecNLog.FormatBasicBeginCatchErrMsg () ) 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.
-- ------------------------------------------------------------------------------------------
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
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="<nf>"'
  Set @sql = replace(@sql, '<nf>', @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
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="<nf>"'
  Set @sql = replace(@sql, '<nf>', @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
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 [<db>] Set offline With ROLLBACK immediate
    '
    
    Set @Info = 'Database [<db>]is put offline because the previous error'

    Set @AlterDb = Replace(@AlterDb, '<db>', @DbToLockOut)
    Set @Info = Replace(@Info, '<db>', @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
-- ---------------------------------------------------------------------------------------
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.
---------------------------------------------------------------------------------------
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
  (
  <SrcQry>
  )
  , tgtRows As
  (
  <TgtQry>
  )
  , UnionOfDataSetsToCompare as
  (
  Select "(source) [<SrcInstance>].[<SrcDB>].<SrcTab>" as DataSetId
  , <ColList>
  From SrcRows
  UNION ALL 
  Select "(target) [<TgtInstance>].[<TgtDB>].<TgtTab>" as DataSetId
  , <ColList>
  From TgtRows
  )
  select 
    MAX(DataSetId) as DataSetid
  , <ColList>
  from UnionOfDataSetsToCompare
  group by 
    <ColList>
  Having MAX(DataSetId) = MIN(DataSetId)
  order by 
    <ColList>
  '
  -- 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>', @SrcQry)
  If @TgtQry = '' And  @TgtTab = '' Set  @TgtQry = @SrcQry
  If @TgtQry <> '' Set @sql = REPLACE(@sql, '<TgtQry>', @TgtQry)
  
  If @Srctab = '' Set @sql = REPLACE(@sql, '(source) [<SrcInstance>].[<SrcDB>].<SrcTab>'
                                         , '@SrcQry')
  If @Tgttab = '' Set @sql = REPLACE(@sql, '(target) [<TgtInstance>].[<TgtDB>].<TgtTab>'
                                         , '@TgtQry')
  
  If @Srctab = '' And @SrcQry = '' 
  Begin
    Print 'Provide either @srcTab or @srcQry parameter'
    Return
  End
    
  Set @sql = REPLACE(@sql, '<SrcQry>', 
  'Select
    <ColList>
  from [<SrcInstance>].[<SrcDB>].<SrcTab>
  <SrcWhereClause>')

  Set @sql = REPLACE(@sql, '<TgtQry>', 
  'Select 
    <ColList>
  from [<TgtInstance>].[<TgtDB>].<TgtTab>
  <TgtWhereClause>')

  -- replace tags
  Set @sql = REPLACE (@sql, '<ColList>', @ColList)
  Set @sql = REPLACE (@sql, '<SrcTab>', @Srctab)
  Set @sql = REPLACE (@sql, '<TgtTab>', @Tgttab)
  Set @sql = REPLACE (@sql, '<SrcWhereClause>', @SrcWhereClause)
  Set @sql = REPLACE (@sql, '<TgtWhereClause>',@TgtWhereClause)
  Set @sql = REPLACE (@sql, '<SrcDB>', @SrcDB )
  Set @sql = REPLACE (@sql, '<TgtDB>', @TgtDB )
  
  -- remove the linked server syntax part if not specified
  If @SrcInstance = '' Set @sql = REPLACE (@sql, '[<SrcInstance>].', '')
  Set @sql = REPLACE (@sql, '<SrcInstance>', @SrcInstance )

  If @TgtInstance = '' Set @sql = REPLACE (@sql, '[<TgtInstance>].', '')
  Set @sql = REPLACE (@sql, '<TgtInstance>', @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
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.
-- ---------------------------------------------------------------------------------------
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='<b><font #ErrColor# size="3">#shortResultMess#</font></b>') 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 <br> for Html crlf
    Cross Apply (Select MainSqlCmdWithBreak=Replace(Ctx.MainSqlCmd, crlf, '<br>')) as MainSqlCmdWithBreak
    -- add crlf to <br> to improve readability of html code, crlf has no effect on rendering of html
    Cross Apply (Select MainSqlCmdWithBreakAndCrLf=Replace(MainSqlCmdWithBreak, '<br>', '<br>'+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='</span><span class="kw">') as Kw
    Cross Apply (Select id='</span><span class="id">') as id
    Cross Apply (Select cm='</span><span class="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=== 
<br>
<i><b>To list ALL maintenance commands ran by the maintenance process, execute the following 
<br>command in a query window connected to the SQL Server instance that ran the maintenance:</b></i>
<div style="background-color: #ADD8E6;">
<pre>
#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
</pre>
</div>
===TemplateJobOk===*/

/*===TemplateJobErr==== 
<i><b>To list only the errors, copy & paste the following command in a query window
<br>connected to the SQL Server instance that ran the maintenance.</b></i>
<div style="background-color: #F9DEDD;">
<pre>
#kw#Select #id#cmdStartTime, JobNo, seq, Typ, line, Txt, MaintJobName, MainSqlCmd, Who, Prog, Host, SqlAgentJobName, JobId, JobStart, JobEnd </span> 
#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
</pre>
</div>
<br>
<br>To bring back quickly any databases online from offline, run this command:
<br>
<b>Exec YourSQLDba.Maint.BringBackOnlineAllOfflineDb</b>
<br>
<br>
===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
-- ------------------------------------------------------------------------------
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===
<head>
  <style type="text/css">
    .kw { color:darkblue;}
    .id { color:darkred; }
    .cm { color:darkgreen; }
  </style>  

  <style>
  /* global style for all cells */
    table {
      border-collapse: collapse;
      width: auto;
      table-layout: auto;
    }
    th, td {
      border: 1px solid #000; /* Bordure pour les cellules */
      padding: 5px;           /* Espacement intérieur de 5 pixels */
      height: 25px;           /* Hauteur fixe de 50 pixels */
      line-height: 20px;      /* Hauteur de ligne de 50 pixels pour centrer verticalement le texte */
      text-align: left;       /* Alignement du texte à gauche (modifiable selon les besoins) */
      vertical-align: middle; /* Centrage vertical du contenu */
    }
    /* Style to reduce vertical space before text */
    .reduce-space {
      margin-top: 0; /* Réduit la marge supérieure */
    }
    .no-margin {
      margin: 0; /* Réinitialise toutes les marges */
    }
  </style>  
</head>

  <body style="font-family:verdana;font-size:9pt">
  <font size="3"><b>Maintenance report from YourSqlDba <span color="#777777">#YourSqlDbaVersion#</span></b></font>
  <br>
  <table border="0" style="font-size:9pt">
    <tr><td> Server:</td> <td>#ServerInstance#</td></tr>
    <tr><td> #reportSource#</td> <td>#JobNameSource#</td></tr>
    <tr><td> Start, end: </td> <td>#StartOfMaintTxt#, &nbsp;&nbsp;#EndOfMaintTxt#</td></tr>
    <tr><td> Result:</td> <td>#shortResultMessTmp#</td></tr>
  </Table>
<Strong>#HowToShowHistory#</Strong>
  <font size="2"><b>Command launched from #Host# by #reportSource# #JobNameSource#</b></font>
  <br>
  <table border=1 cellspacing=0 style="background:#CCCCCC;border-collapse:collapse;border:none">
    <tr>
      <td valign=top style="border:solid windowtext 1.0pt">
<Strong><span style="font-family: Courier New; font-size: 0.8em;">#MainSqlCmdWithBreakAndCrLf#</span></Strong>
      </td>
    </tr>
  </table>
  </body>  
===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
-- ------------------------------------------------------------------------------
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
-- ------------------------------------------------------------------------------
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 <name> (<Physname>)
----------------------------------------------------------------------------------------------
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);
  Set @errm = YourSqlDba.yExecNLog.FormatBasicBeginCatchErrMsg ()  
  Set @errm = YourSqlDba.yExecNLog.FormatBasicBeginCatchErrMsg ()
  -- 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.
-- ------------------------------------------------------------------------------
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
-- ------------------------------------------------------------------------------
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 [<DbName>] Set online
    '
    Set @sql = yExecNLog.Unindent_TSQL(@sql)
    Exec yExecNLog.QryReplace @sql output, '<DbName>', @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
-----------------------------------------------------------------------------
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 = "<d>";'
  Set @sql = replace (@sql, '<d>', 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 = "<d>";'
  Set @sql = replace (@sql, '<d>', 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 = "<d>" '
  Set @sql = replace (@sql, '<d>', 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 = "<d>"'
  Set @sql = replace (@sql, '<d>', 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,"<d>"'
  Set @sql = replace (@sql, '<d>', 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, '<d>', 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 
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 = 
      '<SundayChecks>
      Use [<DbName>]
      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 [<DbName>]; 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)) % <SpreadCheckDb>) = <seqCheckNow>
      ' 

      -- On Saturday
      IF (@DayOfWeek = 7)
      BEGIN
        Set @sql = replace(@sql,'<SundayChecks>', 'DBCC CHECKALLOC ("<DbName>"); DBCC CHECKCATALOG ("<DbName>");'  )  
      END
      ELSE
      BEGIN
        Set @sql = replace(@sql,'<SundayChecks>', ''  )
      END

      Set @sql = replace(@sql,'<DbName>', @dbName )
      Set @sql = replace(@sql,'<SpreadCheckDb>', @SpreadCheckDb )
      Set @sql = replace(@sql,'<seqCheckNow>', @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>', @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("<DbName>") '+ IIF(@doFullCheckDb = 0,' WITH PHYSICAL_ONLY;',';')
      Set @sql = replace(@sql,'<DbName>', @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
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 [<DbName>]
     set nocount on
     ;With
       TableSizeStats as
     (
     select 
       object_schema_name(Ps.object_id) as scn --collate <srvCol>
     , object_name(Ps.object_id) as tb --collate <srvCol>
     , Sum(Ps.Page_count) as Pg
    From
      sys.dm_db_index_physical_stats (db_id("<DbName>"), 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)) % <SpreadUpdStatRun>) = <seqStatNow>
    '  
    set @sql = replace(@sql,'<srvCol>',convert(nvarchar(100), Serverproperty('collation'))) 
    Set @sql = replace(@sql,'<seqStatNow>', convert(nvarchar(20), @seqStatNow))
    Set @sql = replace(@sql,'<SpreadUpdStatRun>', convert(nvarchar(20), @SpreadUpdStatRun))
    set @sql = replace(@sql,'"','''') -- to avoid doubling of quotes in boilerplate
    set @sql = replace(@sql,'<DbName>',@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("<DbName>.<scn>.<tb>") '
      set @sql = replace (@sql, '<DbName>', @DbName)
      set @sql = replace (@sql, '<scn>', @scn)
      set @sql = replace (@sql, '<tb>', @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 [<DbName>].[<scn>].[<tb>] WITH sample <sampling> PERCENT'
        set @sql = replace (@sql, '<DbName>', @DbName)
        set @sql = replace (@sql, '<scn>', @scn)
        set @sql = replace (@sql, '<tb>', @tb)
        If @sampling = 0 
          set @sql = replace (@sql, 'WITH Sample <sampling> PERCENT', '')
        Else 
        BEGIN
          If @sampling < 100 
            set @sql = replace (@sql, '<sampling>', Str(@sampling))
          Else 
            set @sql = replace (@sql, 'Sample <sampling> 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
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 [<DbName>]
    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 <srvCol> 
    , OBJ.name --collate <srvCol>
    , OBJ.type_desc --collate <srvCol> 
    , 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("<DbName>"),  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("<DbName>"), 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,'<srvCol>',convert(nvarchar(100), Serverproperty('collation'))) 
    set @sql = replace(@sql,'"','''') -- trick to use " instead of doubling quotes in query string

    set @sql = replace(@sql,'<DbName>',@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 [<idx>] ON [<DbName>].[<scn>].[<tb>] 
                Reorganize PARTITION = <partition_number> 
                With (LOB_COMPACTION = On)
                ' 
          When @ReorgType = 'Rebuild'
          Then '
                ALTER INDEX [<idx>] ON [<DbName>].[<scn>].[<tb>] 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 [<DbName>]
    --    Create clustered index [IdxDefrag] ON [<scn>].[<tb>]  ([<colName>]) with (fillfactor = 95);
    --    Exec("Drop index [<scn>].[<tb>].[IdxDefrag]")  
    --    ' 
    --  Else 
        Set @sql = ''
      End
          
      set @sql = replace (@sql, '<scn>', @scn collate database_default)
      set @sql = replace (@sql, '<tb>', @tb collate database_default)
      set @sql = replace (@sql, '<idx>', isnull(@idx, '') collate database_default)
      set @sql = replace (@sql, '<colName>', @colName collate database_default) -- if no clustered index
      set @sql = replace (@sql, '<DbName>', @DbName collate database_default)
      set @sql = replace (@sql, '"', '''')
      
      
      If @TotPartNb > 1 
        Set @sql = replace(@sql, '<partition_number>', Convert(nvarchar(20), @partitionNum))
      Else   
        Set @sql = replace(@sql, 'PARTITION = <partition_number>', '')

      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
-- ------------------------------------------------------------------------------
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.MakeBackupFileName
(
  @DbName sysname
, @bkpTyp Char(1)
, @FullBackupPath nvarchar(512)
, @Language nvarchar(512)
, @Ext nvarchar(7) = NULL
, @TimeStampNamingForBackups Int = 1
)
returns nvarchar(max)
as
Begin
  -- ===================================================================================== 
  -- Find weekday name which is part of generated backup name
  -- ===================================================================================== 

  -- Find weekday from date.  

  Declare @DayOfWeek   nvarchar(8)
  Declare @DayOfWeekNo    Int
  Declare @DayOfWeekNoStr Char(1)
  Declare @filename  nvarchar(512)

  declare @BackupTimeStamp nvarchar(60)
  
  If @DbName <> 'msdb'
  Begin 
    Set @BackupTimeStamp = Convert(nvarchar(30), getdate(), 120)
    Set @BackupTimeStamp = STUFF (@BackupTimeStamp, 11, 1, '_')
    Set @BackupTimeStamp = STUFF (@BackupTimeStamp, 14, 1, 'h')
    Set @BackupTimeStamp = STUFF (@BackupTimeStamp, 17, 1, 'm')
  End
  Else
  Begin
    -- for MSDB we don't keep time part in the timestamp just date part because
    -- MSDB is taken in backup many times a day
    Set @BackupTimeStamp = Convert(nvarchar(10), getdate(), 121)  
  end
      
  -- use independant Set datefirst setting using @@datefirst 
  -- to get a predictible @dayOfWeekNo.  Set datefirst value is dependent of language
  Set @DayOfWeekNo = ((@@datefirst + DatePart(dw, getdate())) % 7) + 1

  -- @DayOfWeekNo = Sat = 0 Sun = 1 Mon = 2....
  -- translate Sat = 0 by Sat = 6, Sun = 1 par Sun = 7 an so on
  Set @DayOfWeekNoStr = Substring('6712345', @DayOfWeekNo, 1) 
  
  Set @DayOfWeek = 
  Case 
    When @Language like 'Français%' Then -- default french language server
      case @DayOfWeekNoStr
        when '1' then 'Lun'
        when '2' then 'Mar'
        when '3' then 'Mer'
        when '4' then 'Jeu'
        when '5' then 'Ven'
        when '6' then 'Sam'
        when '7' then 'Dim'
      end
    Else -- else default to us-english
      case @DayOfWeekNoStr
        when '1' then 'Mon'
        when '2' then 'Tue'
        when '3' then 'Wed'
        when '4' then 'Thu'
        when '5' then 'Fri'
        when '6' then 'Sat'
        when '7' then 'Sun'
      end
  End            
  -- Make file name boiler plate with replaceable parameters identified by label  between "<" et ">"
  Set @filename = '<destin><DbName>_[<DteHr>_<jour>]_<typ>.<ext>'

  -- replace parameters ....
  Set @filename = replace(@filename,'<destin>', yUtl.NormalizePath(@FullBackupPath))

  If @bkpTyp = 'F'
     Set @filename = replace(@filename,'<typ>', 'database')
  Else If @bkpTyp = 'D'
     Set @filename = REPLACE(@filename,'<typ>', 'differential')
  Else   
     Set @filename = replace(@filename,'<typ>', 'logs')

 
  -- generate logs by day by default
  If @TimeStampNamingForBackups IS NULL Or  @TimeStampNamingForBackups = 1 
  Begin 
    Set @filename = replace(@filename,'<jour>', @DayOfWeek) 
    Set @filename = replace(@filename,'<DteHr>', @BackupTimeStamp) 
  End
  Else
    Set @filename = replace(@filename,'[<DteHr>_<jour>]_', '');

  -- set extension and db name as part of the file name
  Set @filename = replace(@filename,'<ext>',  case when @bkpTyp = 'F' Then ISNULL(@Ext, 'Bak') else ISNULL(@Ext, 'Trn') end) 
  Set @filename = replace(@filename,'<DbName>', @DbName) -- nom de la Bd

  Return (@filename)
  
End -- yMaint.MakeBackupFileName
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)
)
returns nvarchar(max)
as
Begin

  Declare @sql       nvarchar(max)

  -- Make query boilerplate with replaçable parameters delimited by "<" and ">"
  -- double quotes are replaced by 2 single quotes. This trick avoid the unreadability
  -- of double single quotes
  Set @sql = 
  '
   backup <typ> [<DbName>] 
   to disk = "<fileName>" 
   with <repl><diff>, checksum, name = "<name>", bufferCount = 20, MAXTRANSFERSIZE = 4096000 <Encryption>
   '

  set @sql = replace(@sql,'"','''') -- trick that avoid doubling single quote in the boilerplate

  If @bkpTyp = 'F' Or @bkpTyp = 'D'
     Set @sql = replace(@sql,'<typ>', 'database')
  Else   
     Set @sql = replace(@sql,'<typ>', 'log')

  Set @sql = replace(@sql,'<DbName>', @DbName) -- nom de la Bd
  
  Set @sql = replace(@sql,'<repl>', case when @overwrite = 1 Then 'Init, Format' else 'noInit' end) 

  Set @sql = replace(@sql,'<diff>', case when @bkpTyp = 'D' Then ', DIFFERENTIAL' else '' end) 

  Set @sql = Replace(@sql, '<Filename>', @filename)

  If @EncryptionAlgorithm<>'' and @EncryptionCertificate<>''
  BEGIN
     Set @sql = Replace(@sql, '<Encryption>',',ENCRYPTION (ALGORITHM = <Algorithm>, SERVER CERTIFICATE = <Certificate>)')
     Set @sql = Replace(@sql, '<Algorithm>',@EncryptionAlgorithm)
     Set @sql = Replace(@sql, '<Certificate>',@EncryptionCertificate)
  END
  ELSE
     Set @sql = Replace(@sql, '<Encryption>','')

  Set @name = 
      case 
        when @name like 'SaveDbOnNewFileSet%' Then 'SaveDbOnNewFileSet'
        Else 'YourSQLDba'
      End + ':'+replace(left(convert(varchar(8), getdate(), 108),5), ':', 'h')+': '+@filename
 
  -- backup name (not file backup name, but name parameter of backup command)
  -- is limited to 128, must be truncated accordingly before time stamps
  -- patindex below finds position just before timestamps in the name
  Declare @pos int
  Declare @fin nvarchar(100)

  If len(@name) > 128
  Begin
    If @bkpTyp = 'F' 
      Set @pos = Patindex ('%[_]_________________________[_]database.Bak', @name)
    Else 
      Set @pos = Patindex ('%[_]_________________________[_]logs.trn', @name);

    Set @fin = Substring(@name, @pos, 255)
    Set @name = left(@name, 128 - len(@fin) - 3) + '...' + @fin
  End
  
  Set @sql = replace(@sql,'<name>', @name)

  Return (@sql)
  
End -- yMaint.MakeBackupCmd
GO
-------------------------------------------------------------------------------------
-- Sp to ensure and or create specific SQL Agent Job for restore to MirrorServer
-------------------------------------------------------------------------------------
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
-- ----------------------------------------------------------------------------------------------
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
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
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
-- ------------------------------------------------------------------------------
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 = '<ERROR>')
  Begin
    Set @err = 
    (
    Select CONVERT(nvarchar(max),'')+Line+NCHAR(10) as [text()]
    From @FilesFromFolder
    Where line <> '<ERROR>'
    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
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 [<loginname>];'
      Set @loginExists = 0      
    End

    -- If login needs to be created
    If @loginExists = 0
    Begin
      If @type IN ('G', 'U')
        Set @sql = @sql + 'CREATE LOGIN [<loginname>] FROM WINDOWS WITH DEFAULT_LANGUAGE=<language>;'    
      Else
        Set @sql = @sql + 
        '
        CREATE LOGIN [<loginname>]
          WITH PASSWORD=<password> HASHED
             , SID=<sid>
             , CHECK_POLICY=<checkpolicy>
             , CHECK_EXPIRATION=<checkexpiration>, DEFAULT_LANGUAGE=<language>;
        '
    End

    -- If login has been disabled, disable it
    If @is_disabled_local = 0 AND @is_disabled = 1
      Set @sql = @sql +'ALTER Login [<loginname>] DISABLE;'

    -- If login has been enabled, enable it
    If @is_disabled_local = 1 AND @is_disabled = 0
      Set @sql = @sql +'ALTER Login [<loginname>] ENABLE;'

    If @sysadmin       = 1 Set @sql = @sql + 'EXEC sp_addsrvrolemember "<loginname>", "sysadmin";'
    If @securityadmin  = 1 Set @sql = @sql + 'EXEC sp_addsrvrolemember "<loginname>", "securityadmin";'
    If @serveradmin    = 1 Set @sql = @sql + 'EXEC sp_addsrvrolemember "<loginname>", "serveradmin";'
    If @setupadmin     = 1 Set @sql = @sql + 'EXEC sp_addsrvrolemember "<loginname>", "setupadmin";'
    If @processadmin   = 1 Set @sql = @sql + 'EXEC sp_addsrvrolemember "<loginname>", "processadmin";'
    If @diskadmin      = 1 Set @sql = @sql + 'EXEC sp_addsrvrolemember "<loginname>", "diskadmin";'
    If @dbcreator      = 1 Set @sql = @sql + 'EXEC sp_addsrvrolemember "<loginname>", "dbcreator";'
    If @bulkadmin      = 1 Set @sql = @sql + 'EXEC sp_addsrvrolemember "<loginname>", "bulkadmin";'

    Set @sql = REPLACE(@sql, '<loginname>', @loginname)
    Set @sql = REPLACE(@sql, '<password>', Convert(nvarchar(max),@password_hash,1))
    Set @sql = REPLACE(@sql, '<sid>', Convert(nvarchar(max),@sid,1))
    Set @sql = REPLACE(@sql, '<checkpolicy>', @policy_checked)
    Set @sql = REPLACE(@sql, '<checkexpiration>', @expiration_checked)
    If @defLanguage is Not NULL
      Set @sql = REPLACE(@sql, '<language>', @deflanguage)
    Else
    Begin 
      Set @sql = REPLACE(@sql, ' WITH DEFAULT_LANGUAGE=<language>', '')
      Set @sql = REPLACE(@sql, ', DEFAULT_LANGUAGE=<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
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 [<mirrorserver>].YourSqlDba.yMirroring.MirrorLoginSync @servername = "<servername>", @loginname = "<loginname>", @type = "<type>", @password_hash = <password_hash>, @sid = <sid>, @policy_checked = "<policy_checked>", @expiration_checked = "<expiration_checked>", @deflanguage = "<deflanguage>", @sysadmin = <sysadmin>, @securityadmin = <securityadmin>, @serveradmin = <serveradmin>, @setupadmin = <setupadmin>, @processadmin = <processadmin>, @diskadmin = <diskadmin>, @dbcreator = <dbcreator>, @bulkadmin = <bulkadmin>, @jobNo = NULL, @is_disabled=<is_disabled>'
      
      Set @sql = yExecNLog.Unindent_TSQL(@sql)
      Set @sql = REPLACE(@sql, '<mirrorserver>', @MirrorServerName)     
      Set @sql = REPLACE(@sql, '<servername>', @servername)
      Set @sql = REPLACE(@sql, '<loginname>', @loginname)
      Set @sql = REPLACE(@sql, '<type>', @type)
      Set @sql = REPLACE(@sql, '<password_hash>', Convert(nvarchar(max),@password_hash,1))
      Set @sql = REPLACE(@sql, '<sid>', Convert(nvarchar(max),@sid,1))
      Set @sql = REPLACE(@sql, '<policy_checked>', @policy_checked)
      Set @sql = REPLACE(@sql, '<expiration_checked>', @expiration_checked)
      Set @sql = REPLACE(@sql, '<deflanguage>', @deflanguage)
      Set @sql = REPLACE(@sql, '<sysadmin>', @sysadmin)
      Set @sql = REPLACE(@sql, '<securityadmin>', @securityadmin)
      Set @sql = REPLACE(@sql, '<serveradmin>', @serveradmin)
      Set @sql = REPLACE(@sql, '<setupadmin>', @setupadmin)
      Set @sql = REPLACE(@sql, '<processadmin>', @processadmin)
      Set @sql = REPLACE(@sql, '<diskadmin>', @diskadmin)
      Set @sql = REPLACE(@sql, '<dbcreator>', @dbcreator)
      Set @sql = REPLACE(@sql, '<bulkadmin>', @bulkadmin)
      Set @sql = REPLACE(@sql, '<is_disabled>',@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
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 ([<RemoteServer>], "select Db_Id(""YourSQLDba"") as DbId") as x
  If @Exists Is NULL
    Set @RemoteVersionInfo = "Remote YourSqlDba is missing"
  '

  Set @sql = REPLACE( @sql, '<RemoteServer>', @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 ([<RemoteServer>], "select OBJECT_ID(""YourSQLDba.Install.versioninfo"") as ObjectId") as x

  If @exists IS NULL
    Set @RemoteVersionInfo = "Version before Install.VersionInfo"
  '
    
  Set @sql = REPLACE( @sql, '<RemoteServer>', @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 ([<RemoteServer>], "select versionNumber From YourSQLDba.Install.VersionInfo() F") as x
  '

  Set @sql = REPLACE( @sql, '<RemoteServer>', @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
-- ------------------------------------------------------------------------------
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) = ''
  
  -- 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
      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'
      End  
      Else If @DoDiffBkp = 1
      Begin
        Set @fileName = yMaint.MakeBackupFileName (@DbName, 'D', @FullBackupPath, @Language, @FullBkExt, @TimeStampNamingForBackups)
        Set @ctx = 'Diff backups'
      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'

        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
      Exec yExecNLog.LogAndOrExec 
         @context = 'yMaint.backups'
       , @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 = "<DbName>", @MustLogBackupToShrink = @MustLogBackupToShrink  output
        truncate table #MustLogBackupToShrink 
        insert into #MustLogBackupToShrink Values(@MustLogBackupToShrink)
        '
        Set @sql2 = replace(@sql2, '<DbName>', @DbName)
        Set @sql2 = replace(@sql2, '<JobNo>', 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
        Set @fileName = yMaint.MakeBackupFileName(@DbName, 'L', @LogBackupPath, @Language, @LogBkExt, @TimeStampNamingForBackups)  
        
        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
        Exec yExecNLog.LogAndOrExec 
          @context = 'yMaint.backups'
        , @sql = @sql
        , @Info = 'Log backups (init)'
        , @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.
----------------------------------------------------------------------------------------------------------
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='[REDACTED] 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=[REDACTED] @useself='False'
  
    -- Make YourSqlDba be the same with same account on other serveur.
     Set @sql = '
     Print "Synchronize YourSqlDba account on <mirrorserver>"
     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=<password_hash> HASHED
       Exec (""alter authorization on database::yoursqldba to [YourSqlDba]"")
       CREATE CREDENTIAL YourSqlDbaRemoteServerCred WITH IDENTITY = ""YourSqlDba"", SECRET = ""<newPwd>"";  
     End
     End try
     Begin catch
       Declare @msg nvarchar(max) = ""Msg ""+convert(varchar, error_number())+"" ""+Error_message()
       Raiserror (@Msg, 11, 1)
     End catch
     "
     ) At [<mirrorServer>]
     '
     Set @sql = REPLACE(@sql, '<mirrorserver>', @MirrorServerName)     
     Set @sql = REPLACE(@sql, '<password_hash>', Convert(nvarchar(max),@password_hash_Local,1))
     Set @sql = REPLACE(@sql, '<newPwd>', @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 
----------------------------------------------------------------------------------------------------------
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 ([<RemoteServer>], "select Db_Id(""Master"") as DbId") as x
    '
    Set @sql = REPLACE( @sql, '<RemoteServer>', @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 <servername> 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
      '
      <br>
      <b>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!</b>
      <br>
      <br>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:
        <br>
        <br>
        <b>Exec YourSQLDba.Mirroring.SetYourSqlDbaAccountForMirroring</b>
        <br>
      If the same MirrorServer has multiple source servers specify a common password on every of them:
        <br>
        <br>
        <b>Exec YourSQLDba.Mirroring.SetYourSqlDbaAccountForMirroring @YourSqlDbaAccountForMirroringPwd = ''choose some password''
        <br>
      '
    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
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
-------------------------------------------------------------------------------------------
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
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
  
  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) 

  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 <a href="YourSqlDba documentation home page">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</a>, 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: <ServerInstance>.
        <br><br>
        By applying the latest version you get rid of this monthly reminder.
        <br><br>
        Update is very easy. Just get the latest script from versions links from <a href="https://tinyurl.com/27wspjdf">YourSqlDba documentation home page</a>, then open it copy/paste and then run it.
        <br><br>
        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 <a href="https://github.com/pelsql/YourSqlDba">YourSqlDba''s Github home project</a> 
        and from this home page there is a <a href="https://github.com/pelsql/YourSqlDba#readme">Read me</a> section where you can also find a link to the most recent script version.
        <br>
        '
      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: <ServerInstance>.
        <br><br>
        L''application de la dernière version fait disparaître ce message.
        <br><br>
        La mise à jour est très simple. Obtenez le script en cliquant sur le lien de la plus récente version à partir de 
        la <a href="https://tinyurl.com/27wspjdf">Page d''acceuil de la documentation de YourSqlDba</a> et puis ouvrez, copier/collez le script puis exécutez-le.
        <br><br>
        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 <a href="https://github.com/pelsql/YourSqlDba">Github</a> et à partir de cette page d''acceuil se trouve une section <a href="https://github.com/pelsql/YourSqlDba#readme">Lisez-moi</a>, ou se trouve également un lien vers la version la plus récente.
        <br>
        '
      End

      Set @msgBody  = replace(@msgBody, '<ServerInstance>', convert(sysname, serverproperty('ServerName')))

      Set @msgBody  = replace(@msgBody, '<ServerInstance>', 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 
    Delete Mirroring.RestoreQueue -- remove leftover from previous exec with error flag E
  End

  -- ==============================================================================
  -- 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
  End -- If @DoBackup  

  -- Wait for emptying the Mirroring.RestoreQueue
  -- If backups are to be mirrored than we Launch a login synchronisation on the mirror server
  If isnull(@MirrorServer, '') <> ''
    And (@DoBackup = 'F' Or @DoBackup = 'D')
  Begin
    While (1=1) -- wait all mirrored backup to be done before lauching login synchro
    Begin
      -- wait while there is still something to for process
      If Exists (Select * From Mirroring.RestoreQueue Where ErrorN = 0)
        Waitfor Delay '00:01:00' -- wait a minute for a second test
      Else
      Begin
        -- there is nothing else to process but restores in error remain
        If Exists (Select * From Mirroring.RestoreQueue Where ErrorN <> 0)
        Begin
          Exec yExecNLog.LogAndOrExec 
            @context = 'Maint.YourSqlDba_DoMaint'
          , @Info = 'Error in Maint.YourSqlDba_DoMaint'
          , @err = 'Some restores to MirrorServer failed. See Mirroring.RestoreQueue'
          Break;
        End
        Else
        Begin -- nothing remains then quit
          If Not Exists(Select * From Mirroring.RestoreQueue)
            Break
        End
      End 
    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
    declare @msg  nvarchar(4000)
    Select @Msg=yExecNLog.FormatBasicBeginCatchErrMsg()

    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
-- ------------------------------------------------------------------------------
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
-- -------------------------------------------------------------------------------------------------------------
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 [<DbName>]
  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 [<DbName>].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>', @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 [<DbName>] TO DISK ="<nomSauvegarde>" WITH stats=1, INIT, format, COPY_ONLY, NAME = "SaveDbCopyOnly: <nomSauvegarde>"
    '
  
  Set @sql = Replace( @sql, '<DbName>', @DbName )
  Set @sql = Replace( @sql, '<nomSauvegarde>', @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
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 <DbName>

  Insert Into #dbfiles (noseq, name, physical_name, separateur )
  Select
    ROW_NUMBER() OVER(ORDER BY name),
    name
  , physical_name
  , Charindex("\", Reverse(physical_name))
  FROM [<DbName>].sys.database_files
  '

  Set @sql = replace(@sql, '"', '''')
  Set @sql = replace(@sql, '<DbName>', @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 [<DbNameDest>]
   FROM DISK="<nomSauvegarde>" 
   WITH 
     stats=1,REPLACE
   <ClauseMove>
  '

  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 "<logicalname>" TO "<physical_path><physical_name>"'

    Set @ClauseMove = Replace( @ClauseMove, '<logicalname>', @name )
    Set @ClauseMove = Replace( @ClauseMove, '<physical_path>', Left(@physical_name, len(@physical_name) - @separateur) )
    Set @ClauseMove = Replace( @ClauseMove, '<physical_name>'
                             , Replace( Right(@physical_name, @separateur), @sourceDb, @TargetDb ))

    Set @sql = Replace(@sql, '<ClauseMove>', @ClauseMove + nchar(13) + nchar(10) + '<ClauseMove>') 


    If Replace(@name, @sourceDb, @TargetDb) <> @name
    Begin
      Set @AlterLogicalFiles = @AlterLogicalFiles 
                             + ' ALTER DATABASE [<DbNameDest>] MODIFY FILE (NAME="<logicalname>", NEWNAME="<new_logicalname>")' 
                             + nchar(13) 
                             + nchar(10)
	  Set @AlterLogicalFiles = replace (@AlterLogicalFiles, '<DbNameDest>', @TargetDb)
	  Set @AlterLogicalFiles = replace (@AlterLogicalFiles, '<logicalname>', @name)
	  Set @AlterLogicalFiles = replace (@AlterLogicalFiles, '<new_logicalname>', Replace(@name, @sourceDb, @TargetDb) )
	End

    Set @NoSeq = @NoSeq + 1

  End

  Drop Table if exists #dbfiles

  Set @sql = Replace(@sql, '<ClauseMove>', '')
  Set @sql = replace (@sql, '"', '''')
  Set @sql = replace (@sql, '<DbNameDest>', @TargetDb)
  Set @sql = replace (@sql, '<nomSauvegarde>', @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
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 <DbName>

  Insert Into #dbfiles (noseq, name, physical_name, separateur )
  Select
    ROW_NUMBER() OVER(ORDER BY name),
    name
  , physical_name
  , Charindex("\", Reverse(physical_name))
  FROM [<DbName>].sys.database_files
  '

  Set @sql = replace(@sql, '"', '''')
  Set @sql = replace(@sql, '<DbName>', @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 [<DbNameDest>]
   FROM DISK="<nomSauvegarde>" 
   WITH 
     stats=1,REPLACE,NORECOVERY
   <ClauseMove>
  <LogRestore>
  Restore Log [<DbNameDest>] 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 "<logicalname>" TO "<physical_path><physical_name>"'

    Set @ClauseMove = Replace( @ClauseMove, '<logicalname>', @name )
    Set @ClauseMove = Replace( @ClauseMove, '<physical_path>'
                             , Left(@physical_name, len(@physical_name) - @separateur) )
    Set @ClauseMove = Replace( @ClauseMove, '<physical_name>'
                             , Replace( Right(@physical_name, @separateur), @sourceDb, @TargetDb ))

    Set @sql = Replace(@sql, '<ClauseMove>', @ClauseMove + nchar(13) + nchar(10) + '<ClauseMove>') 


    If Replace(@name, @sourceDb, @TargetDb) <> @name
    Begin
      Set @AlterLogicalFiles = @AlterLogicalFiles 
                             + ' ALTER DATABASE [<DbNameDest>] MODIFY FILE (NAME="<logicalname>", NEWNAME="<new_logicalname>")' 
                             + nchar(13) 
                             + nchar(10)
	     Set @AlterLogicalFiles = replace (@AlterLogicalFiles, '<DbNameDest>', @TargetDb)
	     Set @AlterLogicalFiles = replace (@AlterLogicalFiles, '<logicalname>', @name)
	     Set @AlterLogicalFiles = replace ( @AlterLogicalFiles, '<new_logicalname>'
	                                      , 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 [<DbNameDest>] From Disk="<LogBackupFile>" With FILE=<Position>, NoRecovery'  
      
      Set @RestoreLog = Replace(@RestoreLog, '<DbNameDest>', @TargetDb)
      Set @RestoreLog = Replace(@RestoreLog, '<LogBackupFile>', @LogBkpFile)
      Set @RestoreLog = Replace(@RestoreLog, '<Position>', Convert(nvarchar(255), @position))
      
      Set @Sql = replace(@Sql, '<LogRestore>', @RestoreLog +  Char(13) + Char(10) + '<LogRestore>' )  
        
    End
  
  End  
      
  Drop Table if exists #dbfiles

  Set @sql = replace(@sql, '<LogRestore>', '')
  Set @sql = Replace(@sql, '<ClauseMove>', '')
  Set @sql = replace (@sql, '"', '''')
  Set @sql = replace (@sql, '<DbNameDest>', @TargetDb)
  Set @sql = replace (@sql, '<nomSauvegarde>', @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 [<DbNameDest>] SET RECOVERY SIMPLE'
    Set @sql = REPLACE(@sql, '<DbNameDest>', @TargetDb)
    
    Exec (@sql)
  End
  
End -- Maint.duplicateDbFromBackupHistory

GO
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 [<DbNameDest>]
   FROM DISK="<nomSauvegarde>" 
   WITH 
   <ClauseMove><Replace>
  '

  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 "<logicalname>" TO "<physical_path>\<physical_name>"'

    Set @ClauseMove = Replace( @ClauseMove, '<logicalname>', @LogicalName )

    If Replace(@LogicalName, @DbName, @TargetDb) <> @LogicalName
    Begin
      Set @AlterLogicalFiles = @AlterLogicalFiles 
                             + ' ALTER DATABASE [<DbNameDest>] MODIFY FILE (NAME="<logicalname>", NEWNAME="<new_logicalname>")' 
                             + nchar(13) 
                             + nchar(10)
      Set @AlterLogicalFiles = replace (@AlterLogicalFiles, '<DbNameDest>', @TargetDb)
      Set @AlterLogicalFiles = replace (@AlterLogicalFiles, '<logicalname>', @LogicalName)
      Set @AlterLogicalFiles = replace ( @AlterLogicalFiles
                                       , '<new_logicalname>'
                                       , 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
                               , '<physical_path>'
                               , Case When @FileType = 'L' Then @pathLog Else @pathData End )
      Set @ClauseMove = Replace( @ClauseMove
                               , '<physical_name>'
                               , 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, '<physical_path>', @pathLog )
        Set @ClauseMove = Replace( @ClauseMove, '<physical_name>', @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, '<physical_path>', @pathData )

        If @NoSeq = 0
          Set @ClauseMove = Replace( @ClauseMove, '<physical_name>', @TargetDb + '.mdf' )
        Else
          Set @ClauseMove = Replace( @ClauseMove
                                   , '<physical_name>'
                                   , @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, '<physical_path>', @pathData )

        Set @ClauseMove = Replace( @ClauseMove
                                 , '<physical_name>'
                                 , @TargetDb + '.FTCatalog' + convert(nvarchar, @NoSeq) )

        Set @NoSeq = @NoSeq + 1
      End

    End

    Set @sql = Replace( @sql
                      , '<ClauseMove>'
                      , @ClauseMove + nchar(13) + nchar(10) + ',<ClauseMove>') 

  End

  Set @sql = Replace(@sql, ',<ClauseMove>', '')
  Set @sql = replace (@sql, '"', '''')
  Set @sql = replace (@sql, '<DbNameDest>', @TargetDb)
  Set @sql = replace (@sql, '<nomSauvegarde>', @PathAndFilename)
  Set @sql = replace ( @sql
                     , '<Replace>'
                     , 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
-- ------------------------------------------------------------------------------
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
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
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 = "<operateur>"
  , @MaintJobName = "YourSQLDba: DoInteg,DoUpdateStats,DoReorg,Full backups"
  , @DoInteg = 1
  , @DoUpdStats = 1
  , @DoReorg = 1
  , @DoBackup = "F"
  , @FullBackupPath = "<destin>" 
  , @LogBackupPath = "<destinLog>"  
  -- 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 = <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, '<destin>', @FullBackupPath) 
  Set @sql = replace (@sql, '<destinLog>', @LogBackupPath) 
  Set @sql = replace (@sql, '<operateur>', @operator)
  Set @sql = replace ( @sql
                     , '<ConsecutiveDaysOfFailedBackupsToPutDbOffline>'
                     , 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 = "<operateur>"
  , @MaintJobName = ''Log backups''
  , @DoBackup = ''L''
  , @FullBackupPath = "<destin>" 
  , @LogBackupPath = "<destinLog>" 
  -- 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, '<destin>', @FullBackupPath) 
  Set @sql = replace (@sql, '<destinLog>', @LogBackupPath) 
  Set @sql = replace (@sql, '<operateur>', @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
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 = [REDACTED]

  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 =
  '
  <br>
  <font size="3"><b>Initial setup of YourSqlDba : Review YourSqlDba jobs step</b></font><br>
  <br>
  <br>
  Full maintenance job step:
  <br>
  <br>
  <table width="100%" border=1 cellspacing=0 cellpadding=5 style="background:#CCCCCC;border-collapse:collapse;border:none">
    <tr>
    <td width="100%" valign=top style="border:solid windowtext 1.0pt">
        <font face="Courier New" size="2">
        <span style="color:navy">
#FullMaintenanceScript#
        </span></font>
      </td>
    </tr>
    <tr>
  </table>
  <br>
  <br>
  Log backups jobs:
  <br>
  <br>
  <table width="100%" border=1 cellspacing=0 cellpadding=5 style="background:#CCCCCC;border-collapse:collapse;border:none">
    <tr>
    <td width="100%" valign=top style="border:solid windowtext 1.0pt">
        <font face="Courier New" size="2">
        <span style="color:navy">
#LogBackupScript#
        </span></font>
      </td>
    </tr>
  </table>
  '
  Set @msg = replace(@Msg, '#FullMaintenanceScript#', replace(replace(@FullMaintenanceScript, nchar(13), ''),nchar(10),'<br>'))
  Set @msg = replace(@Msg, '#LogBackupScript#', replace(replace(@LogBackupScript, nchar(13), ''),nchar(10),'<br>'))

  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.
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.
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 yExecNLog.QryReplace @sql output, '"', ''''
  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

-- ------------------------------------------------------------------------------
-- 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 <DriveLetter> /Delete'
  Set @cmd  = Replace( @cmd, '<DriveLetter>', @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 <DriveLetter> <unc>'
    Set @cmd  = Replace( @cmd, '<DriveLetter>', @DriveLetter )
    
    Set @cmd  = Replace( @cmd, '<unc>', @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
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 <DriveLetter> /DELETE'
      Set @cmd  = Replace( @cmd, '<DriveLetter>', @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
Create Or Alter Proc Maint.ListNetworkDrives 
As 
Begin

  Set nocount on

  Select DriveLetter, Unc
  From Maint.NetworkDrivesToSetOnStartup

End -- Maint.ListNetworkDrives
GO
If object_id('yMirroring.InactivateYourSqlDbaJobs') is not null 
  drop proc yMirroring.InactivateYourSqlDbaJobs
GO
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 "<job_id>", @enabled= 0'
   Set @sql = Replace( @sql, '<job_id>', convert(nvarchar(36), @job_id))
   Set @sql = Replace( @sql, '"', '''')

   --Print @sql
   Exec( @sql )
 End

 Drop table if exists #jobs

End -- yMirroring.InactivateYourSqlDbaJobs
GO
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
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
          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;

      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
-------------------------------------------------------------------------------------
Create Or Alter Procedure Mirroring.AddServer 
  @MirrorServer nvarchar(512)
, @remoteLogin nvarchar(512)
, @remotePassword nvarchar(512)
, @ExcSysAdminLoginsInSync int = 0
, @ExcLoginsFilter nvarchar(max) = ''
, @MirrorServerDataSrc nvarchar(512) = ''
, @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='', @provider='SQLNCLI', @datasrc=@MirrorServerDataSrc
  END
  ELSE
  BEGIN
    EXEC master.dbo.sp_addlinkedserver @server = @MirrorServer, @srvproduct=N'SQL Server'
  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 = [REDACTED]

  -- 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 = [REDACTED]
  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
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, '&GT;', '>')

    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 "&GT;" 
    -- so it must be turned back to >
    Set @ReplaceSrcBkpPathToMatchingMirrorPath = replace(@ReplaceSrcBkpPathToMatchingMirrorPath, '&GT;', '>')

    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 abut its last_lsn in msdb
      -- Generate restore command
      Set @sql = 
      '
       If databasepropertyex("<DbNameDest>", "status")="RESTORING"
       Begin
         Exec
         (
         "
         Restore Database [<DbNameDest>] with recovery;
         Drop DATABASE [<DbNameDest>];
         "
         )
       End;
       RESTORE DATABASE [<DbNameDest>]
       FROM DISK="<nomSauvegarde>" 
       WITH 
         <ClauseMove>
         ,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 "<logicalname>" TO "<physical_name>"'
          Set @ClauseMove = Replace( @ClauseMove, '<physical_name>', @PhysicalName )
          Set @ClauseMove = Replace( @ClauseMove, '<logicalname>', @LogicalName )
        End
        Else  
        Begin -- use default Db location parameters

          Set @ClauseMove = 'MOVE "<logicalname>" TO "<physical_path>\<physical_name>"'

          If @FileType = 'L' -- log location not usually the same that other files
            Set @ClauseMove = Replace( @ClauseMove, '<physical_path>', @pathLog )  --bug restore
          Else
            Set @ClauseMove = Replace( @ClauseMove, '<physical_path>', @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, '<physical_name>', @PhysicalName )
          Set @ClauseMove = Replace( @ClauseMove, '<logicalname>', @LogicalName )
        End
            
        Set @sql = Replace(@sql, '<ClauseMove>', @ClauseMove + nchar(13) + nchar(10) + '        ,<ClauseMove>') 
      End

      Set @sql = Replace(@sql, ',<ClauseMove>', '')
      Set @sql = replace (@sql, '"', '''')
      Set @sql = replace (@sql, '<DbNameDest>', @DbName)
      Set @sql = replace (@sql, '<nomSauvegarde>', @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 = '<RestoreCmd>'
      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 [<DbNameDest>]
         FROM DISK="<nomSauvegarde>" 
         WITH FILE=<Position>, NORECOVERY, CHECKSUM,bufferCount = 20,MAXTRANSFERSIZE = 4096000
        '    
        Set @sqlcmd = replace (@sqlcmd, '"', '''')
        Set @sqlcmd = replace (@sqlcmd, '<DbNameDest>', @DbName)
        Set @sqlcmd = replace (@sqlcmd, '<nomSauvegarde>', @Filename)
        Set @sqlcmd = replace (@sqlcmd, '<Position>', CONVERT(nvarchar(10), @Position))

        Set @sql = REPLACE( @sql, '<RestoreCmd>', @sqlcmd)
        Set @sql = @sql + char(10) + '<RestoreCmd>'
            
      End
      
      Set @sql = REPLACE( @sql, '<RestoreCmd>', '')
      
    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 = '<RestoreCmd>'
      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
         
      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 LOG [<DbNameDest>]
         FROM DISK="<nomSauvegarde>" 
         WITH FILE=<Position>, NORECOVERY, CHECKSUM,bufferCount = 20,MAXTRANSFERSIZE = 4096000
        '    
        Set @sqlcmd = replace (@sqlcmd, '"', '''')
        Set @sqlcmd = replace (@sqlcmd, '<DbNameDest>', @DbName)
        Set @sqlcmd = replace (@sqlcmd, '<nomSauvegarde>', @Filename)
        Set @sqlcmd = replace (@sqlcmd, '<Position>', CONVERT(nvarchar(10), @Position))

        Set @sql = REPLACE( @sql, '<RestoreCmd>', @sqlcmd)
        Set @sql = @sql + char(10) + '<RestoreCmd>'
            
      End
      
      Set @sql = REPLACE( @sql, '<RestoreCmd>', '')
      
    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 <DriveLetter> /Delete'
      Set @cmd  = Replace( @cmd, '<DriveLetter>', @DriveLetter )
      
      Exec sp_executeSql @Sql, N'@cmd nvarchar(4000)', @cmd
      
      Set @cmd = 'net use <DriveLetter> <unc>'
      Set @cmd  = Replace( @cmd, '<DriveLetter>', @DriveLetter )
      Set @cmd  = Replace( @cmd, '<unc>', @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.
-- -------------------------------------------------------------------------
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="<db>"    
      If Not Exists (Select * From [<db>].sys.tables Where name="RecoveryModelBeforePrepDbForMaintenanceMode")
        Select convert(sysname, DATABASEPROPERTYEX ("<db>", "recovery")) as recovery_model_desc 
        Into [<db>].dbo.RecoveryModelBeforePrepDbForMaintenanceMode
	   
	    Alter database [<db>] Set Single_User With Rollback  Immediate
	    Alter database [<db>] MODIFY NAME = [<db>_<suffix>]
	    Alter database [<db>_<suffix>] Set MULTI_USER With Rollback  Immediate	 
      '
      Set @sql = replace(@sql, '<db>', @name)
      Set @sql = replace(@sql, '<suffix>', @DbNameSuffixForMaintenance)
      Set @sql = replace(@sql, '"', '''')
    
      --print @sql
      exec(@sql)
      Set @sql = 
      '
      use [<db>_<suffix>];

	    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, '<db>', @name)
      Set @sql = replace(@sql, '<suffix>', @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 = "<db>_<suffix>",@PathAndFilename="<backuppath>\<db>_<suffix>.Bak"'
          
          Set @sql = replace(@sql, '<db>', @name)
          Set @sql = replace(@sql, '<suffix>', @DbNameSuffixForMaintenance)
          Set @sql = replace(@sql, '<backuppath>', @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 ("<db>_<suffix>", "recovery") <> "SIMPLE" 
	     Alter database [<db>_<suffix>] Set RECOVERY SIMPLE WITH NO_WAIT	   
    '
      Set @sql = replace(@sql, '<db>', @name)
      Set @sql = replace(@sql, '<suffix>', @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 [<db>] From Disk = "<FullBackupFile>" With NoRecovery ,stats=1, replace
  <LogRestore>
  Restore Log [<db>] 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 [<db>] From Disk="<LogBackupFile>" With FILE=<Position>, NoRecovery, STOPATMARK="<StopMark>"'  
      Set @RestoreLog = Replace(@RestoreLog, '<Position>', Convert(nvarchar(255), @position))
      
      Set @SqlCmd = replace(@SqlCmd, '<LogRestore>', @RestoreLog +  Char(13) + Char(10) + '<LogRestore>' )  
      Set @SqlCmd = replace(@SqlCmd, '<LogBackupFile>', @LogBkpFile)
        
    End
  
  End  
  
  Set @SqlCmd = replace(@SqlCmd, '<LogRestore>', '')
  Set @SqlCmd = replace(@SqlCmd, '<db>', @DbName)
  Set @SqlCmd = replace(@SqlCmd, '<StopMark>', @LogMarkName)
  Set @SqlCmd = replace(@SqlCmd, '<FullBackupFile>', @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 [<db>] SET Single_User WITH ROLLBACK IMMEDIATE
          ALTER DATABASE [<db>] SET MULTI_USER WITH ROLLBACK IMMEDIATE
          Restore Database [<db>] From Disk = "<backuppath>\<db>.Bak" With stats=1, replace
          '
        
          Set @sql = replace(@sql, '<db>', @name)
          Set @sql = replace(@sql, '<backuppath>', @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 [<db>] SET Single_user WITH ROLLBACK IMMEDIATE
          ALTER DATABASE [<db>] SET MULTI_USER WITH ROLLBACK IMMEDIATE
          '+@Sql
                 
          Set @sql = replace(@sql, '<db>', @name)
          Set @sql = replace(@sql, '<backuppath>', @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 [<DbAndSuffix>];
    Set @recovery_model_saved = 
    convert(int, objectpropertyex(object_id("dbo.RecoveryModelBeforePrepDbForMaintenanceMode"), "isUserTable"))
    '
    Set @sql = replace(@sql, '<DbAndSuffix>', @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 [<DbAndSuffix>].dbo.RecoveryModelBeforePrepDbForMaintenanceMode'
      Set @sql = replace(@sql, '<DbAndSuffix>', @DbAndSuffix)
      Exec Sp_ExecuteSql @Sql, N'@recovery_model sysname output', @recovery_model Output
   
      Set @sql = 
      '
      If Db_name() <> "Master"  Use Master;
	     Alter database [<DbAndSuffix>] SET Single_User WITH ROLLBACK IMMEDIATE
	     Alter database [<DbAndSuffix>] SET Multi_User
      Alter Database [<DbAndSuffix>] Set RECOVERY <recovery_model>
      Drop Table If Exists [<DbAndSuffix>].dbo.RecoveryModelBeforePrepDbForMaintenanceMode
      '
      Set @sql = replace(@sql, '<DbAndSuffix>', @DbAndSuffix)
      Set @sql = replace(@sql, '<recovery_model>', @recovery_model)
      Set @sql = replace(@sql, '"', '''')
      Exec (@sql)
    End

    Set @sql = 
    '
    If Db_name() <> "Master"  Use Master;
      Alter database [<DbAndSuffix>] SET Single_user WITH ROLLBACK IMMEDIATE
      Alter database [<DbAndSuffix>] MODIFY NAME = [<db>]
      Alter database [<db>] Set MULTI_USER With Rollback  Immediate
    '
    Set @sql = replace(@sql, '<DbAndSuffix>', @DbAndSuffix)
    Set @sql = replace(@sql, '<Db>', @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
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
-- ---------------------------------------------------------------------------------------
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 [<DbName>_Export]
  ON 
    <FSpecData>
  Log On 
    <fSpecLog>
  Collate <Collate>  
  '
  Set @sql = REPLACE(@Sql, '<DbName>', @dbName)
  If @collation Is Null
    Set @sql = REPLACE(@Sql, '<collate>', convert(sysname, DatabasepropertyEx(@dbName, 'Collation')))
  else
    Set @sql = REPLACE(@Sql, '<collate>', @collation)

  Set @fileId = 0
  Set @DataSpaceid = 0
  Set @fileGroupNameAv = ''
  
  Set @fSpec = 
  '
  <Fgroup>
  <fSpec>
  '

  While (1=1)
  begin
    Set @sqlM = -- fichiers data, du groupe primaire en premier, puis des autres
    '
    use [<DbName>]
    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>', @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, '<Fgroup>'
                           , 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, ', <Fgroup>', ', ')
      
    Set @fSpec = REPLACE(@fSpec, '<FSpec>', 
   '
    (
      NAME = <LogicalFileName>
    , FILENAME = "<PhysicalFileName>"
    , SIZE = <size>
    , MAXSIZE = <max_size>
    , FILEGROWTH = <growth>
    )
  , <FGroup>
    <fSpec>'
    )   

    
    Set @fSpec = REPLACE(@fSpec, '<LogicalFileName>', Replace (@name, @dbName, @dbName+'_Export'))
    Set @fSpec = REPLACE(@fSpec, '<PhysicalFileName>',  Replace (@physicalName, @dbName, @dbName+'_Export') )
    Set @fSpec = REPLACE(@fSpec, '<Size>', @Size)
    Set @fSpec = REPLACE(@fSpec, '<max_Size>', @maxSize + @maxSizeUnit)
    Set @fSpec = REPLACE(@fSpec, '<growth>', @Growth + @growthMode)

  End
  Set @fSpec = REPLACE(@fSpec, ', <FGroup>', '') -- remove remaining tag
  Set @fSpec = REPLACE(@fSpec, '<FSpec>', '') -- remove remaining tag
  
  Set @Sql = REPLACE (@sql, '<FSpecData>', @fSpec) -- insert it into create database stmt
    

  Set @fileId = 0
  Set @fSpec = 
  '
  <FSpec>
  '
      
  While (1=1)
  begin
    Set @sqlM = -- fichiers data, du groupe primaire en premier, puis des autres
    '
    use [<DbName>]
    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>', @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, '<FSpec>', 
    '(
      NAME = <LogicalFileName> 
    , FILENAME = "<PhysicalFileName>"
    , SIZE = <size>
    , MAXSIZE = <max_size>
    , FILEGROWTH = <growth>
    )
    , <FSpec>')   

    Set @fSpec = REPLACE(@fSpec, '<LogicalFileName>', Replace (@name, @dbName, @dbName+'_Export'))
    Set @fSpec = REPLACE(@fSpec, '<PhysicalFileName>',  Replace (@physicalName, @dbName, @dbName+'_Export'))
    Set @fSpec = REPLACE(@fSpec, '<Size>', @size)
    Set @fSpec = REPLACE(@fSpec, '<max_Size>', @maxSize+@maxSizeUnit)
    Set @fSpec = REPLACE(@fSpec, '<growth>', @Growth+@GrowthMode)

  End
  Set @fSpec = REPLACE(@fSpec, ', <FSpec>', '') -- remove remaining tag
  Set @Sql = REPLACE (@sql, '<FSpecLog>', @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 [<db>_export]
  Set recovery Simple
  '
  Set @sql = replace(@sql, '<db>', @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 [<Db>]
  ;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, '<db>', @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 [<Db>]
  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, '<db>', @dbName)
  set @sql = replace (@sql, '<svrCollation>', 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 [<Db>]
  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, '<db>', @dbName)
  set @sql = replace (@sql, '<svrCollation>', 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
    [<db>].INFORMATION_SCHEMA.KEY_COLUMN_USAGE CU
    ON CU.CONSTRAINT_SCHEMA = K.Sn Collate <svrCollation> And
       CU.CONSTRAINT_NAME = K.Kn Collate <svrCollation>  

    left JOIN   -- index clé primaire ou index unique référencé (IX.name)
    [<db>].sys.indexes IX
    on Ix.object_id = K.RefObjId
       And Ix.index_id = K.KeyId

    left JOIN  -- table référencé (Ob.name)
    [<Db>].sys.objects Ob
    on ob.Object_Id = K.RefObjId

    left JOIN   -- schema si index unique référencé (S.name)
    [<Db>].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é
    [<Db>].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)
    [<Db>].sys.columns c
    ON c.object_id = Ixc.object_id 
       And c.column_id = Ixc.column_id
  '  
  set @sql = replace (@sql, '<svrCollation>', convert(sysname, Serverproperty('Collation')))
  set @sql = replace (@sql, '<db>', @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 [<Db>]
  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, '<svrCollation>', convert(sysname, Serverproperty('Collation')))
  Set @sql = replace(@sql, '<db>', @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 [<db>]
  truncate table #ColsTablesAMigr
  ;With TabList
  as
  (
  /*
        (Select 
           "dbo" as sn, name as tn, object_id as id, 0 as iden, *
         from [<Db>].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, '<svrCollation>', convert(sysname, Serverproperty('Collation')))
  Set @sql = replace(@sql, '<db>', @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 [<Db>]
  ;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, '<db>', @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 [<Db>]
  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, '<svrCollation>', convert(sysname, Serverproperty('Collation')))
  Set @sql = replace(@sql, '<db>', @dbName)
  Set @sql = replace(@sql, '"', '''')
  
  Exec yExecNLog.LogAndOrExec 
    @context = 'yExport.ExportData'
  , @Info = 'Recording index info.'
  , @sql = @sql
  , @raiseError = @stopOnError
  
  Set @sql =
  '
  Use [<Db>]
  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, '<svrCollation>', convert(sysname, Serverproperty('Collation')))
  Set @sql = replace(@sql, '<db>', @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 [<db>_export]
    exec("Create Schema [<sn>] authorization dbo")
    '
    Set @sql = replace(@sql, '"', '''')
    Set @sql = replace(@sql, '<db>', @dbName)
    Set @sql = replace(@sql, '<sn>', @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 [<db>_export]
    exec("Create Type [<sn>].[<nouvTyp>] From <typ> <lc> <nullSpec>")
    '
    Set @sql = replace(@sql, '"', '''')
    Set @sql = replace(@sql, '<db>', @dbName)
    Set @sql = replace(@sql, '<sn>', @sn)
    Set @sql = replace(@sql, '<nouvTyp>', @nouvTyp)
    Set @sql = replace(@sql, '<typ>', @typ)
    Set @sql = replace(@sql, '<lc>', @lc)
    Set @sql = replace(@sql, '<nullspec>', @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 [<db>]
  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, '<db>', @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 [<db>_export]
      set ansi_nulls <aNull>;
      Set quoted_identifier <qIden>;
      If object_id("[<sn>].[<mn>]") 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("[<sn>].[<mn>]") is not null
        Set @created = 1
      Else 
        Set @created = 0
      ' 
      Print 'try create ['+@sn+'].['+@mn+']'
      print '------------------------------------------'
      Set @sql = replace(@sql, '"', '''')
      Set @sql = replace(@sql, '<db>', @dbName)
      Set @sql = replace(@sql, '<sn>', @sn)
      Set @sql = replace(@sql, '<mn>', @mn)
      Set @Sql = replace(@Sql, '<aNull>', case When @aNull = 1 Then 'On' Else 'Off' End)
      Set @Sql = replace(@Sql, '<qIden>', 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 [<db>_export].[<sn>].[<tn>] à partir de [<Db>].[<sn>].[<tn>]
      '

      Set @sql = replace(@sql, '"', '''')
      Set @sql = replace(@sql, '<db>', @dbName)
      Set @sql = replace(@sql, '<sn>', @sn)
      Set @sql = replace(@sql, '<tn>', @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 ' [<cn>]\n' 
                Else '    ,[<cn>]\n' 
              End 
        End
            
        Set @ColsSelect = Replace(@ColsSelect, '<cn>', @cn)
        Set @ColsSelect = Replace(@ColsSelect, '<lc>', @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 + 
            '[<cn>] [<typ>] <lc> <defIden> <defdef> <nullSpec> \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, '<defdef>', '')
        Else  
          Set @colsCreateTable = Replace( @colsCreateTable
                                        , '<defdef>'
                                        , 'CONSTRAINT [DF_<tn>_<cn>] Default <def>')
          
        -- remplace tous les marqueurs  
        Set @colsCreateTable = Replace(@colsCreateTable, '<tn>', @tn)
        Set @colsCreateTable = Replace(@colsCreateTable, '<cn>', @cn)
        If @is_computed <> 1
        Begin
          Set @colsCreateTable = Replace(@colsCreateTable, '<Typ>', @Typ)
          Set @colsCreateTable = Replace(@colsCreateTable, '<lc>', @lc)
          Set @colsCreateTable = Replace(@colsCreateTable, '<defIden>', isnull(@defIden,''))
          Set @colsCreateTable = Replace(@colsCreateTable, '<def>', isnull(@def,''))
          Set @colsCreateTable = Replace(@colsCreateTable, '<nullSpec>', @nullSpec)
        End
        Else
        Begin
          Set @colsCreateTable = Replace(@colsCreateTable, '[<Typ>]', 'as '+@computedColDef)
          Set @colsCreateTable = Replace(@colsCreateTable, '<lc>', '')
          Set @colsCreateTable = Replace(@colsCreateTable, '<defIden>', '')
          Set @colsCreateTable = Replace(@colsCreateTable, '<def>', '')
          Set @colsCreateTable = Replace(@colsCreateTable, '<nullSpec>', '')
        End

      End -- While une colonne de la table
      
  -------------------------------------------------------------------------------------     
  --  Executer le create de la table
  -------------------------------------------------------------------------------------
      Set @Sql = 
      '
      Use [<db>_export]      
      create Table [<sn>].[<tn>]
      (
      <colsCreateTable>
      )
      ON [<FgDat>] TEXTIMAGE_ON [<FgLob>]
      '
      Set @sql = replace(@sql, '<db>', @dbName)
      Set @sql = replace(@sql, '<sn>', @sn)
      Set @sql = replace(@sql, '<tn>', @tn)
      Set @sql = replace(@sql, '<colsCreateTable>', @colsCreateTable)

      If @FgDat = ''
        Set @sql = replace(@sql, 'ON [<FgDat>] ', '')
      Else  
        Set @sql = replace(@sql, '<FgDat>', @FgDat)

      If @FgLob = '' Or (@fgLob = @fgDef)
        Set @sql = replace(@sql, 'TEXTIMAGE_ON [<FgLob>]', '')
      Else  
        Set @sql = replace(@sql, '<FgLob>', @FgLob)

      Set @sql = replace(@sql, '"', '''')
      Set @sql = Replace(@sql, '\n', nchar(10)) -- il y en a dans <colsInsertInto>
      
      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 <db>
      If exists
         (
         select * 
         from sys.views 
         where name = "<tn>"
           And Schema_name(schema_id) = "yExport_<sn>"
         )
        Set @schemaAlt = 1
      Else
        Set @schemaAlt = 0
      '  
      Set @sql = replace(@sql, '<db>', @dbName)
      Set @sql = replace(@sql, '<sn>', @sn)
      Set @sql = replace(@sql, '<tn>', @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 [<db>_export].[<sn>].[<tn>] at %s",10,1, @d) with nowait -- force output no error
      '
      Set @sql = replace(@sql, '<db>', @dbName)
      Set @sql = replace(@sql, '<sn>', @sn)
      Set @sql = replace(@sql, '<tn>', @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 [<db>_export].[<sn>].[<tn>] on
      
      
      Insert into [<db>_export].[<sn>].[<tn>] with (tablock)
      (
      <colsInsertInto>
      )
      Select 
      <colsSelect>
      from [<Db>].[<sn>].[<tn>]      
      
      
      Set @nb = @@rowcount
      Set identity_insert [<db>_export].[<sn>].[<tn>] 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 [<db>_export].[<sn>].[<tn>] ",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, '[<Db>].[<sn>].[<tn>]', '[<Db>].[yExport_<sn>].[<tn>]')

      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 [<db>_export].[<sn>].[<tn>] on', '')
        Set @sql = replace(@sql, 'Set identity_insert [<db>_export].[<sn>].[<tn>] off', '')
      End  
      Set @sql = replace(@sql, '<colsInsertInto>', @ColsInsertInto)
      Set @sql = replace(@sql, '<colsSelect>', @ColsSelect)
      Set @sql = replace(@sql, '<db>', @dbName)
      Set @sql = replace(@sql, '<sn>', @sn)
      Set @sql = replace(@sql, '<tn>', @tn)
      Set @sql = replace(@sql, '"', '''')
      Set @sql = Replace(@sql, '\n', nchar(10)) -- il y en a dans <colsInsertInto>

      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 [<db>_export]
         ' +
         Case 
           When @Pk = 1 Then 
           '
           Alter table [<sn>].[<tn>] add constraint [<IdxN>] primary key <ikc>
           (<cols>) 
           With (FILLFACTOR = 90) ON [<FgIdx>]
           '
           When @iUniqC = 1 Then 
           '
           Alter table [<sn>].[<tn>] add constraint [<IdxN>] unique 
           (<cols>) 
           With (FILLFACTOR = 90) ON [<FgIdx>]
           '
           Else
           '
           Create <iUniq> <ikc> Index [<IdxN>] On [<sn>].[<tn>]  
           (<cols>) 
           With (FILLFACTOR = 90) ON [<FgIdx>]
        '
        End -- case
        
        Set @sql = replace(@sql, '<db>', @dbName)
        Set @sql = replace(@sql, '<sn>', @sn)
        Set @sql = replace (@sql, '<tn>', @tn)
        Set @sql = replace (@sql, '<IdxN>', @In)
        Set @sql = replace (@sql, '<ikc>', @TypDesc)
        Set @sql = replace (@sql, '<iUniq>', case When @iUniq = 1 Then 'Unique' Else '' End)
        Set @sql = replace (@sql, '<cols>', @Cols)
        Set @sql = replace (@sql, '<ligcols>', @LigCols)
        Set @sql = replace (@sql, '<pk>', Str(@pk,1))
        Set @sql = replace (@sql, '<iUniqC>', Str(@iUniqC,1))

        If @FgIdx = ''
          Set @sql = replace(@sql, 'ON [<FgIdx>] ', '')
        Else  
          Set @sql = replace(@sql, '<FgIdx>', @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 [<db>_export]
    declare @c int
    Select @c = count(distinct [<cn>])
    From (Select [<sn>].[<tn>].[<cn>] From [<sn>].[<tn>] tablesample(10 percent)) as x
    '
    Set @sql = Replace(@sql, '<db>', @dbName)
    Set @sql = Replace(@sql, '<sn>', @Sn)
    Set @sql = Replace(@sql, '<tn>', @tn)
    Set @sql = Replace(@sql, '<cn>', @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 [<db>_export]
    Alter table [<ks>].[<tn>] <nocheck> add constraint [<Kn>] FOREIGN KEY 
    (<cols>)
    REFERENCES [<Usn>].[<Utn>]
    (<uCols>)
    <FkOnClause>;
    Alter table [<ks>].[<tn>] NoCheck Constraint [<Kn>];
    Checkpoint
    '
    If @is_not_trusted = 0 
    Begin
      Set @sql = replace (@sql, '<nocheck>', '')
      Set @sql = replace (@sql, 'Alter table [<sn>].[<tn>] NOCHECK Constraint [<Kn>];', '')
    End  
    Else  
      Set @sql = replace (@sql, '<nocheck>', 'WITH NOCHECK')
      
    Set @sql = replace(@sql, '<db>', @dbName)
    Set @sql = replace (@sql, '<ks>', @ks)
    Set @sql = replace (@sql, '<tn>', @tn)
    Set @sql = replace (@sql, '<Kn>', @Kn)
    Set @sql = replace (@sql, '<cols>', @Cols)
    Set @sql = replace (@sql, '<Usn>', @uSn)
    Set @sql = replace (@sql, '<Utn>', @utn)
    Set @sql = replace (@sql, '<uCols>', @uCols)
    Set @sql = replace (@sql, '<FkOnClause>', @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 [<db>_Export]
  Select @logn = name from sys.database_files where type_desc = "LOGN"
  '
  Set @sql = replace(@sql, '<db>', @dbName)
  Set @sql = replace(@sql, '"', '''')
  Exec sp_executeSql @Sql, N'@logN sysname output', @logn = @logn output

  Set @sql =
  '
  use [<db>_Export]
  dbcc shrinkfile("<logn>") with NO_INFOMSGS
  '
  Set @sql = replace(@sql, '<db>', @dbName)
  Set @sql = replace(@sql, '"', '''')
  Set @sql = replace(@sql, '<logn>', @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 [<db>]

   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
     [<Db>].sys.database_principals P
   Where p.type_desc = "SQL_USER"
     And p.default_schema_name is not null
     And not exists(Select * from [<Db>_export].sys.database_principals as E Where E.name = P.Name collate Latin1_general_ci_ai)
   '
   Set @sql = replace(@sql,'<db>', @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 [<Db>]

  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 [<Db>_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, '<db>', @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 [<Db>]

  ;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,'<db>', @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 [<db>_export];
      ALTER AUTHORIZATION ON Database::[<db>_export] To [<LogN>];
      '
      Set @sql = replace(@sql,'<db>', @dbName)
      Set @sql = replace(@sql,'<LogN>', 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 [<db>_export];
        Create user [<Usr>] For Login [<logN>] with default_schema = <default_schema>
        '
      Else
        Set @sql =
        '
        -- user is orphaned or aliased to dbo
        use [<db>_export];
        Create user [<Usr>] 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 [<db>_export];
        Grant connect to guest  
        '
      End
    End  

    Set @sql = replace(@sql,'<db>', isnull(@dbName, ''))
    Set @sql = replace(@sql,'<usr>', isnull(@Usr, ''))
    Set @sql = replace(@sql,'<logN>', isnull(@logN, ''))
    If @default_schema is null
      Set @sql = replace (@sql, 'with default_schema = <default_schema>', '')
    Else
      Set @sql = replace (@sql, '<default_schema>', @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 [<db>_export];
    exec ("Create ROLE <role> AUTHORIZATION dbo") 
    '
    Set @sql = replace (@sql, '<Role>', @r)
    Set @sql = replace (@sql, '<db>', @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 [<db>_export];
    If "<membre>" <> "dbo" 
      Exec sp_addrolemember "<role>", "<membre>"
    '
    Set @sql = replace (@sql, '<db>', @dbName)
    Set @sql = replace (@sql, '<Role>', @r)
    Set @sql = replace (@sql, '<membre>', @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 [<db>]

  ;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,'<db>', @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 [<db>_export]; 
    If object_Id("[<obj>]") IS NOT NULL
      <action> <perms> ON OBJECT::[<obj>] ([<Col>]) 
      To <SomeUsers> <WithGrantOpt>
    '
    Set @Sql = replace(@sql, 
                       '<action>', 
                       case 
                         When @action <> 'GRANT_WITH_GRANT_OPTION' 
                         Then @action 
                         Else 'GRANT' 
                       End
                      )
    Set @Sql = replace(@sql, 
                       '<WithGrantOpt>', 
                       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>', @perms)
    Set @Sql = replace(@sql, '<obj>', @obj)
    Set @Sql = replace(@sql, '<SomeUsers>', @SomeUsers)
    If @col is NULL      
      Set @Sql = replace(@sql, '([<Col>])', '')
    Else
      Set @Sql = replace(@sql, '<Col>', @col)

    Set @sql = replace(@sql,'<db>', @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 [<db>] WITH RECOVERY, REPLACE' 
 
 Set @sql = REPLACE(@sql, '<db>', @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 [<db>] WITH RECOVERY' 

    Set @sql = REPLACE(@sql, '<db>', @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 [<tn>]'
    Set @sql = Replace(@sql, '<tn>', 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 [<cn>]'
    Set @sql = Replace(@sql, '<cn>', @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 [<db>] REMOVE FILE [<fn>]'
    Set @sql = Replace(@sql, '<db>', db_name())
    Set @sql = Replace(@sql, '<fn>', @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 [<db>] REMOVE FILEGROUP [<fgn>]'
    Set @sql = Replace(@sql, '<db>', db_name())
    Set @sql = Replace(@sql, '<fgn>', @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 [<db>] ADD FILEGROUP [CATALOGS]'
    Set @sql = Replace(@sql, '<db>', 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 [<db>] ADD FILE (NAME="<db>_CATALOGS", FILENAME="<filepath><db>_CATALOGS.ndf", SIZE=<size>, FILEGROWTH= 10 %) TO FILEGROUP [CATALOGS]'
    Set @sql = Replace(@sql, '<db>', db_name())
    Set @sql = Replace(@sql, '<filepath>', @PhysicalPath)
    Set @sql = Replace(@sql, '<size>', 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 [<name>] WITH ACCENT_SENSITIVITY = <AS>'
    Set @sql = Replace(@sql, '<name>', @CatalogName )  
    Set @sql = Replace(@sql, '<AS>', 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 [<tn>] 
    (
    <colspec>
    )
    KEY INDEX [<indexname>]
    ON ([<catalog>], 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 = '<cn> <TypeColumn> LANGUAGE <language_id>'
      Set @colspec = Replace(@colspec, '<cn>', @ColName)
      Set @colspec = Replace(@colspec, '<TypeColumn>', Case When @TypeColName Is Null Then '' Else 'TYPE COLUMN ' + @TypeColName End)
      Set @colspec = Replace(@colspec, '<language_id>', @language_id)
          
      Set @colspec = @colspec + ',<colspec>'
      Set @sql = Replace(@sql, '<colspec>', @colspec)

    End

    Set @sql = Replace(@sql, ',<colspec>', '')

      
    Set @sql = Replace(@sql, '<tn>', @IndexName )  
    Set @sql = Replace(@sql, '<indexname>', @UniqueIndexName )    
    Set @sql = Replace(@sql, '<catalog>', @CatalogName )  
    Set @sql = Replace(@sql, '"', '''')
    Print @sql
    Exec(@sql)
      
  End

End -- yUpgrade.UpgradeFulltextCatalogsFromSql2005
GO
Create Or Alter Procedure Upgrade.MakeDbCompatibleToTarget
  @DbName sysname
As
Begin
  Declare @sql nvarchar(max)
 
  -- Put the database in multi-user
  Set @sql = 'ALTER DATABASE [<db>] SET MULTI_USER WITH ROLLBACK IMMEDIATE' 
 
  Set @sql = REPLACE(@sql, '<db>', @DbName)
  print @sql
  Exec(@sql)


  Declare @dbCompatLevel Int
  select @dbCompatLevel = compatibility_level From Sys.databases Where name = @DbName

  Select @sql = 'ALTER DATABASE[<db>] SET COMPATIBILITY_LEVEL = ' + convert(nvarchar, yInstall.SqlVersionNumber ())
  Set @sql = REPLACE(@sql, '<db>', @DbName)
  print @sql
  Exec(@sql)

End -- Upgrade.MakeDbCompatibleToTarget
--------------------------------------------------------------------------------------
------------------------------------------------------------------------------------------
GO
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 ([<MirrorServer>], "SELECT Convert(sysname, DatabasepropertyEx(""<dbname>"", ""Status"")) as DbStatus")'
    Set @sql = REPLACE(@sql, '<MirrorServer>', @MirrorServer)  
    Set @sql = REPLACE(@sql, '<dbName>', @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 ([<MirrorServer>], "SELECT YourSqlDba.yInstall.SqlVersionNumber() AS [Version]")'
    Set @sql = REPLACE(@sql, '<MirrorServer>', @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 [<dbname>] SET Single_user WITH ROLLBACK IMMEDIATE
      ALTER DATABASE [<dbname>] SET Multi_User 
      ' 
     
      Set @sql = REPLACE(@sql, '<dbname>', @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 [<MirrorServer>].YourSqlDba.yMirroring.DoRestore 
          @BackupType="<BackupType>"
        , @Filename="<Filename>"
        , @DbName="<DbName>"
        , @ReplaceSrcBkpPathToMatchingMirrorPath = "<ReplaceSrcBkpPathToMatchingMirrorPath>"
        , @ReplacePathsInDbFilenames = "<ReplacePathsInDbFilenames>"
        '
        Set @sql = REPLACE(@sql, '<BackupType>', @bkpTyp)
        Set @sql = REPLACE(@sql, '<Filename>', @fileName)
        Set @sql = REPLACE(@sql, '<DbName>', @DbName)
        Set @sql = REPLACE(@sql, '<MirrorServer>', @MirrorServer)  
        Set @sql = REPLACE(@sql, '<ReplaceSrcBkpPathToMatchingMirrorPath>', @ReplaceSrcBkpPathToMatchingMirrorPath)  
        Set @sql = REPLACE(@sql, '<ReplacePathsInDbFilenames>', @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 [<dbname>] SET MULTI_USER WITH ROLLBACK IMMEDIATE' 
    
      Set @sql = REPLACE(@sql, '<dbname>', @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 [<dbname>] SET OFFLINE WITH ROLLBACK IMMEDIATE' 
    
      Set @sql = REPLACE(@sql, '<dbname>', @DbName)
      Print ''
      print '> ' + @sql
      Exec(@sql)

      -- Switch the database on remote server, from state "restoring" to available and recovered
      Set @sql = 'Exec [<MirrorServer>].YourSqlDba.yMirroring.DatabaseRecovery @DbName="<DbName>"'
      
      Set @sql = REPLACE(@sql, '<DbName>', @DbName)
      Set @sql = REPLACE(@sql, '<MirrorServer>', @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::[<DbName>] To [<DbOwner>]") at [<MirrorServer>]'
        Set @sql = REPLACE(@sql, '<DbName>', @DbName)
        Set @sql = REPLACE(@sql, '<DbOwner>', @DbOwner)  
        Set @sql = REPLACE(@sql, '<MirrorServer>', @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 [<MirrorServer>].YourSqlDba.Upgrade.MakeDbCompatibleToTarget @DbName="<DbName>"'
      
      Set @sql = REPLACE(@sql, '<DbName>', @DbName)
      Set @sql = REPLACE(@sql, '<MirrorServer>', @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
        [<mirrorserver>].master.sys.databases X
        On Db.DbName = X.name Collate Latin1_general_ci_ai
         And X.compatibility_level = <compatLevel>
         And X.user_access_desc = "MULTI_USER"
         And X.state_desc = "ONLINE"      
      Where Db.DbName = "<dbname>" 
      '
      Set @sql = REPLACE(@sql, '<dbname>', @DbName)
      Set @sql = REPLACE(@sql, '<mirrorserver>', @MirrorServer)  
      Set @sql = REPLACE(@sql, '<compatLevel>', 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

-- 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
-- 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
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
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
*/