-- ============================================================================ -- DATABASE CHANGES - Consolidated Script -- Date: 30 November 2025 -- Purpose: Performance optimization for AquaBarrier project management API -- ============================================================================ -- ============================================================================ -- PART 1: ADD TIMESTAMP COLUMNS TO PROJECTS TABLE -- ============================================================================ -- Purpose: Support dashboard metrics and track record lifecycle -- Impact: Enables "New Projects" filtering in dashboard -- ============================================================================ -- Add CreatedDate column (if not exists) IF NOT EXISTS (SELECT 1 FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_NAME = 'Projects' AND COLUMN_NAME = 'CreatedDate') BEGIN ALTER TABLE [dbo].[Projects] ADD [CreatedDate] [datetime2](7) NULL DEFAULT (getutcdate()); PRINT 'Added CreatedDate column to Projects table'; END ELSE PRINT 'CreatedDate already exists in Projects table'; GO -- Add ModifiedDate column (if not exists) IF NOT EXISTS (SELECT 1 FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_NAME = 'Projects' AND COLUMN_NAME = 'ModifiedDate') BEGIN ALTER TABLE [dbo].[Projects] ADD [ModifiedDate] [datetime2](7) NULL DEFAULT (getutcdate()); PRINT 'Added ModifiedDate column to Projects table'; END ELSE PRINT 'ModifiedDate already exists in Projects table'; GO -- Backfill CreatedDate with current date for existing records (if NULL) UPDATE [dbo].[Projects] SET [CreatedDate] = GETUTCDATE() WHERE [CreatedDate] IS NULL; PRINT 'Backfilled CreatedDate for ' + CAST(@@ROWCOUNT AS varchar) + ' existing Projects'; GO -- ============================================================================ -- PART 2: CREATE PERFORMANCE INDEXES -- ============================================================================ -- Purpose: Optimize query performance for project and customer endpoints -- Impact: Reduces query execution time from 5+ seconds to sub-second -- ============================================================================ -- Index for dashboard metrics filtering by CreatedDate IF NOT EXISTS (SELECT 1 FROM sys.indexes WHERE name = 'IX_Projects_CreatedDate' AND object_id = OBJECT_ID('dbo.Projects')) BEGIN CREATE NONCLUSTERED INDEX [IX_Projects_CreatedDate] ON [dbo].[Projects] ( [CreatedDate] ASC )WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, SORT_IN_TEMPDB = OFF, DROP_EXISTING = OFF, ONLINE = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON, OPTIMIZE_FOR_SEQUENTIAL_KEY = OFF) ON [PRIMARY]; PRINT 'Created index IX_Projects_CreatedDate'; END ELSE PRINT 'Index IX_Projects_CreatedDate already exists'; GO -- Index for Bidders ProjectNo lookups (if not exists) IF NOT EXISTS (SELECT 1 FROM sys.indexes WHERE name = 'IX_Bidders_ProjectNo' AND object_id = OBJECT_ID('dbo.Bidders')) BEGIN CREATE NONCLUSTERED INDEX [IX_Bidders_ProjectNo] ON [dbo].[Bidders] ( [ProjNo] ASC )WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, SORT_IN_TEMPDB = OFF, DROP_EXISTING = OFF, ONLINE = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON, OPTIMIZE_FOR_SEQUENTIAL_KEY = OFF) ON [PRIMARY]; PRINT 'Created index IX_Bidders_ProjectNo'; END ELSE PRINT 'Index IX_Bidders_ProjectNo already exists'; GO -- Index for Bidders Id (keyset pagination) IF NOT EXISTS (SELECT 1 FROM sys.indexes WHERE name = 'IX_Bidders_Id' AND object_id = OBJECT_ID('dbo.Bidders')) BEGIN CREATE NONCLUSTERED INDEX [IX_Bidders_Id] ON [dbo].[Bidders] ( [Id] ASC )WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, SORT_IN_TEMPDB = OFF, DROP_EXISTING = OFF, ONLINE = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON, OPTIMIZE_FOR_SEQUENTIAL_KEY = OFF) ON [PRIMARY]; PRINT 'Created index IX_Bidders_Id'; END ELSE PRINT 'Index IX_Bidders_Id already exists'; GO -- Index for Bidders CustId lookups IF NOT EXISTS (SELECT 1 FROM sys.indexes WHERE name = 'IX_Bidders_CustId' AND object_id = OBJECT_ID('dbo.Bidders')) BEGIN CREATE NONCLUSTERED INDEX [IX_Bidders_CustId] ON [dbo].[Bidders] ( [CustId] ASC )WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, SORT_IN_TEMPDB = OFF, DROP_EXISTING = OFF, ONLINE = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON, OPTIMIZE_FOR_SEQUENTIAL_KEY = OFF) ON [PRIMARY]; PRINT 'Created index IX_Bidders_CustId'; END ELSE PRINT 'Index IX_Bidders_CustId already exists'; GO -- Index for Customers CustomerID lookups IF EXISTS (SELECT 1 FROM sys.indexes WHERE name = 'IX_Customers_CustomerID' AND object_id = OBJECT_ID('dbo.Customers')) BEGIN DROP INDEX [IX_Customers_CustomerID] ON [dbo].[Customers]; PRINT 'Dropped existing IX_Customers_CustomerID to recreate with INCLUDE(CompanyName)'; END GO CREATE NONCLUSTERED INDEX [IX_Customers_CustomerID] ON [dbo].[Customers] ( [CustomerID] ASC ) INCLUDE ([CompanyName]) WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, SORT_IN_TEMPDB = OFF, DROP_EXISTING = OFF, ONLINE = ON, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON, OPTIMIZE_FOR_SEQUENTIAL_KEY = OFF) ON [PRIMARY]; PRINT 'Created index IX_Customers_CustomerID with INCLUDE(CompanyName)'; GO -- Composite index to optimize keyset pagination and project bidders fetch IF NOT EXISTS (SELECT 1 FROM sys.indexes WHERE name = 'IX_Bidders_ProjNo_Id' AND object_id = OBJECT_ID('dbo.Bidders')) BEGIN CREATE NONCLUSTERED INDEX [IX_Bidders_ProjNo_Id] ON [dbo].[Bidders] ( [ProjNo] ASC, [Id] ASC ) INCLUDE ([CustId]) WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, SORT_IN_TEMPDB = OFF, DROP_EXISTING = OFF, ONLINE = ON, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON, OPTIMIZE_FOR_SEQUENTIAL_KEY = OFF) ON [PRIMARY]; PRINT 'Created index IX_Bidders_ProjNo_Id'; END ELSE PRINT 'Index IX_Bidders_ProjNo_Id already exists'; GO -- Composite index to support ORDER BY [Primary] DESC, Id for offset pagination IF NOT EXISTS (SELECT 1 FROM sys.indexes WHERE name = 'IX_Bidders_ProjNo_Primary_Id' AND object_id = OBJECT_ID('dbo.Bidders')) BEGIN CREATE NONCLUSTERED INDEX [IX_Bidders_ProjNo_Primary_Id] ON [dbo].[Bidders] ( [ProjNo] ASC, [Primary] DESC, [Id] ASC ) INCLUDE ( [CustId], [Quote], [DateLastContact], [DateFollowup], [CustType], [EmailAddress], [ReplacementCost], [Enabled] ) WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, SORT_IN_TEMPDB = OFF, DROP_EXISTING = OFF, ONLINE = ON, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON, OPTIMIZE_FOR_SEQUENTIAL_KEY = OFF) ON [PRIMARY]; PRINT 'Created index IX_Bidders_ProjNo_Primary_Id'; END ELSE PRINT 'Index IX_Bidders_ProjNo_Primary_Id already exists'; GO -- ============================================================================ -- PART 3: OPTIONAL - MIGRATE Bidders.CustId FROM nvarchar(15) TO int -- ============================================================================ -- Purpose: Eliminate CAST() in JOIN clause for better index usage -- Impact: Removes type conversion overhead, allows index seek on Customers -- WARNING: This is a breaking schema change - test thoroughly before applying -- Status: OPTIONAL - Only apply if type mismatch is causing performance issues -- ============================================================================ /* -- Uncomment this section to perform the migration -- Step 1: Add new int column IF NOT EXISTS (SELECT 1 FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_NAME = 'Bidders' AND COLUMN_NAME = 'CustId_New') BEGIN ALTER TABLE [dbo].[Bidders] ADD [CustId_New] [int] NULL; PRINT 'Added CustId_New column'; END GO -- Step 2: Migrate data UPDATE [dbo].[Bidders] SET [CustId_New] = CAST([CustId] AS int) WHERE ISNUMERIC([CustId]) = 1 AND [CustId_New] IS NULL; PRINT 'Migrated ' + CAST(@@ROWCOUNT AS varchar) + ' rows to CustId_New'; GO -- Step 3: Drop index on old column IF EXISTS (SELECT 1 FROM sys.indexes WHERE name = 'IX_Bidders_CustId' AND object_id = OBJECT_ID('dbo.Bidders')) BEGIN DROP INDEX [IX_Bidders_CustId] ON [dbo].[Bidders]; PRINT 'Dropped index IX_Bidders_CustId'; END GO -- Step 4: Drop old nvarchar column IF EXISTS (SELECT 1 FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_NAME = 'Bidders' AND COLUMN_NAME = 'CustId' AND DATA_TYPE = 'nvarchar') BEGIN ALTER TABLE [dbo].[Bidders] DROP COLUMN [CustId]; PRINT 'Dropped old nvarchar CustId column'; END GO -- Step 5: Rename new column IF EXISTS (SELECT 1 FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_NAME = 'Bidders' AND COLUMN_NAME = 'CustId_New') AND NOT EXISTS (SELECT 1 FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_NAME = 'Bidders' AND COLUMN_NAME = 'CustId') BEGIN EXEC sp_rename 'dbo.Bidders.CustId_New', 'CustId', 'COLUMN'; PRINT 'Renamed CustId_New to CustId'; END GO -- Step 6: Set NOT NULL constraint IF EXISTS (SELECT 1 FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_NAME = 'Bidders' AND COLUMN_NAME = 'CustId' AND DATA_TYPE = 'int' AND IS_NULLABLE = 'YES') BEGIN IF NOT EXISTS (SELECT 1 FROM [dbo].[Bidders] WHERE [CustId] IS NULL) BEGIN ALTER TABLE [dbo].[Bidders] ALTER COLUMN [CustId] [int] NOT NULL; PRINT 'Set CustId to NOT NULL'; END ELSE BEGIN PRINT 'WARNING: NULL values exist in CustId, cannot set NOT NULL'; END END GO -- Step 7: Recreate index on new int column IF NOT EXISTS (SELECT 1 FROM sys.indexes WHERE name = 'IX_Bidders_CustId' AND object_id = OBJECT_ID('dbo.Bidders')) BEGIN CREATE NONCLUSTERED INDEX [IX_Bidders_CustId] ON [dbo].[Bidders] ( [CustId] ASC )WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, SORT_IN_TEMPDB = OFF, DROP_EXISTING = OFF, ONLINE = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON, OPTIMIZE_FOR_SEQUENTIAL_KEY = OFF) ON [PRIMARY]; PRINT 'Recreated index IX_Bidders_CustId on int column'; END GO */ -- ============================================================================ -- VERIFICATION QUERIES -- ============================================================================ PRINT ''; PRINT '============================================================================'; PRINT 'VERIFICATION RESULTS'; PRINT '============================================================================'; -- Verify Projects timestamp columns SELECT 'Projects Timestamp Columns' AS CheckType; SELECT COLUMN_NAME, DATA_TYPE, IS_NULLABLE, COLUMN_DEFAULT FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_NAME = 'Projects' AND COLUMN_NAME IN ('CreatedDate', 'ModifiedDate') ORDER BY COLUMN_NAME; -- Verify all indexes exist SELECT 'Performance Indexes' AS CheckType; SELECT t.name AS TableName, i.name AS IndexName, c.name AS ColumnName, ty.name AS DataType FROM sys.indexes i JOIN sys.index_columns ic ON i.object_id = ic.object_id AND i.index_id = ic.index_id JOIN sys.columns c ON ic.object_id = c.object_id AND ic.column_id = c.column_id JOIN sys.types ty ON c.user_type_id = ty.user_type_id JOIN sys.tables t ON i.object_id = t.object_id WHERE i.name IN ( 'IX_Projects_CreatedDate', 'IX_Bidders_ProjectNo', 'IX_Bidders_Id', 'IX_Bidders_CustId', 'IX_Customers_CustomerID' ) ORDER BY t.name, i.name; -- Verify Bidders.CustId data type SELECT 'Bidders.CustId Data Type' AS CheckType; SELECT COLUMN_NAME, DATA_TYPE, CHARACTER_MAXIMUM_LENGTH, IS_NULLABLE FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_NAME = 'Bidders' AND COLUMN_NAME = 'CustId'; PRINT ''; PRINT '============================================================================'; PRINT 'DATABASE CHANGES COMPLETE'; PRINT '============================================================================'; GO