From 0e07a764381ff4f8566274316b490a43971384d7 Mon Sep 17 00:00:00 2001 From: Joseph Doherty Date: Sat, 3 Jan 2026 10:56:05 -0500 Subject: [PATCH] feat(etl): add ParseTableName and QUOTENAME to CommonScripts - Add ParseTableName method to parse table names with optional schema - Supports: "Table", "dbo.Table", "[dbo].[Table]" - Returns (schema, table) tuple, defaults to "dbo" schema - Update DisableIndexes, RebuildIndexes, UpdateStatistics to: - Use QUOTENAME() for SQL injection protection - Pass schema and table as parameters via SqlScriptRunner - Support non-dbo schemas - Update CustomSql to accept optional parameters and timeout - Add comprehensive tests for ParseTableName with various formats --- .../Etl/Scripts/CommonScripts.cs | 76 +++++++++++++++---- .../Etl/Scripts/CommonScriptsTests.cs | 69 ++++++++++++++++- 2 files changed, 129 insertions(+), 16 deletions(-) diff --git a/NEW/src/JdeScoping.DataSync/Etl/Scripts/CommonScripts.cs b/NEW/src/JdeScoping.DataSync/Etl/Scripts/CommonScripts.cs index e3dcaa6..95f909d 100644 --- a/NEW/src/JdeScoping.DataSync/Etl/Scripts/CommonScripts.cs +++ b/NEW/src/JdeScoping.DataSync/Etl/Scripts/CommonScripts.cs @@ -5,35 +5,85 @@ namespace JdeScoping.DataSync.Etl.Scripts; public static class CommonScripts { - public static IScriptRunner DisableIndexes(IDbConnectionFactory factory, string tableName) + /// + /// Parses a table name, extracting schema if present. + /// Supports: "Table", "dbo.Table", "[dbo].[Table]" + /// + public static (string Schema, string Table) ParseTableName(string tableName) { - var sql = $@" + var cleaned = tableName.Replace("[", "").Replace("]", ""); + var parts = cleaned.Split('.', 2); + return parts.Length == 2 + ? (parts[0], parts[1]) + : ("dbo", parts[0]); + } + + public static IScriptRunner DisableIndexes( + IDbConnectionFactory factory, + string tableName, + int timeoutSeconds = 300) + { + var (schema, table) = ParseTableName(tableName); + + var sql = @" DECLARE @sql NVARCHAR(MAX) = ''; -SELECT @sql = @sql + 'ALTER INDEX [' + i.name + '] ON [{tableName}] DISABLE;' + CHAR(13) +DECLARE @fullTableName NVARCHAR(256) = QUOTENAME(@schemaName) + '.' + QUOTENAME(@tableName); + +SELECT @sql = @sql + 'ALTER INDEX ' + QUOTENAME(i.name) + ' ON ' + @fullTableName + ' DISABLE;' + CHAR(13) FROM sys.indexes i INNER JOIN sys.tables t ON i.object_id = t.object_id -WHERE t.name = '{tableName}' +INNER JOIN sys.schemas s ON t.schema_id = s.schema_id +WHERE t.name = @tableName + AND s.name = @schemaName AND i.type = 2 AND i.is_disabled = 0; + IF LEN(@sql) > 0 EXEC sp_executesql @sql;"; - return new SqlScriptRunner(factory, sql, $"DisableIndexes:{tableName}", timeoutSeconds: 300); + return new SqlScriptRunner(factory, sql, $"DisableIndexes:{schema}.{table}", + parameters: new { tableName = table, schemaName = schema }, + timeoutSeconds: timeoutSeconds); } - public static IScriptRunner RebuildIndexes(IDbConnectionFactory factory, string tableName) + public static IScriptRunner RebuildIndexes( + IDbConnectionFactory factory, + string tableName, + int timeoutSeconds = 3600) { - var sql = $"ALTER INDEX ALL ON [{tableName}] REBUILD WITH (FILLFACTOR = 95)"; - return new SqlScriptRunner(factory, sql, $"RebuildIndexes:{tableName}", timeoutSeconds: 3600); + var (schema, table) = ParseTableName(tableName); + + var sql = @" +DECLARE @sql NVARCHAR(256) = 'ALTER INDEX ALL ON ' + QUOTENAME(@schemaName) + '.' + QUOTENAME(@tableName) + ' REBUILD WITH (FILLFACTOR = 95)'; +EXEC sp_executesql @sql;"; + + return new SqlScriptRunner(factory, sql, $"RebuildIndexes:{schema}.{table}", + parameters: new { tableName = table, schemaName = schema }, + timeoutSeconds: timeoutSeconds); } - public static IScriptRunner UpdateStatistics(IDbConnectionFactory factory, string tableName) + public static IScriptRunner UpdateStatistics( + IDbConnectionFactory factory, + string tableName, + int timeoutSeconds = 600) { - var sql = $"UPDATE STATISTICS [{tableName}]"; - return new SqlScriptRunner(factory, sql, $"UpdateStats:{tableName}", timeoutSeconds: 600); + var (schema, table) = ParseTableName(tableName); + + var sql = @" +DECLARE @sql NVARCHAR(256) = 'UPDATE STATISTICS ' + QUOTENAME(@schemaName) + '.' + QUOTENAME(@tableName); +EXEC sp_executesql @sql;"; + + return new SqlScriptRunner(factory, sql, $"UpdateStats:{schema}.{table}", + parameters: new { tableName = table, schemaName = schema }, + timeoutSeconds: timeoutSeconds); } - public static IScriptRunner CustomSql(IDbConnectionFactory factory, string sql, string name) + public static IScriptRunner CustomSql( + IDbConnectionFactory factory, + string sql, + string name, + object? parameters = null, + int timeoutSeconds = 30) { - return new SqlScriptRunner(factory, sql, name); + return new SqlScriptRunner(factory, sql, name, parameters, timeoutSeconds); } } diff --git a/NEW/tests/JdeScoping.DataSync.Tests/Etl/Scripts/CommonScriptsTests.cs b/NEW/tests/JdeScoping.DataSync.Tests/Etl/Scripts/CommonScriptsTests.cs index 18f6721..0315fca 100644 --- a/NEW/tests/JdeScoping.DataSync.Tests/Etl/Scripts/CommonScriptsTests.cs +++ b/NEW/tests/JdeScoping.DataSync.Tests/Etl/Scripts/CommonScriptsTests.cs @@ -8,25 +8,71 @@ public class CommonScriptsTests { private readonly IDbConnectionFactory _factory = Substitute.For(); + [Theory] + [InlineData("WorkOrder", "dbo", "WorkOrder")] + [InlineData("dbo.WorkOrder", "dbo", "WorkOrder")] + [InlineData("[dbo].[WorkOrder]", "dbo", "WorkOrder")] + [InlineData("Config.Settings", "Config", "Settings")] + [InlineData("[Config].[Settings]", "Config", "Settings")] + [InlineData("myschema.MyTable", "myschema", "MyTable")] + [InlineData("[other].[table.with.dots]", "other", "table.with.dots")] + public void ParseTableName_VariousFormats_ParsesCorrectly(string input, string expectedSchema, string expectedTable) + { + // Act + var (schema, table) = CommonScripts.ParseTableName(input); + + // Assert + Assert.Equal(expectedSchema, schema); + Assert.Equal(expectedTable, table); + } + [Fact] public void DisableIndexes_ReturnsRunnerWithCorrectName() { var runner = CommonScripts.DisableIndexes(_factory, "WorkOrder"); - Assert.Equal("DisableIndexes:WorkOrder", runner.ScriptName); + Assert.Equal("DisableIndexes:dbo.WorkOrder", runner.ScriptName); + } + + [Fact] + public void DisableIndexes_WithSchema_ReturnsRunnerWithCorrectName() + { + var runner = CommonScripts.DisableIndexes(_factory, "Config.Settings"); + Assert.Equal("DisableIndexes:Config.Settings", runner.ScriptName); + } + + [Fact] + public void DisableIndexes_WithBracketedSchema_ReturnsRunnerWithCorrectName() + { + var runner = CommonScripts.DisableIndexes(_factory, "[dbo].[WorkOrder]"); + Assert.Equal("DisableIndexes:dbo.WorkOrder", runner.ScriptName); } [Fact] public void RebuildIndexes_ReturnsRunnerWithCorrectName() { var runner = CommonScripts.RebuildIndexes(_factory, "WorkOrder"); - Assert.Equal("RebuildIndexes:WorkOrder", runner.ScriptName); + Assert.Equal("RebuildIndexes:dbo.WorkOrder", runner.ScriptName); + } + + [Fact] + public void RebuildIndexes_WithSchema_ReturnsRunnerWithCorrectName() + { + var runner = CommonScripts.RebuildIndexes(_factory, "Config.Settings"); + Assert.Equal("RebuildIndexes:Config.Settings", runner.ScriptName); } [Fact] public void UpdateStatistics_ReturnsRunnerWithCorrectName() { var runner = CommonScripts.UpdateStatistics(_factory, "WorkOrder"); - Assert.Equal("UpdateStats:WorkOrder", runner.ScriptName); + Assert.Equal("UpdateStats:dbo.WorkOrder", runner.ScriptName); + } + + [Fact] + public void UpdateStatistics_WithSchema_ReturnsRunnerWithCorrectName() + { + var runner = CommonScripts.UpdateStatistics(_factory, "Config.Settings"); + Assert.Equal("UpdateStats:Config.Settings", runner.ScriptName); } [Fact] @@ -35,4 +81,21 @@ public class CommonScriptsTests var runner = CommonScripts.CustomSql(_factory, "SELECT 1", "MyCustomScript"); Assert.Equal("MyCustomScript", runner.ScriptName); } + + [Fact] + public void CustomSql_WithParameters_ReturnsRunner() + { + var parameters = new { value = 42 }; + var runner = CommonScripts.CustomSql(_factory, "SELECT @value", "ParameterizedScript", parameters); + Assert.Equal("ParameterizedScript", runner.ScriptName); + Assert.NotNull(runner); + } + + [Fact] + public void CustomSql_WithTimeout_ReturnsRunner() + { + var runner = CommonScripts.CustomSql(_factory, "SELECT 1", "TimeoutScript", timeoutSeconds: 120); + Assert.Equal("TimeoutScript", runner.ScriptName); + Assert.NotNull(runner); + } }