diff --git a/Strata.SqlTools.QueryBreakdown.sln b/Strata.SqlTools.QueryBreakdown.sln index e468eba..b1f9f08 100644 --- a/Strata.SqlTools.QueryBreakdown.sln +++ b/Strata.SqlTools.QueryBreakdown.sln @@ -41,6 +41,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Strata.SqlTools.SqlBreakdow EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Strata.SqlTools.Query", "src\Strata.SqlTools.Query\Strata.SqlTools.Query.csproj", "{177F3799-6A91-477B-87CC-BCCE05B87E1C}" EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Tests.Integration", "Tests.Integration", "{CFCF0678-C587-40E9-B0AC-A7082AEBF206}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU diff --git a/tests/Strata.SqlTools.Snowflake.Tests/Snowflake/DeleteBreakdownTests.cs b/tests/Strata.SqlTools.Snowflake.Tests/Snowflake/DeleteBreakdownTests.cs new file mode 100644 index 0000000..9a9c97c --- /dev/null +++ b/tests/Strata.SqlTools.Snowflake.Tests/Snowflake/DeleteBreakdownTests.cs @@ -0,0 +1,172 @@ +using Strata.SqlTools.Breakdowns.Snowflake; + +namespace Strata.SqlTools.SqlBreakdown.Tests.Snowflake; + +[TestFixture] +public class DeleteBreakdownTests +{ + #region Constructor Tests + + [Test] + public void Constructor_Default_InitializesEmptyClauses() + { + // Arrange & Act + var breakdown = new DeleteBreakdown(); + + Assert.Multiple(() => + { + // Assert + Assert.That(breakdown.FromClause.Clause, Is.EqualTo(string.Empty)); + Assert.That(breakdown.WhereClause.Clause, Is.EqualTo(string.Empty)); + Assert.That(breakdown.IsUsingWhereClause, Is.False); + }); + } + + [Test] + public void Constructor_WithClauses_SetsProperties() + { + // Arrange & Act + var breakdown = new DeleteBreakdown("USERS", "ID = :ID"); + + Assert.Multiple(() => + { + // Assert + Assert.That(breakdown.FromClause.Clause, Is.EqualTo("USERS")); + Assert.That(breakdown.WhereClause.Clause, Is.EqualTo("ID = :ID")); + Assert.That(breakdown.IsUsingWhereClause, Is.True); + }); + } + + #endregion + + #region GetSql Tests + + [Test] + public void GetSql_WithWhere_RendersDeleteFromWhere() + { + // Arrange - Snowflake overrides the formatter with simplified DELETE FROM syntax + var breakdown = new DeleteBreakdown("USERS", "ID = :ID"); + + // Act + var sql = breakdown.GetSql(); + + Assert.Multiple(() => + { + // Assert + Assert.That(sql, Does.Contain("DELETE FROM")); + Assert.That(sql, Does.Contain("USERS")); + Assert.That(sql, Does.Contain("WHERE")); + Assert.That(sql, Does.Contain("ID = :ID")); + }); + } + + [Test] + public void GetSql_WithoutWhere_OmitsWhereKeyword() + { + // Arrange + var breakdown = new DeleteBreakdown("USERS", string.Empty); + + // Act + var sql = breakdown.GetSql(); + + Assert.Multiple(() => + { + // Assert + Assert.That(sql, Does.Contain("DELETE FROM")); + Assert.That(sql, Does.Not.Contain("WHERE")); + }); + } + + #endregion + + #region Parse Tests + + [Test] + public void Parse_SnowflakeDelete_PopulatesClauses() + { + // Arrange + var input = "DELETE FROM USERS WHERE ID = 1"; + + // Act + var breakdown = DeleteBreakdown.Parse(input); + + Assert.Multiple(() => + { + // Assert + Assert.That(breakdown.FromClause.Clause, Is.EqualTo("USERS")); + Assert.That(breakdown.WhereClause.Clause, Is.EqualTo("ID = 1")); + }); + } + + [Test] + public void Parse_DeleteWithoutWhere_LeavesWhereEmpty() + { + // Act + var breakdown = DeleteBreakdown.Parse("DELETE FROM USERS"); + + Assert.Multiple(() => + { + // Assert + Assert.That(breakdown.FromClause.Clause, Is.EqualTo("USERS")); + Assert.That(breakdown.IsUsingWhereClause, Is.False); + }); + } + + [Test] + public void Parse_MicrosoftSqlMode_DelegatesToBase() + { + // Act - T-SQL DELETE with alias is supported through the base breakdown + var breakdown = DeleteBreakdown.Parse("DELETE FROM Users WHERE Id = 1", isMicrosoftSql: true); + + Assert.Multiple(() => + { + // Assert + Assert.That(breakdown, Is.InstanceOf()); + Assert.That(breakdown.FromClause.Clause, Is.EqualTo("Users")); + }); + } + + [Test] + public void Parse_NullOrWhitespace_ThrowsArgumentNullException() + { + // Act & Assert + Assert.Throws(() => DeleteBreakdown.Parse(null!)); + } + + [Test] + public void Parse_NotADeleteStatement_ThrowsFormatException() + { + // Act & Assert + Assert.Throws(() => DeleteBreakdown.Parse("SELECT * FROM USERS")); + } + + [Test] + public void TryParse_MicrosoftSqlMode_ReturnsSnowflakeInstance() + { + // Act + var success = DeleteBreakdown.TryParse("DELETE FROM Users WHERE Id = 1", out var breakdown, isMicrosoftSql: true); + + Assert.Multiple(() => + { + // Assert + Assert.That(success, Is.True); + Assert.That(breakdown, Is.InstanceOf()); + }); + } + + [Test] + public void TryParse_WrongPrefix_ReturnsFalse() + { + // Act + var success = DeleteBreakdown.TryParse("UPDATE USERS SET ID = 1", out _, out var error); + + Assert.Multiple(() => + { + // Assert + Assert.That(success, Is.False); + Assert.That(error, Does.Contain("DELETE")); + }); + } + + #endregion +} diff --git a/tests/Strata.SqlTools.Snowflake.Tests/Snowflake/InsertBreakdownTests.cs b/tests/Strata.SqlTools.Snowflake.Tests/Snowflake/InsertBreakdownTests.cs new file mode 100644 index 0000000..62c9229 --- /dev/null +++ b/tests/Strata.SqlTools.Snowflake.Tests/Snowflake/InsertBreakdownTests.cs @@ -0,0 +1,151 @@ +using Strata.SqlTools.Breakdowns.Snowflake; + +namespace Strata.SqlTools.SqlBreakdown.Tests.Snowflake; + +[TestFixture] +public class InsertBreakdownTests +{ + #region Constructor Tests + + [Test] + public void Constructor_Default_InitializesEmptyClauses() + { + // Arrange & Act + var breakdown = new InsertBreakdown(); + + Assert.Multiple(() => + { + // Assert + Assert.That(breakdown.TableName.Clause, Is.EqualTo(string.Empty)); + Assert.That(breakdown.InsertIntoClause.Clause, Is.EqualTo(string.Empty)); + Assert.That(breakdown.ValuesClause.Clause, Is.EqualTo(string.Empty)); + }); + } + + [Test] + public void Constructor_WithClauses_SetsProperties() + { + // Arrange & Act + var breakdown = new InsertBreakdown("USERS", "ID, NAME", ":ID, :NAME"); + + Assert.Multiple(() => + { + // Assert + Assert.That(breakdown.TableName.Clause, Is.EqualTo("USERS")); + Assert.That(breakdown.InsertIntoClause.Clause, Is.EqualTo("ID, NAME")); + Assert.That(breakdown.ValuesClause.Clause, Is.EqualTo(":ID, :NAME")); + }); + } + + [Test] + public void Constructor_WithColumnNames_GeneratesColonPrefixedParameters() + { + // Arrange & Act - Snowflake uses :parameter syntax (vs SQL Server's @parameter) + var breakdown = new InsertBreakdown("USERS", new List { "ID", "NAME" }); + + Assert.Multiple(() => + { + // Assert + Assert.That(breakdown.InsertIntoClause.Clause, Is.EqualTo("[ID],[NAME]")); + Assert.That(breakdown.ValuesClause.Clause, Is.EqualTo(":ID,:NAME")); + }); + } + + #endregion + + #region Parse Tests + + [Test] + public void Parse_SnowflakeInsert_PopulatesClauses() + { + // Arrange + var input = "INSERT INTO USERS (ID, NAME) VALUES (1, 'BOB')"; + + // Act + var breakdown = InsertBreakdown.Parse(input); + + Assert.Multiple(() => + { + // Assert + Assert.That(breakdown.TableName.Clause, Is.EqualTo("USERS")); + Assert.That(breakdown.InsertIntoClause.Clause, Is.EqualTo("ID, NAME")); + Assert.That(breakdown.ValuesClause.Clause, Is.EqualTo("1, 'BOB'")); + }); + } + + [Test] + public void Parse_MicrosoftSqlMode_DelegatesToBaseAndSucceeds() + { + // Arrange + var input = "INSERT INTO Users (Id) VALUES (1)"; + + // Act + var breakdown = InsertBreakdown.Parse(input, isMicrosoftSql: true); + + Assert.Multiple(() => + { + // Assert + Assert.That(breakdown, Is.InstanceOf()); + Assert.That(breakdown.TableName.Clause, Is.EqualTo("Users")); + Assert.That(breakdown.InsertIntoClause.Clause, Is.EqualTo("Id")); + }); + } + + [Test] + public void Parse_NullOrWhitespace_ThrowsArgumentNullException() + { + // Act & Assert + Assert.Throws(() => InsertBreakdown.Parse(" ")); + } + + [Test] + public void Parse_NotAnInsertStatement_ThrowsFormatException() + { + // Act & Assert + Assert.Throws(() => InsertBreakdown.Parse("SELECT * FROM USERS")); + } + + [Test] + public void TryParse_ValidInsert_ReturnsTrue() + { + // Act + var success = InsertBreakdown.TryParse("INSERT INTO USERS (ID) VALUES (1)", out var breakdown); + + Assert.Multiple(() => + { + // Assert + Assert.That(success, Is.True); + Assert.That(breakdown.TableName.Clause, Is.EqualTo("USERS")); + }); + } + + [Test] + public void TryParse_MicrosoftSqlMode_ReturnsSnowflakeInstance() + { + // Act + var success = InsertBreakdown.TryParse("INSERT INTO Users (Id) VALUES (1)", out var breakdown, isMicrosoftSql: true); + + Assert.Multiple(() => + { + // Assert + Assert.That(success, Is.True); + Assert.That(breakdown, Is.InstanceOf()); + }); + } + + [Test] + public void TryParse_WrongPrefix_ReturnsFalse() + { + // Act + var success = InsertBreakdown.TryParse("DELETE FROM USERS", out _, out var error); + + Assert.Multiple(() => + { + // Assert + Assert.That(success, Is.False); + Assert.That(error, Does.Contain("INSERT INTO")); + }); + } + + #endregion +} diff --git a/tests/Strata.SqlTools.Snowflake.Tests/Snowflake/ProcedureBreakdownTests.cs b/tests/Strata.SqlTools.Snowflake.Tests/Snowflake/ProcedureBreakdownTests.cs new file mode 100644 index 0000000..f274dfb --- /dev/null +++ b/tests/Strata.SqlTools.Snowflake.Tests/Snowflake/ProcedureBreakdownTests.cs @@ -0,0 +1,193 @@ +using Strata.SqlTools.Breakdowns.Snowflake; + +namespace Strata.SqlTools.SqlBreakdown.Tests.Snowflake; + +[TestFixture] +public class ProcedureBreakdownTests +{ + #region Constructor Tests + + [Test] + public void Constructor_WithName_SetsProcedureName() + { + // Arrange & Act + var breakdown = new ProcedureBreakdown("MY_PROC"); + + Assert.Multiple(() => + { + // Assert + Assert.That(breakdown.ProcedureName.Clause, Is.EqualTo("MY_PROC")); + Assert.That(breakdown.IsUsingParameters, Is.False); + }); + } + + [Test] + public void Constructor_WithParameters_SetsParameters() + { + // Arrange + var parameters = new Dictionary { ["@ID"] = "1" }; + + // Act + var breakdown = new ProcedureBreakdown("MY_PROC", parameters); + + Assert.Multiple(() => + { + // Assert + Assert.That(breakdown.IsUsingParameters, Is.True); + Assert.That(breakdown.Parameters["@ID"], Is.EqualTo("1")); + }); + } + + #endregion + + #region GetSql Tests - Snowflake CALL Syntax + + [Test] + public void GetSql_WithoutParameters_RendersCallWithEmptyParens() + { + // Arrange - Snowflake uses CALL proc() rather than EXEC proc + var breakdown = new ProcedureBreakdown("MY_PROC"); + + // Act + var sql = breakdown.GetSql(); + + Assert.Multiple(() => + { + // Assert + Assert.That(sql, Does.Contain("CALL")); + Assert.That(sql, Does.Contain("MY_PROC")); + Assert.That(sql, Does.Contain("()")); + Assert.That(sql, Does.Not.Contain("EXEC")); + }); + } + + [Test] + public void GetSql_WithParameters_RendersNamedArrowSyntax() + { + // Arrange + var breakdown = new ProcedureBreakdown("MY_PROC"); + breakdown.AddParameter("@ID", "1"); + + // Act - Snowflake named-parameter syntax strips @ and uses => + var sql = breakdown.GetSql(); + + Assert.Multiple(() => + { + // Assert + Assert.That(sql, Does.Contain("CALL")); + Assert.That(sql, Does.Contain("ID => 1")); + }); + } + + #endregion + + #region Parse Tests - Snowflake CALL + + [Test] + public void Parse_CallWithoutParameters_PopulatesName() + { + // Act + var breakdown = ProcedureBreakdown.Parse("CALL MY_PROC()"); + + Assert.Multiple(() => + { + // Assert + Assert.That(breakdown.ProcedureName.Clause, Is.EqualTo("MY_PROC")); + Assert.That(breakdown.IsUsingParameters, Is.False); + }); + } + + [Test] + public void Parse_CallWithNamedParameters_PopulatesParametersWithAtPrefix() + { + // Arrange + var input = "CALL MY_PROC(ID => 1, NAME => 'BOB')"; + + // Act + var breakdown = ProcedureBreakdown.Parse(input); + + Assert.Multiple(() => + { + // Assert - parsed named params are stored with @ prefix for consistency + Assert.That(breakdown.ProcedureName.Clause, Is.EqualTo("MY_PROC")); + Assert.That(breakdown.Parameters["@ID"], Is.EqualTo("1")); + Assert.That(breakdown.Parameters["@NAME"], Is.EqualTo("'BOB'")); + }); + } + + [Test] + public void Parse_CallWithPositionalParameters_GeneratesParamNames() + { + // Arrange - no => present, so values are treated positionally + var input = "CALL MY_PROC(1, 'BOB')"; + + // Act + var breakdown = ProcedureBreakdown.Parse(input); + + Assert.Multiple(() => + { + // Assert + Assert.That(breakdown.Parameters["@param1"], Is.EqualTo("1")); + Assert.That(breakdown.Parameters["@param2"], Is.EqualTo("'BOB'")); + }); + } + + [Test] + public void Parse_MicrosoftSqlMode_AcceptsExecSyntax() + { + // Act - T-SQL EXEC is parsed via base-class delegation + var breakdown = ProcedureBreakdown.Parse("EXEC dbo.GetUser @Id = 1", isMicrosoftSql: true); + + Assert.Multiple(() => + { + // Assert + Assert.That(breakdown, Is.InstanceOf()); + Assert.That(breakdown.ProcedureName.Clause, Is.EqualTo("dbo.GetUser")); + Assert.That(breakdown.Parameters["@Id"], Is.EqualTo("1")); + }); + } + + [Test] + public void Parse_NullOrWhitespace_ThrowsArgumentNullException() + { + // Act & Assert + Assert.Throws(() => ProcedureBreakdown.Parse(" ")); + } + + [Test] + public void Parse_NotACallStatement_ThrowsFormatException() + { + // Act & Assert + Assert.Throws(() => ProcedureBreakdown.Parse("SELECT * FROM USERS")); + } + + [Test] + public void TryParse_ValidCall_ReturnsTrue() + { + // Act + var success = ProcedureBreakdown.TryParse("CALL MY_PROC()", out var breakdown); + + Assert.Multiple(() => + { + // Assert + Assert.That(success, Is.True); + Assert.That(breakdown.ProcedureName.Clause, Is.EqualTo("MY_PROC")); + }); + } + + [Test] + public void TryParse_WrongPrefix_ReturnsFalseWithMessage() + { + // Act + var success = ProcedureBreakdown.TryParse("SELECT 1", out _, out var error); + + Assert.Multiple(() => + { + // Assert + Assert.That(success, Is.False); + Assert.That(error, Does.Contain("CALL")); + }); + } + + #endregion +} diff --git a/tests/Strata.SqlTools.Snowflake.Tests/Snowflake/UpdateBreakdownTests.cs b/tests/Strata.SqlTools.Snowflake.Tests/Snowflake/UpdateBreakdownTests.cs new file mode 100644 index 0000000..09c3ee3 --- /dev/null +++ b/tests/Strata.SqlTools.Snowflake.Tests/Snowflake/UpdateBreakdownTests.cs @@ -0,0 +1,161 @@ +using Strata.SqlTools.Breakdowns.Snowflake; + +namespace Strata.SqlTools.SqlBreakdown.Tests.Snowflake; + +[TestFixture] +public class UpdateBreakdownTests +{ + #region Constructor Tests + + [Test] + public void Constructor_Default_InitializesEmptyClauses() + { + // Arrange & Act + var breakdown = new UpdateBreakdown(); + + Assert.Multiple(() => + { + // Assert + Assert.That(breakdown.TableName.Clause, Is.EqualTo(string.Empty)); + Assert.That(breakdown.SetClause.Clause, Is.EqualTo(string.Empty)); + Assert.That(breakdown.IsUsingWhereClause, Is.False); + }); + } + + [Test] + public void Constructor_WithClauses_SetsProperties() + { + // Arrange & Act + var breakdown = new UpdateBreakdown("USERS", "NAME = :NAME", "ID = :ID"); + + Assert.Multiple(() => + { + // Assert + Assert.That(breakdown.TableName.Clause, Is.EqualTo("USERS")); + Assert.That(breakdown.SetClause.Clause, Is.EqualTo("NAME = :NAME")); + Assert.That(breakdown.WhereClause.Clause, Is.EqualTo("ID = :ID")); + }); + } + + #endregion + + #region GetSql Tests + + [Test] + public void GetSql_RendersUpdateSetWhere() + { + // Arrange - Snowflake inherits the SQL Server UPDATE formatter + var breakdown = new UpdateBreakdown("USERS", "NAME = :NAME", "ID = :ID"); + + // Act + var sql = breakdown.GetSql(); + + Assert.Multiple(() => + { + // Assert + Assert.That(sql, Does.Contain("UPDATE")); + Assert.That(sql, Does.Contain("USERS")); + Assert.That(sql, Does.Contain("SET")); + Assert.That(sql, Does.Contain("NAME = :NAME")); + Assert.That(sql, Does.Contain("WHERE")); + }); + } + + #endregion + + #region Parse Tests + + [Test] + public void Parse_SnowflakeUpdate_PopulatesClauses() + { + // Arrange + var input = "UPDATE USERS SET NAME = 'BOB' WHERE ID = 1"; + + // Act + var breakdown = UpdateBreakdown.Parse(input); + + Assert.Multiple(() => + { + // Assert + Assert.That(breakdown.TableName.Clause, Is.EqualTo("USERS")); + Assert.That(breakdown.SetClause.Clause, Is.EqualTo("NAME = 'BOB'")); + Assert.That(breakdown.WhereClause.Clause, Is.EqualTo("ID = 1")); + }); + } + + [Test] + public void Parse_UpdateWithFrom_PopulatesFromClause() + { + // Arrange + var input = "UPDATE T SET T.NAME = S.NAME FROM TARGET T INNER JOIN SOURCE S ON T.ID = S.ID WHERE S.ACTIVE = 1"; + + // Act + var breakdown = UpdateBreakdown.Parse(input); + + Assert.Multiple(() => + { + // Assert + Assert.That(breakdown.IsUsingFromClause, Is.True); + Assert.That(breakdown.FromClause.Clause, Does.Contain("INNER JOIN SOURCE")); + Assert.That(breakdown.WhereClause.Clause, Is.EqualTo("S.ACTIVE = 1")); + }); + } + + [Test] + public void Parse_MicrosoftSqlMode_DelegatesToBase() + { + // Act + var breakdown = UpdateBreakdown.Parse("UPDATE Users SET Name = 'Bob' WHERE Id = 1", isMicrosoftSql: true); + + Assert.Multiple(() => + { + // Assert + Assert.That(breakdown, Is.InstanceOf()); + Assert.That(breakdown.TableName.Clause, Is.EqualTo("Users")); + }); + } + + [Test] + public void Parse_NullOrWhitespace_ThrowsArgumentNullException() + { + // Act & Assert + Assert.Throws(() => UpdateBreakdown.Parse(string.Empty)); + } + + [Test] + public void Parse_NotAnUpdateStatement_ThrowsFormatException() + { + // Act & Assert + Assert.Throws(() => UpdateBreakdown.Parse("SELECT * FROM USERS")); + } + + [Test] + public void TryParse_MicrosoftSqlMode_ReturnsSnowflakeInstance() + { + // Act + var success = UpdateBreakdown.TryParse("UPDATE Users SET Name = 'Bob'", out var breakdown, isMicrosoftSql: true); + + Assert.Multiple(() => + { + // Assert + Assert.That(success, Is.True); + Assert.That(breakdown, Is.InstanceOf()); + }); + } + + [Test] + public void TryParse_WrongPrefix_ReturnsFalse() + { + // Act + var success = UpdateBreakdown.TryParse("DELETE FROM USERS", out _, out var error); + + Assert.Multiple(() => + { + // Assert + Assert.That(success, Is.False); + Assert.That(error, Does.Contain("UPDATE")); + }); + } + + #endregion +} diff --git a/tests/Strata.SqlTools.SqlBreakdown.Tests/Utilities/ArrayUtilsTests.cs b/tests/Strata.SqlTools.SqlBreakdown.Tests/Utilities/ArrayUtilsTests.cs new file mode 100644 index 0000000..9b6e724 --- /dev/null +++ b/tests/Strata.SqlTools.SqlBreakdown.Tests/Utilities/ArrayUtilsTests.cs @@ -0,0 +1,324 @@ +using System.Collections; +using Strata.SqlTools.SqlBreakdown.Utilities; + +namespace Strata.SqlTools.SqlBreakdown.Tests.Utilities; + +[TestFixture] +public class ArrayUtilsTests +{ + private static readonly Guid GuidA = new("11111111-1111-1111-1111-111111111111"); + private static readonly Guid GuidB = new("22222222-2222-2222-2222-222222222222"); + private static readonly Guid GuidC = new("33333333-3333-3333-3333-333333333333"); + + #region NewArray / Coalesce + + [Test] + public void NewArray_ReturnsArrayOfValues() + { + // Act + var result = ArrayUtils.NewArray(1, 2, 3); + + // Assert + Assert.That(result, Is.EqualTo(new[] { 1, 2, 3 })); + } + + [Test] + public void Coalesce_ReturnsFirstNonNull() + { + // Act & Assert + Assert.That(ArrayUtils.Coalesce(null!, null!, "x", "y"), Is.EqualTo("x")); + } + + [Test] + public void Coalesce_AllNull_ReturnsNull() + { + // Act & Assert + Assert.That(ArrayUtils.Coalesce(null!, null!), Is.Null); + } + + #endregion + + #region CSV -> Collection + + [Test] + public void GetArrayListFromCsv_TrimsAndSplits() + { + // Act + var result = ArrayUtils.GetArrayListFromCsv("a, b, c"); + + // Assert + Assert.That(result, Is.EqualTo(new ArrayList { "a", "b", "c" })); + } + + [Test] + public void GetArrayListFromCsv_Empty_ReturnsEmptyList() + { + // Act & Assert + Assert.That(ArrayUtils.GetArrayListFromCsv(string.Empty), Is.Empty); + } + + [Test] + public void GetGenericListFromCsv_TrimsEntries() + { + // Act + var result = ArrayUtils.GetGenericListFromCsv("a, b ,c"); + + // Assert + Assert.That(result, Is.EqualTo(new List { "a", "b", "c" })); + } + + [Test] + public void GetGenericListFromCsv_Empty_ReturnsEmpty() + { + // Act & Assert + Assert.That(ArrayUtils.GetGenericListFromCsv(string.Empty), Is.Empty); + } + + [Test] + public void GetIntegerListFromCsv_SkipsNonIntegers() + { + // Act + var result = ArrayUtils.GetIntegerListFromCsv("1,2,x,3"); + + // Assert + Assert.That(result, Is.EqualTo(new List { 1, 2, 3 })); + } + + [Test] + public void GetGuidListFromCsv_ParsesValidSkipsInvalid() + { + // Act + var result = ArrayUtils.GetGuidListFromCsv($"{GuidA},not-a-guid,{GuidB}"); + + // Assert + Assert.That(result, Is.EqualTo(new List { GuidA, GuidB })); + } + + [Test] + public void GetGuidListFromCsv_Empty_ReturnsEmpty() + { + // Act & Assert + Assert.That(ArrayUtils.GetGuidListFromCsv(string.Empty), Is.Empty); + } + + [Test] + public void GetGuidArrayFromCsv_ParsesGuids() + { + // Act + var result = ArrayUtils.GetGuidArrayFromCsv($"{GuidA},{GuidB}"); + + // Assert + Assert.That(result, Is.EqualTo(new[] { GuidA, GuidB })); + } + + #endregion + + #region Collection -> CSV + + [Test] + public void GetCsvFromArrayList_JoinsWithCommas() + { + // Act + var result = ArrayUtils.GetCsvFromArrayList(new ArrayList { "a", "b", "c" }); + + // Assert + Assert.That(result, Is.EqualTo("a,b,c")); + } + + [Test] + public void GetCsvFromGenericList_JoinsWithCommas() + { + // Act & Assert + Assert.That(ArrayUtils.GetCsvFromGenericList(new List { "a", "b" }), Is.EqualTo("a,b")); + } + + [Test] + public void GetCsvFromGenericList_SingleQuotes_WrapsEachValue() + { + // Act + var result = ArrayUtils.GetCsvFromGenericList(new List { "a", "b" }, ArrayUtils.QuoteType.Single); + + // Assert + Assert.That(result, Is.EqualTo("'a','b'")); + } + + [Test] + public void GetCsvFromGenericList_DoubleQuotes_WrapsEachValue() + { + // Act + var result = ArrayUtils.GetCsvFromGenericList(new List { "a", "b" }, ArrayUtils.QuoteType.Double); + + // Assert + Assert.That(result, Is.EqualTo("\"a\",\"b\"")); + } + + [Test] + public void GetCsvFromGenericList_NoneQuoteType_BehavesLikePlainJoin() + { + // Act & Assert + Assert.That(ArrayUtils.GetCsvFromGenericList(new List { "a", "b" }, ArrayUtils.QuoteType.None), Is.EqualTo("a,b")); + } + + [Test] + public void GetCsvFromGenericListOfGuids_Null_ReturnsEmpty() + { + // Act & Assert + Assert.That(ArrayUtils.GetCsvFromGenericListOfGuids(null!), Is.EqualTo(string.Empty)); + } + + [Test] + public void GetCsvFromGenericListOfGuids_NoQuotes_JoinsGuids() + { + // Act + var result = ArrayUtils.GetCsvFromGenericListOfGuids(new List { GuidA, GuidB }); + + // Assert + Assert.That(result, Is.EqualTo($"{GuidA},{GuidB}")); + } + + [Test] + public void GetCsvFromGenericListOfGuids_SingleQuotes_WrapsEachGuid() + { + // Act + var result = ArrayUtils.GetCsvFromGenericListOfGuids(new List { GuidA }, ArrayUtils.QuoteType.Single); + + // Assert + Assert.That(result, Is.EqualTo($"'{GuidA}'")); + } + + #endregion + + #region Encode + + [Test] + public void Encode_NoInternalDelimiters_JoinsWithCommas() + { + // Act & Assert + Assert.That(ArrayUtils.Encode(new List { "x", "y", "z" }), Is.EqualTo("x,y,z")); + } + + [Test] + public void Encode_InternalComma_ReplacedWithIllegalCharacter() + { + // Arrange - internal commas are swapped for char(8) so they survive the comma join + const char illegal = (char)8; + + // Act + var result = ArrayUtils.Encode(new List { "a,b", "c" }); + + // Assert + Assert.That(result, Is.EqualTo($"a{illegal}b,c")); + } + + #endregion + + #region Guid List <-> String + + [Test] + public void ConvertGuidStringToList_Empty_ReturnsEmpty() + { + // Act & Assert + Assert.That(ArrayUtils.ConvertGuidStringToList(string.Empty), Is.Empty); + } + + [Test] + public void ConvertGuidStringToList_ParsesAll() + { + // Act + var result = ArrayUtils.ConvertGuidStringToList($"{GuidA},{GuidB}"); + + // Assert + Assert.That(result, Is.EqualTo(new List { GuidA, GuidB })); + } + + [Test] + public void ConvertToGuidArray_ParsesStrings() + { + // Act + var result = ArrayUtils.ConvertToGuidArray(new[] { GuidA.ToString(), GuidB.ToString() }); + + // Assert + Assert.That(result, Is.EqualTo(new[] { GuidA, GuidB })); + } + + [Test] + public void ConvertToGuidList_ParsesStrings() + { + // Act + var result = ArrayUtils.ConvertToGuidList(new[] { GuidA.ToString() }); + + // Assert + Assert.That(result, Is.EqualTo(new List { GuidA })); + } + + [Test] + public void ConvertGuidListToString_Null_ReturnsEmpty() + { + // Act & Assert + Assert.That(ArrayUtils.ConvertGuidListToString(null!), Is.EqualTo(string.Empty)); + } + + [Test] + public void ConvertGuidListToString_NoSurround_JoinsWithCommas() + { + // Act & Assert + Assert.That(ArrayUtils.ConvertGuidListToString(new[] { GuidA, GuidB }), Is.EqualTo($"{GuidA},{GuidB}")); + } + + [Test] + public void ConvertGuidListToString_WithSurroundChar_WrapsEachGuid() + { + // Act + var result = ArrayUtils.ConvertGuidListToString(new[] { GuidA }, "'"); + + // Assert + Assert.That(result, Is.EqualTo($"'{GuidA}'")); + } + + [Test] + public void ConvertGuidListToStringList_ReturnsStringRepresentations() + { + // Act + var result = ArrayUtils.ConvertGuidListToStringList(new[] { GuidA, GuidB }); + + // Assert + Assert.That(result, Is.EqualTo(new List { GuidA.ToString(), GuidB.ToString() })); + } + + #endregion + + #region Intersection + + [Test] + public void GetGuidListsIntersection_ReturnsCommonGuids() + { + // Act + var result = ArrayUtils.GetGuidListsIntersection( + new List { GuidA, GuidB }, + new List { GuidB, GuidC }); + + // Assert + Assert.That(result, Is.EqualTo(new List { GuidB })); + } + + [Test] + public void GetGuidListsIntersection_NoOverlap_ReturnsEmpty() + { + // Act + var result = ArrayUtils.GetGuidListsIntersection( + new List { GuidA }, + new List { GuidB }); + + // Assert + Assert.That(result, Is.Empty); + } + + [Test] + public void GetGuidListsIntersection_NullArguments_ReturnEmpty() + { + // Act & Assert + Assert.That(ArrayUtils.GetGuidListsIntersection(null!, null!), Is.Empty); + } + + #endregion +} diff --git a/tests/Strata.SqlTools.SqlBreakdown.Tests/Utilities/GuidUtilsTests.cs b/tests/Strata.SqlTools.SqlBreakdown.Tests/Utilities/GuidUtilsTests.cs new file mode 100644 index 0000000..c98e7f2 --- /dev/null +++ b/tests/Strata.SqlTools.SqlBreakdown.Tests/Utilities/GuidUtilsTests.cs @@ -0,0 +1,215 @@ +using Strata.SqlTools.SqlBreakdown.Utilities; + +namespace Strata.SqlTools.SqlBreakdown.Tests.Utilities; + +[TestFixture] +public class GuidUtilsTests +{ + private static readonly Guid Sample = new("0123abcd-4567-89ef-0123-456789abcdef"); + + #region TranslateGuid / UnTranslateGuid + + [Test] + public void TranslateGuid_RemovesHyphensAndPrefixesWithG() + { + // Act + var result = GuidUtils.TranslateGuid(Sample); + + Assert.Multiple(() => + { + // Assert + Assert.That(result, Does.StartWith("g")); + Assert.That(result, Has.Length.EqualTo(33)); + Assert.That(result, Does.Not.Contain("-")); + }); + } + + [Test] + public void UnTranslateGuid_RoundTripsTranslateGuid() + { + // Arrange + var translated = GuidUtils.TranslateGuid(Sample); + + // Act & Assert + Assert.That(GuidUtils.UnTranslateGuid(translated), Is.EqualTo(Sample)); + } + + [Test] + public void UnTranslateGuid_AlreadyHyphenated_ParsesDirectly() + { + // Act & Assert + Assert.That(GuidUtils.UnTranslateGuid(Sample.ToString()), Is.EqualTo(Sample)); + } + + [TestCase("")] // null/empty guard + [TestCase("notprefixed")] // no hyphen, does not start with 'g' + [TestCase("gTOOSHORT")] // starts with 'g' but wrong length (not 33) + public void UnTranslateGuid_InvalidInputs_ReturnEmpty(string value) + { + // Act & Assert + Assert.That(GuidUtils.UnTranslateGuid(value), Is.EqualTo(Guid.Empty)); + } + + #endregion + + #region SQL-safe column GUID + + [Test] + public void GetSqlColumnSafeGuid_UppercasesAndReplacesHyphens() + { + // Act + var result = GuidUtils.GetSqlColumnSafeGuid(Sample); + + Assert.Multiple(() => + { + // Assert + Assert.That(result, Does.StartWith("G")); + Assert.That(result, Does.Not.Contain("-")); + Assert.That(result, Does.Contain("_")); + }); + } + + [Test] + public void GetGuidFromSqlColumnSafeGuid_RoundTrips() + { + // Arrange + var safe = GuidUtils.GetSqlColumnSafeGuid(Sample); + + // Act & Assert + Assert.That(GuidUtils.GetGuidFromSqlColumnSafeGuid(safe), Is.EqualTo(Sample)); + } + + #endregion + + #region TranslateTextToGuid + + [Test] + public void TranslateTextToGuid_IsDeterministic() + { + // Act & Assert - same input must always yield the same GUID + Assert.That(GuidUtils.TranslateTextToGuid("hello world"), Is.EqualTo(GuidUtils.TranslateTextToGuid("hello world"))); + } + + [Test] + public void TranslateTextToGuid_EmptyInput_ReturnsEmpty() + { + // Act & Assert + Assert.That(GuidUtils.TranslateTextToGuid(string.Empty), Is.EqualTo(Guid.Empty)); + } + + [Test] + public void TranslateTextToGuid_DifferentInputs_GenerallyDiffer() + { + // Act & Assert + Assert.That(GuidUtils.TranslateTextToGuid("alpha"), Is.Not.EqualTo(GuidUtils.TranslateTextToGuid("beta"))); + } + + #endregion + + #region Validation + + [TestCase("0123abcd-4567-89ef-0123-456789abcdef", true)] + [TestCase("not a guid", false)] + [TestCase("", false)] + public void IsGuid_String_ValidatesCorrectly(string value, bool expected) + { + // Act & Assert + Assert.That(GuidUtils.IsGuid(value), Is.EqualTo(expected)); + } + + [Test] + public void IsGuid_Object_GuidInstance_ReturnsTrue() + { + // Act & Assert + Assert.That(GuidUtils.IsGuid((object)Sample), Is.True); + } + + [Test] + public void IsGuid_Object_GuidString_ReturnsTrue() + { + // Act & Assert + Assert.That(GuidUtils.IsGuid((object)Sample.ToString()), Is.True); + } + + [Test] + public void IsGuid_Object_NonGuid_ReturnsFalse() + { + // Act & Assert + Assert.That(GuidUtils.IsGuid((object)12345), Is.False); + } + + #endregion + + #region FindFirstGuid / GetGuids + + [Test] + public void FindFirstGuid_EmbeddedGuid_ReturnsIt() + { + // Act + var result = GuidUtils.FindFirstGuid($"prefix {Sample} suffix"); + + // Assert + Assert.That(result, Is.EqualTo(Sample)); + } + + [Test] + public void FindFirstGuid_NoGuid_ReturnsEmpty() + { + // Act & Assert + Assert.That(GuidUtils.FindFirstGuid("nothing here"), Is.EqualTo(Guid.Empty)); + } + + [Test] + public void GetGuids_ReturnsAllEmbeddedGuids() + { + // Arrange + var second = new Guid("99999999-9999-9999-9999-999999999999"); + + // Act + var result = GuidUtils.GetGuids($"a {Sample} b {second} c"); + + // Assert + Assert.That(result, Has.Count.EqualTo(2)); + } + + #endregion + + #region GetGuid + + [Test] + public void GetGuid_ValidString_ParsesValue() + { + // Act & Assert + Assert.That(GuidUtils.GetGuid(Sample.ToString()), Is.EqualTo(Sample)); + } + + [Test] + public void GetGuid_InvalidString_ReturnsEmpty() + { + // Act & Assert + Assert.That(GuidUtils.GetGuid("nope"), Is.EqualTo(Guid.Empty)); + } + + [Test] + public void GetGuid_InvalidString_ReturnsProvidedDefault() + { + // Act & Assert + Assert.That(GuidUtils.GetGuid("nope", Sample), Is.EqualTo(Sample)); + } + + [Test] + public void GetGuid_NullObject_ReturnsEmpty() + { + // Act & Assert + Assert.That(GuidUtils.GetGuid((object)null!), Is.EqualTo(Guid.Empty)); + } + + [Test] + public void GetGuid_Object_ParsesToStringValue() + { + // Act & Assert + Assert.That(GuidUtils.GetGuid((object)Sample.ToString()), Is.EqualTo(Sample)); + } + + #endregion +} diff --git a/tests/Strata.SqlTools.SqlBreakdown.Tests/Utilities/SqlUtilsTests.cs b/tests/Strata.SqlTools.SqlBreakdown.Tests/Utilities/SqlUtilsTests.cs new file mode 100644 index 0000000..710dd37 --- /dev/null +++ b/tests/Strata.SqlTools.SqlBreakdown.Tests/Utilities/SqlUtilsTests.cs @@ -0,0 +1,480 @@ +using System.Data; +using Strata.SqlTools.SqlBreakdown.Enums.SQL; +using Strata.SqlTools.SqlBreakdown.Utilities; + +namespace Strata.SqlTools.SqlBreakdown.Tests.Utilities; + +[TestFixture] +public class SqlUtilsTests +{ + #region String Manipulation + + [Test] + public void StripColumnTableAlias_RemovesTableAliases() + { + // Act & Assert + Assert.That(SqlUtils.StripColumnTableAlias("a.Col1, b.Col2"), Is.EqualTo("Col1,Col2")); + } + + [Test] + public void StripColumnTableAlias_NoAlias_LeavesColumnUntouched() + { + // Act & Assert + Assert.That(SqlUtils.StripColumnTableAlias("MyCol"), Is.EqualTo("MyCol")); + } + + [TestCase("O'Brien", "O''Brien")] + [TestCase("", "")] + [TestCase("clean", "clean")] + public void EscapeInvalidCharacter_DoublesSingleQuotes(string input, string expected) + { + // Act & Assert + Assert.That(SqlUtils.EscapeInvalidCharacter(input), Is.EqualTo(expected)); + } + + [TestCase("Col", "[Col]")] + [TestCase("[Col]", "[Col]")] + [TestCase("[Col", "[Col]")] + [TestCase("Col]", "[Col]")] + public void AddBrackets_WrapsWhenMissing(string input, string expected) + { + // Act & Assert + Assert.That(SqlUtils.AddBrackets(input), Is.EqualTo(expected)); + } + + [Test] + public void RemoveBrackets_StripsSquareBrackets() + { + // Act & Assert + Assert.That(SqlUtils.RemoveBrackets("[Col]"), Is.EqualTo("Col")); + } + + [Test] + public void GetSqlFriendlyName_RemovesInvalidCharactersByDefault() + { + // Act & Assert + Assert.That(SqlUtils.GetSqlFriendlyName("a.b c"), Is.EqualTo("abc")); + } + + [Test] + public void GetSqlFriendlyName_UsesReplacement() + { + // Act & Assert + Assert.That(SqlUtils.GetSqlFriendlyName("a.b c", "_"), Is.EqualTo("a_b_c")); + } + + #endregion + + #region Object Name Handling + + [Test] + public void GetSqlObjectExpression_BuildsBracketedExpression() + { + // Act & Assert + Assert.That(SqlUtils.GetSqlObjectExpression("dbo", "MyTable"), Is.EqualTo("[dbo].[MyTable]")); + } + + [Test] + public void GetSqlObjectExpression_StripsExistingBrackets() + { + // Act & Assert + Assert.That(SqlUtils.GetSqlObjectExpression("[dbo]", "[MyTable]"), Is.EqualTo("[dbo].[MyTable]")); + } + + [Test] + public void GetSqlObjectName_ExtractsObjectPortion() + { + // Act & Assert + Assert.That(SqlUtils.GetSqlObjectName("[dbo].[MyTable]"), Is.EqualTo("MyTable")); + } + + [Test] + public void GetSqlObjectName_NoDot_ReturnsInput() + { + // Act & Assert + Assert.That(SqlUtils.GetSqlObjectName("MyTable"), Is.EqualTo("MyTable")); + } + + [Test] + public void GetSqlObjectName_MultipleDots_Throws() + { + // Act & Assert + Assert.Throws(() => SqlUtils.GetSqlObjectName("a.b.c")); + } + + [Test] + public void GetSqlSchemaName_ExtractsSchema() + { + // Act & Assert + Assert.That(SqlUtils.GetSqlSchemaName("data.DimClient"), Is.EqualTo("data")); + } + + [Test] + public void GetSqlSchemaName_NoDot_ReturnsDbo() + { + // Act & Assert + Assert.That(SqlUtils.GetSqlSchemaName("DimClient"), Is.EqualTo("dbo")); + } + + [Test] + public void GetSqlSchemaName_MultipleDots_Throws() + { + // Act & Assert + Assert.Throws(() => SqlUtils.GetSqlSchemaName("a.b.c")); + } + + [TestCase("dbo", true)] + [TestCase("DBO", true)] + [TestCase("data", false)] + public void IsDefaultSchema_ChecksAgainstDbo(string schema, bool expected) + { + // Act & Assert + Assert.That(SqlUtils.IsDefaultSchema(schema), Is.EqualTo(expected)); + } + + #endregion + + #region Data Type Conversions + + [Test] + public void GetStringConversionLength_KnownTypes() + { + Assert.Multiple(() => + { + Assert.That(SqlUtils.GetStringConversionLength(SqlDataType.Bit), Is.EqualTo(1)); + Assert.That(SqlUtils.GetStringConversionLength(SqlDataType.Int), Is.EqualTo(int.MaxValue.ToString().Length)); + Assert.That(SqlUtils.GetStringConversionLength(SqlDataType.UniqueIdentifier), Is.EqualTo(Guid.Empty.ToString().Length)); + Assert.That(SqlUtils.GetStringConversionLength(SqlDataType.NVarChar), Is.EqualTo(SqlUtils.MAX_NVARCHAR_LENGTH)); + }); + } + + [Test] + public void ConvertGuidToAlias_ProducesPrefixedUppercaseAlias() + { + // Arrange + var guid = new Guid("0123abcd-4567-89ef-0123-456789abcdef"); + + // Act + var result = SqlUtils.ConvertGuidToAlias(guid, "p"); + + Assert.Multiple(() => + { + // Assert + Assert.That(result, Does.StartWith("p")); + Assert.That(result, Does.Not.Contain("-")); + Assert.That(result, Is.EqualTo("p0123ABCD456789EF0123456789ABCDEF")); + }); + } + + [Test] + public void IsFixedPrecision_DecimalIsNotFixed() + { + Assert.Multiple(() => + { + Assert.That(SqlUtils.IsFixedPrecision(SqlDataType.Decimal), Is.False); + Assert.That(SqlUtils.IsFixedPrecision(SqlDataType.Int), Is.True); + // IsFixedScale mirrors IsFixedPrecision + Assert.That(SqlUtils.IsFixedScale(SqlDataType.Decimal), Is.False); + Assert.That(SqlUtils.IsFixedScale(SqlDataType.Int), Is.True); + }); + } + + [Test] + public void IsFixedMaxLength_VariesByType() + { + Assert.Multiple(() => + { + Assert.That(SqlUtils.IsFixedMaxLength(SqlDataType.Int), Is.True); + Assert.That(SqlUtils.IsFixedMaxLength(SqlDataType.UniqueIdentifier), Is.True); + Assert.That(SqlUtils.IsFixedMaxLength(SqlDataType.VarChar), Is.False); + }); + } + + [Test] + public void GetFixedScale_KnownTypes() + { + Assert.Multiple(() => + { + Assert.That(SqlUtils.GetFixedScale(SqlDataType.DateTime), Is.EqualTo(3)); + Assert.That(SqlUtils.GetFixedScale(SqlDataType.Money), Is.EqualTo(4)); + Assert.That(SqlUtils.GetFixedScale(SqlDataType.Int), Is.EqualTo(0)); + }); + } + + [Test] + public void GetFixedScale_Decimal_Throws() + { + // Act & Assert + Assert.Throws(() => SqlUtils.GetFixedScale(SqlDataType.Decimal)); + } + + [Test] + public void GetFixedPrecision_KnownTypes() + { + Assert.Multiple(() => + { + Assert.That(SqlUtils.GetFixedPrecision(SqlDataType.BigInt), Is.EqualTo(19)); + Assert.That(SqlUtils.GetFixedPrecision(SqlDataType.Int), Is.EqualTo(10)); + Assert.That(SqlUtils.GetFixedPrecision(SqlDataType.Bit), Is.EqualTo(1)); + }); + } + + [Test] + public void GetFixedMaxLength_KnownTypes() + { + Assert.Multiple(() => + { + Assert.That(SqlUtils.GetFixedMaxLength(SqlDataType.Int), Is.EqualTo(4)); + Assert.That(SqlUtils.GetFixedMaxLength(SqlDataType.UniqueIdentifier), Is.EqualTo(16)); + Assert.That(SqlUtils.GetFixedMaxLength(SqlDataType.TinyInt), Is.EqualTo(1)); + }); + } + + [Test] + public void GetFixedMaxLength_UnsupportedType_Throws() + { + // Act & Assert - VarChar has no fixed length + Assert.Throws(() => SqlUtils.GetFixedMaxLength(SqlDataType.VarChar)); + } + + #endregion + + #region Well-Known SQL Errors + + [Test] + public void GetWellKnownSqlError_DuplicateKeyRow_IsUniqueKeyViolation() + { + // Arrange + var ex = new Exception("Cannot insert duplicate key row in object 'dbo.Users'."); + + // Act & Assert + Assert.That(SqlUtils.GetWellKnownSqlError(ex), Is.EqualTo(SqlUtils.WellKnownSqlError.UniqueKeyViolation)); + } + + [Test] + public void GetWellKnownSqlError_ObjectAlreadyExists_IsDetected() + { + // Arrange + var ex = new Exception("There is already an object named 'Users' in the database."); + + // Act & Assert + Assert.That(SqlUtils.GetWellKnownSqlError(ex), Is.EqualTo(SqlUtils.WellKnownSqlError.ObjectAlreadyExists)); + } + + [Test] + public void GetWellKnownSqlError_TruncateForeignKey_IsDetected() + { + // Arrange + var ex = new Exception("Cannot truncate table 'dbo.Orders' because it is being referenced by a FOREIGN KEY constraint."); + + // Act & Assert + Assert.That(SqlUtils.GetWellKnownSqlError(ex), Is.EqualTo(SqlUtils.WellKnownSqlError.TruncateTableForeignKeyReferenceError)); + } + + [Test] + public void GetWellKnownSqlError_UnrecognizedMessage_IsUnknown() + { + // Act & Assert + Assert.That(SqlUtils.GetWellKnownSqlError(new Exception("Something else entirely")), Is.EqualTo(SqlUtils.WellKnownSqlError.Unknown)); + } + + #endregion + + #region Data Type Helpers + + [Test] + public void GetClientFriendlyDataTypeName_MapsCategories() + { + Assert.Multiple(() => + { + Assert.That(SqlUtils.GetClientFriendlyDataTypeName(SqlDataType.NVarChar), Is.EqualTo("Text")); + Assert.That(SqlUtils.GetClientFriendlyDataTypeName(SqlDataType.Int), Is.EqualTo("Whole Number")); + Assert.That(SqlUtils.GetClientFriendlyDataTypeName(SqlDataType.Decimal), Is.EqualTo("Decimal")); + Assert.That(SqlUtils.GetClientFriendlyDataTypeName(SqlDataType.DateTime), Is.EqualTo("Date")); + Assert.That(SqlUtils.GetClientFriendlyDataTypeName(SqlDataType.Money), Is.EqualTo("Dollars")); + }); + } + + [Test] + public void IdentityColumnTypes_ContainsIntegerTypes() + { + // Act + var types = SqlUtils.IdentityColumnTypes().ToList(); + + // Assert + Assert.That(types, Is.EquivalentTo(new[] + { + SqlDataType.BigInt, SqlDataType.TinyInt, SqlDataType.Int, SqlDataType.SmallInt + })); + } + + [Test] + public void GetOrderBySqlDirectionString_MapsDirections() + { + Assert.Multiple(() => + { + Assert.That(SqlUtils.GetOrderBySqlDirectionString(SortDirection.Ascending), Is.EqualTo("ASC")); + Assert.That(SqlUtils.GetOrderBySqlDirectionString(SortDirection.Descending), Is.EqualTo("DESC")); + }); + } + + [Test] + public void GetSimpleDataType_MapsToCategories() + { + Assert.Multiple(() => + { + Assert.That(SqlUtils.GetSimpleDataType(SqlDataType.Int), Is.EqualTo(SimpleDataType.Numeric)); + Assert.That(SqlUtils.GetSimpleDataType(SqlDataType.DateTime), Is.EqualTo(SimpleDataType.Date)); + Assert.That(SqlUtils.GetSimpleDataType(SqlDataType.Bit), Is.EqualTo(SimpleDataType.Boolean)); + Assert.That(SqlUtils.GetSimpleDataType(SqlDataType.UniqueIdentifier), Is.EqualTo(SimpleDataType.GUID)); + Assert.That(SqlUtils.GetSimpleDataType(SqlDataType.NVarChar), Is.EqualTo(SimpleDataType.String)); + }); + } + + [TestCase("1", true)] + [TestCase("0", true)] + [TestCase("yes", true)] + [TestCase("no", true)] + [TestCase("true", true)] + [TestCase("maybe", false)] + public void IsStringValueValidForSimpleDataType_Boolean(string value, bool expected) + { + // Act & Assert + Assert.That(SqlUtils.IsStringValueValidForSimpleDataType(SimpleDataType.Boolean, value), Is.EqualTo(expected)); + } + + [TestCase("3.14", true)] + [TestCase("100", true)] + [TestCase("abc", false)] + public void IsStringValueValidForSimpleDataType_Numeric(string value, bool expected) + { + // Act & Assert + Assert.That(SqlUtils.IsStringValueValidForSimpleDataType(SimpleDataType.Numeric, value), Is.EqualTo(expected)); + } + + [Test] + public void IsStringValueValidForSimpleDataType_GuidAndString() + { + Assert.Multiple(() => + { + Assert.That(SqlUtils.IsStringValueValidForSimpleDataType(SimpleDataType.GUID, Guid.NewGuid().ToString()), Is.True); + Assert.That(SqlUtils.IsStringValueValidForSimpleDataType(SimpleDataType.GUID, "nope"), Is.False); + // String accepts anything + Assert.That(SqlUtils.IsStringValueValidForSimpleDataType(SimpleDataType.String, "anything"), Is.True); + }); + } + + [Test] + public void GetValueType_MapsToClrTypes() + { + Assert.Multiple(() => + { + Assert.That(SqlUtils.GetValueType(SqlDataType.Bit), Is.EqualTo(typeof(bool))); + Assert.That(SqlUtils.GetValueType(SqlDataType.DateTime), Is.EqualTo(typeof(DateTime))); + Assert.That(SqlUtils.GetValueType(SqlDataType.Int), Is.EqualTo(typeof(double))); + Assert.That(SqlUtils.GetValueType(SqlDataType.UniqueIdentifier), Is.EqualTo(typeof(Guid))); + Assert.That(SqlUtils.GetValueType(SqlDataType.NVarChar), Is.EqualTo(typeof(string))); + }); + } + + [Test] + public void GetSqlDataType_MapsFromSqlDbType() + { + Assert.Multiple(() => + { + Assert.That(SqlUtils.GetSqlDataType(SqlDbType.Int), Is.EqualTo(SqlDataType.Int)); + Assert.That(SqlUtils.GetSqlDataType(SqlDbType.DateTime2), Is.EqualTo(SqlDataType.DateTime)); + Assert.That(SqlUtils.GetSqlDataType(SqlDbType.UniqueIdentifier), Is.EqualTo(SqlDataType.UniqueIdentifier)); + // Unmapped types fall back to NVarChar + Assert.That(SqlUtils.GetSqlDataType(SqlDbType.Variant), Is.EqualTo(SqlDataType.NVarChar)); + }); + } + + #endregion + + #region Default Values + + [Test] + public void GetSqlDefaultValueDefinition_KnownTypes() + { + Assert.Multiple(() => + { + Assert.That(SqlUtils.GetSqlDefaultValueDefinition(SqlDataType.Int), Is.EqualTo("(0)")); + Assert.That(SqlUtils.GetSqlDefaultValueDefinition(SqlDataType.VarChar), Is.EqualTo("('')")); + Assert.That(SqlUtils.GetSqlDefaultValueDefinition(SqlDataType.DateTime), Is.EqualTo("(getdate())")); + Assert.That(SqlUtils.GetSqlDefaultValueDefinition(SqlDataType.UniqueIdentifier), Is.EqualTo($"('{Guid.Empty}')")); + }); + } + + [Test] + public void GetSqlZeroValue_KnownTypes() + { + Assert.Multiple(() => + { + Assert.That(SqlUtils.GetSqlZeroValue(SqlDataType.Int), Is.EqualTo(0)); + Assert.That(SqlUtils.GetSqlZeroValue(SqlDataType.NVarChar), Is.EqualTo(string.Empty)); + Assert.That(SqlUtils.GetSqlZeroValue(SqlDataType.DateTime), Is.EqualTo(new DateTime(1900, 1, 1, 0, 0, 0, DateTimeKind.Unspecified))); + Assert.That(SqlUtils.GetSqlZeroValue(SqlDataType.UniqueIdentifier), Is.EqualTo(Guid.Empty)); + }); + } + + [Test] + public void GetSqlDefaultValue_NullDefinition_ReturnsNull() + { + // Act & Assert + Assert.That(SqlUtils.GetSqlDefaultValue(SqlDataType.Int, string.Empty), Is.Null); + } + + [Test] + public void GetSqlDefaultValue_ParsesScalarTypes() + { + Assert.Multiple(() => + { + Assert.That(SqlUtils.GetSqlDefaultValue(SqlDataType.Int, "(0)"), Is.EqualTo(0)); + Assert.That(SqlUtils.GetSqlDefaultValue(SqlDataType.BigInt, "(123)"), Is.EqualTo(123L)); + Assert.That(SqlUtils.GetSqlDefaultValue(SqlDataType.Bit, "(1)"), Is.EqualTo(true)); + Assert.That(SqlUtils.GetSqlDefaultValue(SqlDataType.Bit, "(0)"), Is.EqualTo(false)); + Assert.That(SqlUtils.GetSqlDefaultValue(SqlDataType.NVarChar, "('hello')"), Is.EqualTo("hello")); + }); + } + + [Test] + public void GetSqlDefaultValue_UniqueIdentifierZeroGuid_ReturnsEmpty() + { + // Act & Assert + Assert.That(SqlUtils.GetSqlDefaultValue(SqlDataType.UniqueIdentifier, "(dbo.ZeroGUID())"), Is.EqualTo(Guid.Empty)); + } + + #endregion + + #region Join / Connection Strings + + [Test] + public void GetSqlJoinTypeString_MapsJoinTypes() + { + Assert.Multiple(() => + { + Assert.That(SqlUtils.GetSqlJoinTypeString(SqlJoinType.Left), Is.EqualTo("LEFT")); + Assert.That(SqlUtils.GetSqlJoinTypeString(SqlJoinType.Right), Is.EqualTo("RIGHT")); + Assert.That(SqlUtils.GetSqlJoinTypeString(SqlJoinType.Inner), Is.EqualTo("INNER")); + Assert.That(SqlUtils.GetSqlJoinTypeString(SqlJoinType.Cross), Is.EqualTo("CROSS")); + }); + } + + [Test] + public void GetSqlConnectionString_BuildsIntegratedSecurityConnection() + { + // Act + var result = SqlUtils.GetSqlConnectionString("MyServer", "MyDb"); + + Assert.Multiple(() => + { + // Assert + Assert.That(result, Does.Contain("data source=MyServer;")); + Assert.That(result, Does.Contain("initial catalog=MyDb;")); + Assert.That(result, Does.Contain("Integrated Security=SSPI;")); + }); + } + + #endregion +} diff --git a/tests/Strata.SqlTools.SqlBreakdown.Tests/Utilities/StringUtilsTests.cs b/tests/Strata.SqlTools.SqlBreakdown.Tests/Utilities/StringUtilsTests.cs new file mode 100644 index 0000000..bf575b5 --- /dev/null +++ b/tests/Strata.SqlTools.SqlBreakdown.Tests/Utilities/StringUtilsTests.cs @@ -0,0 +1,39 @@ +using Strata.SqlTools.SqlBreakdown.Utilities; + +namespace Strata.SqlTools.SqlBreakdown.Tests.Utilities; + +[TestFixture] +public class StringUtilsTests +{ + [TestCase('a', -842352705)] + [TestCase('z', -842352730)] + [TestCase('A', -842352673)] + [TestCase('Z', -842352698)] + [TestCase('0', -842352768)] + [TestCase('9', -842352759)] + [TestCase(' ', -842352737)] + [TestCase('-', -842352782)] + public void GetHashCode32Bit_KnownCharacters_ReturnsHardcodedHash(char c, int expected) + { + // Act & Assert + Assert.That(StringUtils.GetHashCode32Bit(c), Is.EqualTo(expected)); + } + + [Test] + public void GetHashCode32Bit_UnmappedCharacter_UsesConsistentFallback() + { + // Arrange - '!' is not in the hardcoded table + const char c = '!'; + var expected = -842352737 - (int)c; + + // Act & Assert + Assert.That(StringUtils.GetHashCode32Bit(c), Is.EqualTo(expected)); + } + + [Test] + public void GetHashCode32Bit_SameCharacter_IsDeterministic() + { + // Act & Assert - critical: TranslateTextToGuid relies on stable hashing + Assert.That(StringUtils.GetHashCode32Bit('q'), Is.EqualTo(StringUtils.GetHashCode32Bit('q'))); + } +} diff --git a/tests/Strata.SqlTools.SqlServer.Tests/SqlServer/DeleteBreakdownTests.cs b/tests/Strata.SqlTools.SqlServer.Tests/SqlServer/DeleteBreakdownTests.cs new file mode 100644 index 0000000..664f7af --- /dev/null +++ b/tests/Strata.SqlTools.SqlServer.Tests/SqlServer/DeleteBreakdownTests.cs @@ -0,0 +1,198 @@ +using Strata.SqlTools.Breakdowns.SqlServer; + +namespace Strata.SqlTools.SqlBreakdown.Tests.SqlServer; + +[TestFixture] +public class DeleteBreakdownTests +{ + #region Constructor Tests + + [Test] + public void Constructor_Default_InitializesEmptyClauses() + { + // Arrange & Act + var breakdown = new DeleteBreakdown(); + + Assert.Multiple(() => + { + // Assert + Assert.That(breakdown.FromClause.Clause, Is.EqualTo(string.Empty)); + Assert.That(breakdown.WhereClause.Clause, Is.EqualTo(string.Empty)); + Assert.That(breakdown.DeleteClause.Clause, Is.EqualTo(string.Empty)); + Assert.That(breakdown.IsUsingWhereClause, Is.False); + }); + } + + [Test] + public void Constructor_WithClauses_SetsProperties() + { + // Arrange & Act + var breakdown = new DeleteBreakdown("Users", "Id = @Id"); + + Assert.Multiple(() => + { + // Assert + Assert.That(breakdown.FromClause.Clause, Is.EqualTo("Users")); + Assert.That(breakdown.WhereClause.Clause, Is.EqualTo("Id = @Id")); + Assert.That(breakdown.IsUsingWhereClause, Is.True); + }); + } + + #endregion + + #region GetSql Tests + + [Test] + public void GetSql_WithWhere_RendersDeleteFromWhere() + { + // Arrange + var breakdown = new DeleteBreakdown("Users", "Id = @Id"); + + // Act + var sql = breakdown.GetSql(); + + Assert.Multiple(() => + { + // Assert + Assert.That(sql, Does.Contain("DELETE")); + Assert.That(sql, Does.Contain("FROM")); + Assert.That(sql, Does.Contain("Users")); + Assert.That(sql, Does.Contain("WHERE")); + Assert.That(sql, Does.Contain("Id = @Id")); + }); + } + + [Test] + public void GetSql_WithoutWhere_OmitsWhereKeyword() + { + // Arrange + var breakdown = new DeleteBreakdown("Users", string.Empty); + + // Act + var sql = breakdown.GetSql(); + + Assert.Multiple(() => + { + // Assert + Assert.That(sql, Does.Contain("DELETE")); + Assert.That(sql, Does.Not.Contain("WHERE")); + }); + } + + [Test] + public void GetSql_WithDeleteClause_RendersAlias() + { + // Arrange + var breakdown = new DeleteBreakdown("Users u", "u.Id = 1") + { + DeleteClause = { Clause = "u" } + }; + + // Act + var sql = breakdown.GetSql(); + + // Assert + Assert.That(sql, Does.Contain("DELETE")); + Assert.That(sql, Does.Contain("u")); + } + + #endregion + + #region Parse Tests + + [Test] + public void Parse_SimpleDeleteFrom_PopulatesClauses() + { + // Arrange + var input = "DELETE FROM Users WHERE Id = 1"; + + // Act + var breakdown = DeleteBreakdown.Parse(input); + + Assert.Multiple(() => + { + // Assert + Assert.That(breakdown.FromClause.Clause, Is.EqualTo("Users")); + Assert.That(breakdown.WhereClause.Clause, Is.EqualTo("Id = 1")); + }); + } + + [Test] + public void Parse_DeleteWithAlias_PopulatesDeleteAndFromClauses() + { + // Arrange - DELETE u FROM Users u WHERE ... uses the alias pattern + var input = "DELETE u FROM Users u WHERE u.Id = 1"; + + // Act + var breakdown = DeleteBreakdown.Parse(input); + + Assert.Multiple(() => + { + // Assert + Assert.That(breakdown.DeleteClause.Clause, Is.EqualTo("u")); + Assert.That(breakdown.FromClause.Clause, Is.EqualTo("Users u")); + Assert.That(breakdown.WhereClause.Clause, Is.EqualTo("u.Id = 1")); + }); + } + + [Test] + public void Parse_DeleteWithoutWhere_LeavesWhereEmpty() + { + // Arrange + var input = "DELETE FROM Users"; + + // Act + var breakdown = DeleteBreakdown.Parse(input); + + Assert.Multiple(() => + { + // Assert + Assert.That(breakdown.FromClause.Clause, Is.EqualTo("Users")); + Assert.That(breakdown.IsUsingWhereClause, Is.False); + }); + } + + [Test] + public void Parse_NullOrWhitespace_ThrowsArgumentNullException() + { + // Act & Assert + Assert.Throws(() => DeleteBreakdown.Parse(null!)); + } + + [Test] + public void Parse_NotADeleteStatement_ThrowsFormatException() + { + // Act & Assert + Assert.Throws(() => DeleteBreakdown.Parse("SELECT * FROM Users")); + } + + [Test] + public void TryParse_WrongPrefix_ReturnsFalseWithMessage() + { + // Act + var success = DeleteBreakdown.TryParse("UPDATE Users SET Id = 1", out _, out var error); + + Assert.Multiple(() => + { + // Assert + Assert.That(success, Is.False); + Assert.That(error, Does.Contain("DELETE")); + }); + } + + [Test] + public void TryParse_ValidDelete_ReturnsTrue() + { + // Act + var success = DeleteBreakdown.TryParse("DELETE FROM Users WHERE Id = 1", out var breakdown); + + Assert.Multiple(() => + { + // Assert + Assert.That(success, Is.True); + Assert.That(breakdown.FromClause.Clause, Is.EqualTo("Users")); + }); + } + + #endregion +} diff --git a/tests/Strata.SqlTools.SqlServer.Tests/SqlServer/InsertBreakdownTests.cs b/tests/Strata.SqlTools.SqlServer.Tests/SqlServer/InsertBreakdownTests.cs new file mode 100644 index 0000000..5d2db83 --- /dev/null +++ b/tests/Strata.SqlTools.SqlServer.Tests/SqlServer/InsertBreakdownTests.cs @@ -0,0 +1,209 @@ +using Strata.SqlTools.Breakdowns.SqlServer; + +namespace Strata.SqlTools.SqlBreakdown.Tests.SqlServer; + +[TestFixture] +public class InsertBreakdownTests +{ + #region Constructor Tests + + [Test] + public void Constructor_Default_InitializesEmptyClauses() + { + // Arrange & Act + var breakdown = new InsertBreakdown(); + + Assert.Multiple(() => + { + // Assert + Assert.That(breakdown.TableName.Clause, Is.EqualTo(string.Empty)); + Assert.That(breakdown.InsertIntoClause.Clause, Is.EqualTo(string.Empty)); + Assert.That(breakdown.ValuesClause.Clause, Is.EqualTo(string.Empty)); + Assert.That(breakdown.SetupClauses, Is.Empty); + Assert.That(breakdown.FinishClauses, Is.Empty); + }); + } + + [Test] + public void Constructor_WithClauses_SetsProperties() + { + // Arrange & Act + var breakdown = new InsertBreakdown("Users", "Id, Name", "@Id, @Name"); + + Assert.Multiple(() => + { + // Assert + Assert.That(breakdown.TableName.Clause, Is.EqualTo("Users")); + Assert.That(breakdown.InsertIntoClause.Clause, Is.EqualTo("Id, Name")); + Assert.That(breakdown.ValuesClause.Clause, Is.EqualTo("@Id, @Name")); + }); + } + + [Test] + public void Constructor_WithClauses_ExtractsComments() + { + // Arrange & Act + var breakdown = new InsertBreakdown("Users /* target */", "Id, Name", "@Id, @Name"); + + Assert.Multiple(() => + { + // Assert + Assert.That(breakdown.TableName.Clause, Is.EqualTo("Users")); + Assert.That(breakdown.TableName.Comment, Does.Contain("target")); + }); + } + + [Test] + public void Constructor_WithColumnNames_GeneratesBracketedColumnsAndAtParameters() + { + // Arrange & Act + var breakdown = new InsertBreakdown("Users", new List { "Id", "Name" }); + + Assert.Multiple(() => + { + // Assert + Assert.That(breakdown.TableName.Clause, Is.EqualTo("Users")); + Assert.That(breakdown.InsertIntoClause.Clause, Is.EqualTo("[Id],[Name]")); + Assert.That(breakdown.ValuesClause.Clause, Is.EqualTo("@Id,@Name")); + }); + } + + #endregion + + #region GetSql Tests + + [Test] + public void GetSql_RendersInsertIntoTableColumnsValues() + { + // Arrange + var breakdown = new InsertBreakdown("Users", "Id, Name", "@Id, @Name"); + + // Act + var sql = breakdown.GetSql(); + + Assert.Multiple(() => + { + // Assert + Assert.That(sql, Does.Contain("INSERT INTO")); + Assert.That(sql, Does.Contain("Users")); + Assert.That(sql, Does.Contain("Id, Name")); + Assert.That(sql, Does.Contain("VALUES")); + Assert.That(sql, Does.Contain("@Id, @Name")); + }); + } + + [Test] + public void ToString_ReturnsGeneratedSql() + { + // Arrange + var breakdown = new InsertBreakdown("Users", "Id", "@Id"); + + // Act & Assert + Assert.That(breakdown.ToString(), Does.Contain("INSERT INTO")); + } + + #endregion + + #region Parse Tests + + [Test] + public void Parse_ValidInsert_PopulatesClauses() + { + // Arrange + var input = "INSERT INTO Users (Id, Name) VALUES (1, 'Bob')"; + + // Act + var breakdown = InsertBreakdown.Parse(input); + + Assert.Multiple(() => + { + // Assert + Assert.That(breakdown.TableName.Clause, Is.EqualTo("Users")); + Assert.That(breakdown.InsertIntoClause.Clause, Is.EqualTo("Id, Name")); + Assert.That(breakdown.ValuesClause.Clause, Is.EqualTo("1, 'Bob'")); + }); + } + + [Test] + public void Parse_NullOrWhitespace_ThrowsArgumentNullException() + { + // Act & Assert + Assert.Throws(() => InsertBreakdown.Parse(" ")); + } + + [Test] + public void Parse_NotAnInsertStatement_ThrowsFormatException() + { + // Act & Assert + Assert.Throws(() => InsertBreakdown.Parse("SELECT * FROM Users")); + } + + [Test] + public void TryParse_ValidInsert_ReturnsTrue() + { + // Act + var success = InsertBreakdown.TryParse("INSERT INTO Users (Id) VALUES (1)", out var breakdown); + + Assert.Multiple(() => + { + // Assert + Assert.That(success, Is.True); + Assert.That(breakdown.TableName.Clause, Is.EqualTo("Users")); + }); + } + + [Test] + public void TryParse_WrongPrefix_ReturnsFalseWithMessage() + { + // Act + var success = InsertBreakdown.TryParse("UPDATE Users SET Id = 1", out _, out var error); + + Assert.Multiple(() => + { + // Assert + Assert.That(success, Is.False); + Assert.That(error, Does.Contain("INSERT INTO")); + }); + } + + [Test] + public void TryParse_MalformedInsert_ReturnsFalse() + { + // Act - matches the INSERT INTO prefix but not the column/values grammar + var success = InsertBreakdown.TryParse("INSERT INTO Users", out _, out var error); + + Assert.Multiple(() => + { + // Assert + Assert.That(success, Is.False); + Assert.That(error, Is.Not.Null.And.Not.Empty); + }); + } + + [Test] + public void GetSql_IncludeSetupFinish_RendersSetupAndFinishClauses() + { + // Arrange + var breakdown = new InsertBreakdown("Users", "Id", "@Id") + { + SetupClauses = { "SET NOCOUNT ON" }, + FinishClauses = { "SELECT SCOPE_IDENTITY()" } + }; + + // Act + var withClauses = breakdown.GetSql(includeSetupFinish: true); + var withoutClauses = breakdown.GetSql(includeSetupFinish: false); + + Assert.Multiple(() => + { + // Assert + Assert.That(withClauses, Does.Contain("SET NOCOUNT ON")); + Assert.That(withClauses, Does.Contain("SELECT SCOPE_IDENTITY()")); + Assert.That(withClauses, Does.Contain("INSERT INTO")); + Assert.That(withoutClauses, Does.Not.Contain("SET NOCOUNT ON")); + Assert.That(withoutClauses, Does.Not.Contain("SELECT SCOPE_IDENTITY()")); + }); + } + + #endregion +} diff --git a/tests/Strata.SqlTools.SqlServer.Tests/SqlServer/ProcedureBreakdownTests.cs b/tests/Strata.SqlTools.SqlServer.Tests/SqlServer/ProcedureBreakdownTests.cs new file mode 100644 index 0000000..30a2166 --- /dev/null +++ b/tests/Strata.SqlTools.SqlServer.Tests/SqlServer/ProcedureBreakdownTests.cs @@ -0,0 +1,223 @@ +using Strata.SqlTools.Breakdowns.SqlServer; + +namespace Strata.SqlTools.SqlBreakdown.Tests.SqlServer; + +[TestFixture] +public class ProcedureBreakdownTests +{ + #region Constructor Tests + + [Test] + public void Constructor_Default_InitializesEmptyState() + { + // Arrange & Act + var breakdown = new ProcedureBreakdown(); + + Assert.Multiple(() => + { + // Assert + Assert.That(breakdown.ProcedureName.Clause, Is.EqualTo(string.Empty)); + Assert.That(breakdown.Parameters, Is.Empty); + Assert.That(breakdown.IsUsingParameters, Is.False); + }); + } + + [Test] + public void Constructor_WithName_SetsProcedureName() + { + // Arrange & Act + var breakdown = new ProcedureBreakdown("dbo.GetUsers"); + + // Assert + Assert.That(breakdown.ProcedureName.Clause, Is.EqualTo("dbo.GetUsers")); + } + + [Test] + public void Constructor_WithParameters_SetsParameters() + { + // Arrange + var parameters = new Dictionary { ["@Id"] = "1" }; + + // Act + var breakdown = new ProcedureBreakdown("dbo.GetUser", parameters); + + Assert.Multiple(() => + { + // Assert + Assert.That(breakdown.IsUsingParameters, Is.True); + Assert.That(breakdown.Parameters["@Id"], Is.EqualTo("1")); + }); + } + + #endregion + + #region AddParameter Tests + + [Test] + public void AddParameter_WithoutAtPrefix_AddsAtPrefix() + { + // Arrange + var breakdown = new ProcedureBreakdown("dbo.GetUser"); + + // Act + breakdown.AddParameter("Id", "1"); + + // Assert + Assert.That(breakdown.Parameters.ContainsKey("@Id"), Is.True); + } + + [Test] + public void AddParameter_WithAtPrefix_KeepsSinglePrefix() + { + // Arrange + var breakdown = new ProcedureBreakdown("dbo.GetUser"); + + // Act + breakdown.AddParameter("@Id", "1"); + + // Assert + Assert.That(breakdown.Parameters.ContainsKey("@Id"), Is.True); + } + + [Test] + public void AddParameter_NullName_ThrowsArgumentNullException() + { + // Arrange + var breakdown = new ProcedureBreakdown("dbo.GetUser"); + + // Act & Assert + Assert.Throws(() => breakdown.AddParameter(" ", "1")); + } + + #endregion + + #region GetSql Tests + + [Test] + public void GetSql_WithoutParameters_RendersExecName() + { + // Arrange + var breakdown = new ProcedureBreakdown("dbo.GetUsers"); + + // Act + var sql = breakdown.GetSql(); + + Assert.Multiple(() => + { + // Assert + Assert.That(sql, Does.Contain("EXEC")); + Assert.That(sql, Does.Contain("dbo.GetUsers")); + }); + } + + [Test] + public void GetSql_WithParameters_RendersNamedAssignments() + { + // Arrange + var breakdown = new ProcedureBreakdown("dbo.GetUser"); + breakdown.AddParameter("@Id", "1"); + + // Act + var sql = breakdown.GetSql(); + + Assert.Multiple(() => + { + // Assert + Assert.That(sql, Does.Contain("EXEC")); + Assert.That(sql, Does.Contain("@Id = 1")); + }); + } + + #endregion + + #region Parse Tests + + [Test] + public void Parse_ExecWithoutParameters_PopulatesName() + { + // Arrange + var input = "EXEC dbo.GetUsers"; + + // Act + var breakdown = ProcedureBreakdown.Parse(input); + + Assert.Multiple(() => + { + // Assert + Assert.That(breakdown.ProcedureName.Clause, Is.EqualTo("dbo.GetUsers")); + Assert.That(breakdown.IsUsingParameters, Is.False); + }); + } + + [Test] + public void Parse_ExecWithParameters_PopulatesParameters() + { + // Arrange + var input = "EXEC dbo.GetUser @Id = 1, @Active = 1"; + + // Act + var breakdown = ProcedureBreakdown.Parse(input); + + Assert.Multiple(() => + { + // Assert + Assert.That(breakdown.ProcedureName.Clause, Is.EqualTo("dbo.GetUser")); + Assert.That(breakdown.Parameters["@Id"], Is.EqualTo("1")); + Assert.That(breakdown.Parameters["@Active"], Is.EqualTo("1")); + }); + } + + [Test] + public void Parse_ExecuteKeyword_IsAccepted() + { + // Act + var breakdown = ProcedureBreakdown.Parse("EXECUTE dbo.GetUsers"); + + // Assert + Assert.That(breakdown.ProcedureName.Clause, Is.EqualTo("dbo.GetUsers")); + } + + [Test] + public void Parse_NullOrWhitespace_ThrowsArgumentNullException() + { + // Act & Assert + Assert.Throws(() => ProcedureBreakdown.Parse(" ")); + } + + [Test] + public void Parse_NotAnExecStatement_ThrowsFormatException() + { + // Act & Assert + Assert.Throws(() => ProcedureBreakdown.Parse("SELECT * FROM Users")); + } + + [Test] + public void TryParse_WrongPrefix_ReturnsFalseWithMessage() + { + // Act + var success = ProcedureBreakdown.TryParse("SELECT 1", out _, out var error); + + Assert.Multiple(() => + { + // Assert + Assert.That(success, Is.False); + Assert.That(error, Does.Contain("EXEC")); + }); + } + + [Test] + public void TryParse_ValidExec_ReturnsTrue() + { + // Act + var success = ProcedureBreakdown.TryParse("EXEC dbo.GetUsers", out var breakdown); + + Assert.Multiple(() => + { + // Assert + Assert.That(success, Is.True); + Assert.That(breakdown.ProcedureName.Clause, Is.EqualTo("dbo.GetUsers")); + }); + } + + #endregion +} diff --git a/tests/Strata.SqlTools.SqlServer.Tests/SqlServer/UpdateBreakdownTests.cs b/tests/Strata.SqlTools.SqlServer.Tests/SqlServer/UpdateBreakdownTests.cs new file mode 100644 index 0000000..93d98da --- /dev/null +++ b/tests/Strata.SqlTools.SqlServer.Tests/SqlServer/UpdateBreakdownTests.cs @@ -0,0 +1,208 @@ +using Strata.SqlTools.Breakdowns.SqlServer; + +namespace Strata.SqlTools.SqlBreakdown.Tests.SqlServer; + +[TestFixture] +public class UpdateBreakdownTests +{ + #region Constructor Tests + + [Test] + public void Constructor_Default_InitializesEmptyClauses() + { + // Arrange & Act + var breakdown = new UpdateBreakdown(); + + Assert.Multiple(() => + { + // Assert + Assert.That(breakdown.TableName.Clause, Is.EqualTo(string.Empty)); + Assert.That(breakdown.SetClause.Clause, Is.EqualTo(string.Empty)); + Assert.That(breakdown.FromClause.Clause, Is.EqualTo(string.Empty)); + Assert.That(breakdown.WhereClause.Clause, Is.EqualTo(string.Empty)); + Assert.That(breakdown.IsUsingFromClause, Is.False); + Assert.That(breakdown.IsUsingWhereClause, Is.False); + }); + } + + [Test] + public void Constructor_WithClauses_SetsProperties() + { + // Arrange & Act + var breakdown = new UpdateBreakdown("Users", "Name = @Name", "Id = @Id"); + + Assert.Multiple(() => + { + // Assert + Assert.That(breakdown.TableName.Clause, Is.EqualTo("Users")); + Assert.That(breakdown.SetClause.Clause, Is.EqualTo("Name = @Name")); + Assert.That(breakdown.WhereClause.Clause, Is.EqualTo("Id = @Id")); + Assert.That(breakdown.IsUsingWhereClause, Is.True); + }); + } + + #endregion + + #region GetSql Tests + + [Test] + public void GetSql_WithWhere_RendersUpdateSetWhere() + { + // Arrange + var breakdown = new UpdateBreakdown("Users", "Name = @Name", "Id = @Id"); + + // Act + var sql = breakdown.GetSql(); + + Assert.Multiple(() => + { + // Assert + Assert.That(sql, Does.Contain("UPDATE")); + Assert.That(sql, Does.Contain("Users")); + Assert.That(sql, Does.Contain("SET")); + Assert.That(sql, Does.Contain("Name = @Name")); + Assert.That(sql, Does.Contain("WHERE")); + Assert.That(sql, Does.Contain("Id = @Id")); + }); + } + + [Test] + public void GetSql_WithoutWhere_OmitsWhereKeyword() + { + // Arrange + var breakdown = new UpdateBreakdown("Users", "Name = @Name", string.Empty); + + // Act + var sql = breakdown.GetSql(); + + Assert.Multiple(() => + { + // Assert + Assert.That(sql, Does.Contain("UPDATE")); + Assert.That(sql, Does.Not.Contain("WHERE")); + }); + } + + [Test] + public void GetSql_WithFromClause_RendersFromSection() + { + // Arrange + var breakdown = new UpdateBreakdown("u", "u.Name = s.Name", "u.Id = s.Id") + { + FromClause = { Clause = "Users u INNER JOIN Staging s ON u.Id = s.Id" } + }; + + // Act + var sql = breakdown.GetSql(); + + Assert.Multiple(() => + { + // Assert + Assert.That(breakdown.IsUsingFromClause, Is.True); + Assert.That(sql, Does.Contain("FROM")); + Assert.That(sql, Does.Contain("INNER JOIN Staging")); + }); + } + + #endregion + + #region Parse Tests + + [Test] + public void Parse_SimpleUpdate_PopulatesClauses() + { + // Arrange + var input = "UPDATE Users SET Name = 'Bob' WHERE Id = 1"; + + // Act + var breakdown = UpdateBreakdown.Parse(input); + + Assert.Multiple(() => + { + // Assert + Assert.That(breakdown.TableName.Clause, Is.EqualTo("Users")); + Assert.That(breakdown.SetClause.Clause, Is.EqualTo("Name = 'Bob'")); + Assert.That(breakdown.WhereClause.Clause, Is.EqualTo("Id = 1")); + }); + } + + [Test] + public void Parse_UpdateWithFrom_PopulatesFromClause() + { + // Arrange + var input = "UPDATE u SET u.Name = s.Name FROM Users u INNER JOIN Staging s ON u.Id = s.Id WHERE s.Active = 1"; + + // Act + var breakdown = UpdateBreakdown.Parse(input); + + Assert.Multiple(() => + { + // Assert + Assert.That(breakdown.TableName.Clause, Is.EqualTo("u")); + Assert.That(breakdown.IsUsingFromClause, Is.True); + Assert.That(breakdown.FromClause.Clause, Does.Contain("INNER JOIN Staging")); + Assert.That(breakdown.WhereClause.Clause, Is.EqualTo("s.Active = 1")); + }); + } + + [Test] + public void Parse_UpdateWithoutWhere_LeavesWhereEmpty() + { + // Arrange + var input = "UPDATE Users SET Name = 'Bob'"; + + // Act + var breakdown = UpdateBreakdown.Parse(input); + + Assert.Multiple(() => + { + // Assert + Assert.That(breakdown.SetClause.Clause, Is.EqualTo("Name = 'Bob'")); + Assert.That(breakdown.IsUsingWhereClause, Is.False); + }); + } + + [Test] + public void Parse_NullOrWhitespace_ThrowsArgumentNullException() + { + // Act & Assert + Assert.Throws(() => UpdateBreakdown.Parse(string.Empty)); + } + + [Test] + public void Parse_NotAnUpdateStatement_ThrowsFormatException() + { + // Act & Assert + Assert.Throws(() => UpdateBreakdown.Parse("DELETE FROM Users")); + } + + [Test] + public void TryParse_WrongPrefix_ReturnsFalseWithMessage() + { + // Act + var success = UpdateBreakdown.TryParse("SELECT * FROM Users", out _, out var error); + + Assert.Multiple(() => + { + // Assert + Assert.That(success, Is.False); + Assert.That(error, Does.Contain("UPDATE")); + }); + } + + [Test] + public void TryParse_ValidUpdate_ReturnsTrue() + { + // Act + var success = UpdateBreakdown.TryParse("UPDATE Users SET Name = 'Bob' WHERE Id = 1", out var breakdown); + + Assert.Multiple(() => + { + // Assert + Assert.That(success, Is.True); + Assert.That(breakdown.TableName.Clause, Is.EqualTo("Users")); + }); + } + + #endregion +}