diff --git a/DigitalLearningSolutions.Data.Migrations/202506110906_AddSqlMaintenanceSolution.cs b/DigitalLearningSolutions.Data.Migrations/202506110906_AddSqlMaintenanceSolution.cs
new file mode 100644
index 0000000000..86b2026fc4
--- /dev/null
+++ b/DigitalLearningSolutions.Data.Migrations/202506110906_AddSqlMaintenanceSolution.cs
@@ -0,0 +1,17 @@
+namespace DigitalLearningSolutions.Data.Migrations
+{
+ using FluentMigrator;
+ [Migration(202506110906)]
+ public class AddSqlMaintenanceSolution : Migration
+ {
+
+ public override void Up()
+ {
+ Execute.Sql(Properties.Resources.TD_5670_MaintenanceScripts_UP);
+ }
+ public override void Down()
+ {
+ Execute.Sql(Properties.Resources.TD_5670_MaintenanceScripts_DOWN);
+ }
+ }
+}
diff --git a/DigitalLearningSolutions.Data.Migrations/Properties/Resources.Designer.cs b/DigitalLearningSolutions.Data.Migrations/Properties/Resources.Designer.cs
index 4bd193dcf1..0f99da6fea 100644
--- a/DigitalLearningSolutions.Data.Migrations/Properties/Resources.Designer.cs
+++ b/DigitalLearningSolutions.Data.Migrations/Properties/Resources.Designer.cs
@@ -478,7 +478,8 @@ internal static string DLSV2_272_AlterGetLinkedFieldNameFunction_UP {
///-- Create date: 15/10/2021
///-- Description: Reorders the CompetencyAssessmentQuestions - moving the given competency question up or down.
///-- =============================================
- ///CREATE OR ALTER PROCEDURE [dbo].[ReorderCompetencyAssessmentQuestion]
/// [rest of string was truncated]";.
+ ///CREATE OR ALTER PROCEDURE [dbo].[ReorderCompetencyAssessmentQuestion]
+ /// [rest of string was truncated]";.
///
internal static string DLSV2_379_ReorderCompetencyAssessmentQuestionsSP {
get {
@@ -1504,7 +1505,8 @@ internal static string TD_3190_SendOneMonthSelfAssessmentOverdueRemindersSP {
/// @EmailProfileName nvarchar(100),
/// @TestOnly bit
///AS
- ///BEGIN
/// [rest of string was truncated]";.
+ ///BEGIN
+ /// [rest of string was truncated]";.
///
internal static string TD_3190_SendOneMonthSelfAssessmentTBCRemindersSP {
get {
@@ -2525,6 +2527,16 @@ internal static string TD_5514_Alter_SendExpiredTBCReminders_Up {
}
///
+ /// Looks up a localized string similar to IF OBJECT_ID('dbo.IndexOptimize', 'P') IS NOT NULL DROP PROCEDURE dbo.IndexOptimize;
+ ///IF OBJECT_ID('dbo.CommandExecute', 'P') IS NOT NULL DROP PROCEDURE dbo.CommandExecute;
+ ///IF OBJECT_ID('dbo.CommandLog', 'U') IS NOT NULL DROP TABLE dbo.CommandLog;
+ ///.
+ ///
+ internal static string TD_5670_MaintenanceScripts_DOWN {
+ get {
+ return ResourceManager.GetString("TD-5670-MaintenanceScripts_DOWN", resourceCulture);
+ }
+ }
/// Looks up a localized string similar to CREATE OR ALTER PROCEDURE [dbo].[usp_GetSelfAssessmentReport]
/// @SelfAssessmentID INT,
/// @CentreID INT
@@ -2549,6 +2561,27 @@ internal static string TD_5759_CreateOrAlterSelfAssessmentReportSPandTVF_Fix_UP
}
///
+ /// Looks up a localized string similar to -- ============================================
+ ///-- CommandLog table
+ ///-- ============================================
+ ///IF OBJECT_ID('dbo.CommandLog', 'U') IS NOT NULL DROP TABLE dbo.CommandLog;
+ ///CREATE TABLE dbo.CommandLog (
+ /// ID INT IDENTITY PRIMARY KEY,
+ /// DatabaseName SYSNAME NULL,
+ /// SchemaName SYSNAME NULL,
+ /// ObjectName SYSNAME NULL,
+ /// ObjectType CHAR(2) NULL,
+ /// IndexName SYSNAME NULL,
+ /// IndexType TINYINT NULL,
+ /// StatisticsName SYSNAME NULL,
+ /// PartitionNumber INT NULL,
+ /// Ext [rest of string was truncated]";.
+ ///
+ internal static string TD_5670_MaintenanceScripts_UP {
+ get {
+ return ResourceManager.GetString("TD_5670_MaintenanceScripts_UP", resourceCulture);
+ }
+ }
/// Looks up a localized string similar to CREATE OR ALTER FUNCTION dbo.GetOtherCentresForSelfAssessmentTVF
///(
/// @UserID INT,
diff --git a/DigitalLearningSolutions.Data.Migrations/Properties/Resources.resx b/DigitalLearningSolutions.Data.Migrations/Properties/Resources.resx
index d6a79ed9d3..55d97e6ce3 100644
--- a/DigitalLearningSolutions.Data.Migrations/Properties/Resources.resx
+++ b/DigitalLearningSolutions.Data.Migrations/Properties/Resources.resx
@@ -487,6 +487,12 @@
..\Scripts\TD-5447-Alter_ReorderFrameworkCompetency_Up.sql;System.String, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089;utf-16
+
+ ..\Scripts\TD-5670-MaintenanceScripts_UP.sql;System.String, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089;utf-8
+
+
+ ..\Scripts\TD-5670-MaintenanceScripts_DOWN.sql;System.String, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089;utf-8
+
..\Scripts\TD-5759_CreateOrAlterSelfAssessmentReportSPandTVF_UP.sql;System.String, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089;utf-8
diff --git a/DigitalLearningSolutions.Data.Migrations/Scripts/TD-5670-MaintenanceScripts_DOWN.sql b/DigitalLearningSolutions.Data.Migrations/Scripts/TD-5670-MaintenanceScripts_DOWN.sql
new file mode 100644
index 0000000000..14a45533ad
--- /dev/null
+++ b/DigitalLearningSolutions.Data.Migrations/Scripts/TD-5670-MaintenanceScripts_DOWN.sql
@@ -0,0 +1,4 @@
+IF OBJECT_ID('dbo.IndexOptimize', 'P') IS NOT NULL DROP PROCEDURE dbo.IndexOptimize;
+IF OBJECT_ID('dbo.CommandExecute', 'P') IS NOT NULL DROP PROCEDURE dbo.CommandExecute;
+IF OBJECT_ID('dbo.sp_purge_commandlog', 'P') IS NOT NULL DROP PROCEDURE dbo.sp_purge_commandlog;
+IF OBJECT_ID('dbo.CommandLog', 'U') IS NOT NULL DROP TABLE dbo.CommandLog;
diff --git a/DigitalLearningSolutions.Data.Migrations/Scripts/TD-5670-MaintenanceScripts_UP.sql b/DigitalLearningSolutions.Data.Migrations/Scripts/TD-5670-MaintenanceScripts_UP.sql
new file mode 100644
index 0000000000..ac8258b6fa
--- /dev/null
+++ b/DigitalLearningSolutions.Data.Migrations/Scripts/TD-5670-MaintenanceScripts_UP.sql
@@ -0,0 +1,204 @@
+-- ============================================
+-- Drop if exists (for clean redeploy)
+-- ============================================
+IF OBJECT_ID('dbo.IndexOptimize', 'P') IS NOT NULL DROP PROCEDURE dbo.IndexOptimize;
+IF OBJECT_ID('dbo.DatabaseIntegrityCheck', 'P') IS NOT NULL DROP PROCEDURE dbo.DatabaseIntegrityCheck;
+IF OBJECT_ID('dbo.CommandExecute', 'P') IS NOT NULL DROP PROCEDURE dbo.CommandExecute;
+IF OBJECT_ID('dbo.CommandLog', 'U') IS NOT NULL DROP TABLE dbo.CommandLog;
+GO
+
+-- ============================================
+-- CommandLog table
+-- ============================================
+CREATE TABLE dbo.CommandLog (
+ ID INT IDENTITY PRIMARY KEY,
+ DatabaseName SYSNAME NULL,
+ SchemaName SYSNAME NULL,
+ ObjectName SYSNAME NULL,
+ ObjectType CHAR(2) NULL,
+ IndexName SYSNAME NULL,
+ IndexType TINYINT NULL,
+ StatisticsName SYSNAME NULL,
+ PartitionNumber INT NULL,
+ ExtendedInfo XML NULL,
+ Command NVARCHAR(MAX) NOT NULL,
+ CommandType NVARCHAR(60) NOT NULL,
+ StartTime DATETIME NOT NULL,
+ EndTime DATETIME NOT NULL,
+ ErrorNumber INT NOT NULL,
+ ErrorMessage NVARCHAR(MAX) NULL
+);
+GO
+
+-- ============================================
+-- CommandExecute stored procedure
+-- ============================================
+CREATE PROCEDURE dbo.CommandExecute
+ @Command NVARCHAR(MAX),
+ @CommandType NVARCHAR(60),
+ @DatabaseName SYSNAME = NULL,
+ @SchemaName SYSNAME = NULL,
+ @ObjectName SYSNAME = NULL,
+ @ObjectType CHAR(2) = NULL,
+ @IndexName SYSNAME = NULL,
+ @IndexType TINYINT = NULL,
+ @StatisticsName SYSNAME = NULL,
+ @PartitionNumber INT = NULL,
+ @ExtendedInfo XML = NULL
+AS
+BEGIN
+ SET NOCOUNT ON;
+
+ DECLARE @StartTime DATETIME = GETDATE();
+ DECLARE @ErrorNumber INT = 0;
+ DECLARE @ErrorMessage NVARCHAR(MAX) = NULL;
+
+ BEGIN TRY
+ EXEC (@Command);
+ END TRY
+ BEGIN CATCH
+ SET @ErrorNumber = ERROR_NUMBER();
+ SET @ErrorMessage = ERROR_MESSAGE();
+ END CATCH;
+
+ INSERT INTO dbo.CommandLog (
+ DatabaseName, SchemaName, ObjectName, ObjectType, IndexName, IndexType, StatisticsName,
+ PartitionNumber, ExtendedInfo, Command, CommandType, StartTime, EndTime, ErrorNumber, ErrorMessage
+ )
+ VALUES (
+ @DatabaseName, @SchemaName, @ObjectName, @ObjectType, @IndexName, @IndexType, @StatisticsName,
+ @PartitionNumber, @ExtendedInfo, @Command, @CommandType, @StartTime, GETDATE(), @ErrorNumber, @ErrorMessage
+ );
+
+ IF @ErrorNumber <> 0
+ RAISERROR(@ErrorMessage, 16, 1);
+END
+GO
+
+-- ============================================
+-- IndexOptimize stored procedure
+-- ============================================
+CREATE PROCEDURE dbo.IndexOptimize
+ @Databases NVARCHAR(MAX) = 'USER_DATABASES',
+ @FragmentationMedium TINYINT = 30,
+ @FragmentationHigh TINYINT = 70
+AS
+BEGIN
+ SET NOCOUNT ON;
+
+ DECLARE @db SYSNAME;
+ DECLARE db_cursor CURSOR FOR
+ SELECT name FROM sys.databases
+ WHERE (@Databases = 'USER_DATABASES' AND database_id > 4)
+ OR name = @Databases;
+
+ OPEN db_cursor;
+ FETCH NEXT FROM db_cursor INTO @db;
+
+ WHILE @@FETCH_STATUS = 0
+ BEGIN
+ DECLARE @sql NVARCHAR(MAX) = N'
+ USE [' + @db + '];
+
+ DECLARE @schema SYSNAME, @table SYSNAME, @index SYSNAME;
+ DECLARE @index_id INT, @frag FLOAT;
+
+ DECLARE c CURSOR FOR
+ SELECT s.name, t.name, i.name, i.index_id, ips.avg_fragmentation_in_percent
+ FROM sys.dm_db_index_physical_stats(DB_ID(), NULL, NULL, NULL, ''LIMITED'') ips
+ JOIN sys.indexes i ON i.object_id = ips.object_id AND i.index_id = ips.index_id
+ JOIN sys.tables t ON t.object_id = ips.object_id
+ JOIN sys.schemas s ON s.schema_id = t.schema_id
+ WHERE ips.index_id > 0 AND ips.page_count > 100;
+
+ OPEN c;
+ FETCH NEXT FROM c INTO @schema, @table, @index, @index_id, @frag;
+
+ WHILE @@FETCH_STATUS = 0
+ BEGIN
+ DECLARE @cmd NVARCHAR(MAX);
+ SET @cmd = ''ALTER INDEX ['' + @index + ''] ON ['' + @schema + ''].['' + @table + ''] '';
+
+ IF @frag >= ' + CAST(@FragmentationHigh AS NVARCHAR) + '
+ SET @cmd += ''REBUILD'';
+ ELSE IF @frag >= ' + CAST(@FragmentationMedium AS NVARCHAR) + '
+ SET @cmd += ''REORGANIZE'';
+ ELSE
+ SET @cmd = NULL;
+
+ IF @cmd IS NOT NULL
+ EXEC dbo.CommandExecute @Command = @cmd,
+ @CommandType = ''ALTER INDEX'',
+ @DatabaseName = ''' + @db + ''',
+ @SchemaName = @schema,
+ @ObjectName = @table,
+ @ObjectType = ''U'',
+ @IndexName = @index;
+
+ FETCH NEXT FROM c INTO @schema, @table, @index, @index_id, @frag;
+ END;
+
+ CLOSE c;
+ DEALLOCATE c;
+ ';
+
+ EXEC sp_executesql @sql;
+ FETCH NEXT FROM db_cursor INTO @db;
+ END;
+
+ CLOSE db_cursor;
+ DEALLOCATE db_cursor;
+END
+GO
+
+-- ============================================
+-- DatabaseIntegrityCheck stored procedure
+-- ============================================
+CREATE PROCEDURE dbo.DatabaseIntegrityCheck
+ @Databases NVARCHAR(MAX) = 'USER_DATABASES'
+AS
+BEGIN
+ SET NOCOUNT ON;
+
+ DECLARE @db SYSNAME;
+ DECLARE db_cursor CURSOR FOR
+ SELECT name FROM sys.databases
+ WHERE (@Databases = 'USER_DATABASES' AND database_id > 4)
+ OR name = @Databases;
+
+ OPEN db_cursor;
+ FETCH NEXT FROM db_cursor INTO @db;
+
+ WHILE @@FETCH_STATUS = 0
+ BEGIN
+ DECLARE @cmd NVARCHAR(MAX);
+ SET @cmd = 'DBCC CHECKDB([' + @db + ']) WITH NO_INFOMSGS, ALL_ERRORMSGS';
+
+ EXEC dbo.CommandExecute
+ @Command = @cmd,
+ @CommandType = 'DBCC CHECKDB',
+ @DatabaseName = @db;
+
+ FETCH NEXT FROM db_cursor INTO @db;
+ END
+
+ CLOSE db_cursor;
+ DEALLOCATE db_cursor;
+END
+GO
+
+-- ============================================
+-- Purge command log stored procedure
+-- ============================================
+CREATE OR ALTER PROCEDURE dbo.sp_purge_commandlog
+ @DaysToKeep INT = 30
+AS
+BEGIN
+ SET NOCOUNT ON;
+
+ DECLARE @DeleteBefore DATETIME = DATEADD(DAY, -@DaysToKeep, GETDATE());
+
+ DELETE FROM dbo.CommandLog
+ WHERE StartTime < @DeleteBefore;
+END
+GO