Applied via `dotnet format analyzers --diagnostics CA1861 --severity info`,
plus manual cleanup:
- Renamed two cryptic fixer-generated field names:
- QueryBreakdownCollection.stringArray -> SnowflakeFunctionNames (and
inlined the now-redundant local alias)
- ExpressionObjectTests.arg2 -> NotInValues
- Deduped three identical `separator = ['\r','\n']` fields the fixer
emitted in the same test class (kept the first declaration; the other
two test methods now reuse it).
8 files touched.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1949 lines
69 KiB
C#
1949 lines
69 KiB
C#
using Strata.SqlTools.Breakdowns.Snowflake;
|
|
|
|
namespace Strata.SqlTools.SqlBreakdown.Tests.Snowflake;
|
|
|
|
[TestFixture]
|
|
public class QueryBreakdownTests
|
|
{
|
|
#region Constructor Tests
|
|
|
|
[Test]
|
|
public void Constructor_Default_InitializesEmptyProperties()
|
|
{
|
|
// Arrange & Act
|
|
var queryBreakdown = new QueryBreakdown();
|
|
|
|
// Assert
|
|
Assert.That(queryBreakdown.SelectClause.Clause, Is.EqualTo(string.Empty));
|
|
Assert.That(queryBreakdown.FromClause.Clause, Is.EqualTo(string.Empty));
|
|
Assert.That(queryBreakdown.WhereClause.Clause, Is.EqualTo(string.Empty));
|
|
Assert.That(queryBreakdown.OrderByClause.Clause, Is.EqualTo(string.Empty));
|
|
Assert.That(queryBreakdown.GroupByClause.Clause, Is.EqualTo(string.Empty));
|
|
Assert.That(queryBreakdown.HavingClause.Clause, Is.EqualTo(string.Empty));
|
|
}
|
|
|
|
[Test]
|
|
public void Constructor_WithSelectAndFrom_SetsProperties()
|
|
{
|
|
// Arrange
|
|
var selectClause = "ID, NAME";
|
|
var fromClause = "USERS";
|
|
|
|
// Act
|
|
var queryBreakdown = new QueryBreakdown(selectClause, fromClause);
|
|
|
|
// Assert
|
|
Assert.That(queryBreakdown.SelectClause.Clause, Is.EqualTo(selectClause));
|
|
Assert.That(queryBreakdown.FromClause.Clause, Is.EqualTo(fromClause));
|
|
}
|
|
|
|
[Test]
|
|
public void Constructor_WithSelectFromAndWhere_SetsProperties()
|
|
{
|
|
// Arrange
|
|
var selectClause = "ID, NAME";
|
|
var fromClause = "USERS";
|
|
var whereClause = "ACTIVE = 1";
|
|
|
|
// Act
|
|
var queryBreakdown = new QueryBreakdown(selectClause, fromClause, whereClause);
|
|
|
|
// Assert
|
|
Assert.That(queryBreakdown.SelectClause.Clause, Is.EqualTo(selectClause));
|
|
Assert.That(queryBreakdown.FromClause.Clause, Is.EqualTo(fromClause));
|
|
Assert.That(queryBreakdown.WhereClause.Clause, Is.EqualTo(whereClause));
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region GetSql Tests - Snowflake Formatting
|
|
|
|
[Test]
|
|
public void GetSql_SimpleSelect_UsesSnowflakeFormatting()
|
|
{
|
|
// Arrange
|
|
var queryBreakdown = new QueryBreakdown("ID, NAME", "USERS");
|
|
|
|
// Act
|
|
var sql = queryBreakdown.GetSql();
|
|
|
|
// Assert
|
|
Assert.That(sql, Does.Contain("SELECT"));
|
|
Assert.That(sql, Does.Contain("ID, NAME"));
|
|
Assert.That(sql, Does.Contain("FROM"));
|
|
Assert.That(sql, Does.Contain("USERS"));
|
|
// Verify 4-space indentation (Snowflake style)
|
|
Assert.That(sql, Does.Contain(" "));
|
|
}
|
|
|
|
[Test]
|
|
public void GetSql_WithAllClauses_GeneratesCorrectSnowflakeSql()
|
|
{
|
|
// Arrange
|
|
var queryBreakdown = new QueryBreakdown("DEPARTMENT, COUNT(*) AS EMP_COUNT", "EMPLOYEES");
|
|
queryBreakdown.WhereClause.Clause = "ACTIVE = 1";
|
|
queryBreakdown.GroupByClause.Clause = "DEPARTMENT";
|
|
queryBreakdown.HavingClause.Clause = "COUNT(*) > 5";
|
|
queryBreakdown.OrderByClause.Clause = "DEPARTMENT ASC";
|
|
|
|
// Act
|
|
var sql = queryBreakdown.GetSql();
|
|
|
|
// Assert
|
|
Assert.That(sql, Does.Contain("SELECT"));
|
|
Assert.That(sql, Does.Contain("FROM"));
|
|
Assert.That(sql, Does.Contain("WHERE"));
|
|
Assert.That(sql, Does.Contain("GROUP BY"));
|
|
Assert.That(sql, Does.Contain("HAVING"));
|
|
Assert.That(sql, Does.Contain("ORDER BY"));
|
|
}
|
|
|
|
private static readonly char[] separator = new[] { '\r', '\n' };
|
|
|
|
[Test]
|
|
public void GetSql_WithSingleWithClause_UsesSnowflakeIndentation()
|
|
{
|
|
// Arrange
|
|
var mainQueryBreakdown = new QueryBreakdown("P.*, PS.TOTAL_QTY", "PRODUCTS P INNER JOIN PRODUCT_SUMMARY PS ON P.PRODUCT_ID = PS.PRODUCT_ID");
|
|
var cte = new QueryBreakdown("PRODUCT_ID, SUM(QUANTITY) AS TOTAL_QTY", "ORDER_DETAILS");
|
|
cte.GroupByClause.Clause = "PRODUCT_ID";
|
|
mainQueryBreakdown.AddWithClause("PRODUCT_SUMMARY", cte);
|
|
|
|
// Act
|
|
var sql = mainQueryBreakdown.GetSql();
|
|
|
|
// Assert
|
|
Assert.That(sql, Does.Contain("WITH"));
|
|
Assert.That(sql, Does.Contain("PRODUCT_SUMMARY AS ("));
|
|
// Verify 4-space Snowflake indentation
|
|
var lines = sql.Split(separator, StringSplitOptions.RemoveEmptyEntries);
|
|
var indentedLines = lines.Where(l => l.StartsWith(" ")).ToList();
|
|
Assert.That(indentedLines.Count, Is.GreaterThan(0));
|
|
}
|
|
|
|
[Test]
|
|
public void GetSql_WithMultipleWithClauses_GeneratesCorrectSnowflakeSql()
|
|
{
|
|
// Arrange
|
|
var mainQueryBreakdown = new QueryBreakdown("*", "CTE3");
|
|
var cte1 = new QueryBreakdown("COL1", "TABLE1");
|
|
var cte2 = new QueryBreakdown("COL2", "TABLE2");
|
|
var cte3 = new QueryBreakdown("COL3", "TABLE3");
|
|
|
|
mainQueryBreakdown.AddWithClause("CTE1", cte1);
|
|
mainQueryBreakdown.AddWithClause("CTE2", cte2);
|
|
mainQueryBreakdown.AddWithClause("CTE3", cte3);
|
|
|
|
// Act
|
|
var sql = mainQueryBreakdown.GetSql();
|
|
|
|
// Assert
|
|
Assert.That(sql, Does.Contain("WITH"));
|
|
Assert.That(sql, Does.Contain("CTE1 AS ("));
|
|
Assert.That(sql, Does.Contain("CTE2 AS ("));
|
|
Assert.That(sql, Does.Contain("CTE3 AS ("));
|
|
|
|
// Verify commas between CTEs by checking the pattern
|
|
// CTE1 should be followed by ), then comma, then CTE2
|
|
var cte1Index = sql.IndexOf("CTE1 AS (");
|
|
var cte2Index = sql.IndexOf("CTE2 AS (", cte1Index);
|
|
var betweenCtes = sql.Substring(cte1Index, cte2Index - cte1Index);
|
|
Assert.That(betweenCtes, Does.Contain("),"), "CTE1 should be followed by a comma before CTE2");
|
|
|
|
// Verify comma between CTE2 and CTE3
|
|
var cte3Index = sql.IndexOf("CTE3 AS (", cte2Index);
|
|
var betweenCte2And3 = sql.Substring(cte2Index, cte3Index - cte2Index);
|
|
Assert.That(betweenCte2And3, Does.Contain("),"), "CTE2 should be followed by a comma before CTE3");
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region AddWithClause Tests - Snowflake Specific
|
|
|
|
[Test]
|
|
public void AddWithClause_WithSnowflakeSql_DefaultsToSnowflakeParsing()
|
|
{
|
|
// Arrange
|
|
var mainQueryBreakdown = new QueryBreakdown("*", "PRODUCTS P INNER JOIN PRODUCT_ORDERS PO ON P.PRODUCT_ID = PO.PRODUCT_ID");
|
|
var cteSql = "SELECT PRODUCT_ID, COUNT(*) AS ORDER_COUNT FROM ORDERS GROUP BY PRODUCT_ID";
|
|
|
|
// Act - should default to isMicrosoftSql = false
|
|
mainQueryBreakdown.AddWithClause("PRODUCT_ORDERS", cteSql);
|
|
|
|
// Assert
|
|
Assert.That(mainQueryBreakdown.WithClauses.Count, Is.EqualTo(1));
|
|
Assert.That(mainQueryBreakdown.WithClauses[0].TableName, Is.EqualTo("PRODUCT_ORDERS"));
|
|
}
|
|
|
|
[Test]
|
|
public void AddWithClause_WithMicrosoftSqlTrue_UsesTSqlParsing()
|
|
{
|
|
// Arrange
|
|
var mainQueryBreakdown = new QueryBreakdown("*", "Products");
|
|
var cteSql = "SELECT ProductID, COUNT(*) AS OrderCount FROM Orders GROUP BY ProductID";
|
|
|
|
// Act
|
|
mainQueryBreakdown.AddWithClause("ProductOrders", cteSql, isMicrosoftSql: true);
|
|
|
|
// Assert
|
|
Assert.That(mainQueryBreakdown.WithClauses.Count, Is.EqualTo(1));
|
|
Assert.That(mainQueryBreakdown.WithClauses[0].TableName, Is.EqualTo("ProductOrders"));
|
|
}
|
|
|
|
[Test]
|
|
public void AddWithClause_WithQueryBreakdown_AddsToInheritedList()
|
|
{
|
|
// Arrange
|
|
var mainQueryBreakdown = new QueryBreakdown("*", "PRODUCTS");
|
|
var cteQuery = new QueryBreakdown("PRODUCT_ID, COUNT(*) AS ORDER_COUNT", "ORDERS");
|
|
cteQuery.GroupByClause.Clause = "PRODUCT_ID";
|
|
|
|
// Act
|
|
mainQueryBreakdown.AddWithClause("PRODUCT_ORDERS", cteQuery);
|
|
|
|
// Assert
|
|
Assert.That(mainQueryBreakdown.WithClauses.Count, Is.EqualTo(1));
|
|
Assert.That(mainQueryBreakdown.WithClauses[0].Query, Is.EqualTo(cteQuery));
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region CTE Integration Tests - Snowflake Specific
|
|
|
|
[Test]
|
|
public void CTE_NestedCTEs_WithParameterFlow()
|
|
{
|
|
// Arrange: Create inner CTE with Snowflake parameters
|
|
var innerCte = new QueryBreakdown("ID, NAME, CREATED_DATE", "USERS", "CREATED_DATE >= :start_date");
|
|
innerCte.AddParameter(":start_date", DateTime.Today.AddDays(-30));
|
|
|
|
// Arrange: Create middle CTE that references inner CTE
|
|
var middleCte = new QueryBreakdown("ID, NAME", "RECENT_USERS", "ID > :min_user_id");
|
|
middleCte.AddParameter(":min_user_id", 100);
|
|
|
|
// Arrange: Create main query that references middle CTE
|
|
var mainQuery = new QueryBreakdown("COUNT(*) AS ACTIVE_USER_COUNT", "FILTERED_USERS");
|
|
mainQuery.AddWithClause("RECENT_USERS", innerCte);
|
|
mainQuery.AddWithClause("FILTERED_USERS", middleCte);
|
|
|
|
// Act
|
|
var sql = mainQuery.GetSql();
|
|
var innerParams = innerCte.ParameterList.Count();
|
|
var middleParams = middleCte.ParameterList.Count();
|
|
|
|
// Assert - Each CTE maintains its own parameters
|
|
Assert.That(innerParams, Is.EqualTo(1));
|
|
Assert.That(middleParams, Is.EqualTo(1));
|
|
// SQL should contain both WITH clauses
|
|
Assert.That(sql, Does.Contain("WITH"));
|
|
Assert.That(sql, Does.Contain("RECENT_USERS"));
|
|
Assert.That(sql, Does.Contain("FILTERED_USERS"));
|
|
}
|
|
|
|
[Test]
|
|
public void CTE_MultipleCTEs_WithComplexQueries()
|
|
{
|
|
// Arrange: Create CTE 1 - Active users (uppercase for Snowflake style)
|
|
var activeCte = new QueryBreakdown("ID, EMAIL, STATUS", "USERS", "STATUS = 'active'");
|
|
|
|
// Arrange: Create CTE 2 - Recent orders with aggregation
|
|
var ordersCte = new QueryBreakdown("USER_ID, COUNT(*) AS ORDER_COUNT, SUM(TOTAL) AS TOTAL_SPENT", "ORDERS");
|
|
ordersCte.GroupByClause.Clause = "USER_ID";
|
|
ordersCte.HavingClause.Clause = "COUNT(*) > 0";
|
|
|
|
// Arrange: Create main query combining both CTEs
|
|
var mainQuery = new QueryBreakdown("A.ID, A.EMAIL, O.ORDER_COUNT, O.TOTAL_SPENT",
|
|
"ACTIVE_USERS A LEFT JOIN USER_ORDERS O ON A.ID = O.USER_ID");
|
|
mainQuery.OrderByClause.Clause = "O.TOTAL_SPENT DESC";
|
|
|
|
mainQuery.AddWithClause("ACTIVE_USERS", activeCte);
|
|
mainQuery.AddWithClause("USER_ORDERS", ordersCte);
|
|
|
|
// Act
|
|
var sql = mainQuery.GetSql();
|
|
|
|
// Assert
|
|
Assert.That(sql, Does.Contain("WITH"));
|
|
Assert.That(sql, Does.Contain("ACTIVE_USERS"));
|
|
Assert.That(sql, Does.Contain("USER_ORDERS"));
|
|
Assert.That(sql, Does.Contain("GROUP BY"));
|
|
Assert.That(sql, Does.Contain("HAVING"));
|
|
Assert.That(sql, Does.Contain("LEFT JOIN"));
|
|
Assert.That(sql, Does.Contain("ORDER BY"));
|
|
}
|
|
|
|
[Test]
|
|
public void CTE_GetSql_GeneratesProperCTESyntax()
|
|
{
|
|
// Arrange
|
|
var cteQuery = new QueryBreakdown("PRODUCT_ID, SUM(QUANTITY) AS TOTAL_QUANTITY", "ORDER_DETAILS");
|
|
cteQuery.GroupByClause.Clause = "PRODUCT_ID";
|
|
|
|
var mainQuery = new QueryBreakdown("P.PRODUCT_NAME, PT.TOTAL_QUANTITY",
|
|
"PRODUCTS P INNER JOIN PRODUCT_TOTALS PT ON P.PRODUCT_ID = PT.PRODUCT_ID");
|
|
mainQuery.AddWithClause("PRODUCT_TOTALS", cteQuery);
|
|
|
|
// Act
|
|
var sql = mainQuery.GetSql();
|
|
|
|
// Assert - Verify WITH clause appears before main SELECT
|
|
var withIndex = sql.IndexOf("WITH");
|
|
var mainSelectIndex = sql.LastIndexOf("SELECT");
|
|
Assert.That(withIndex, Is.GreaterThan(-1));
|
|
Assert.That(mainSelectIndex, Is.GreaterThan(-1));
|
|
Assert.That(withIndex, Is.LessThan(mainSelectIndex));
|
|
|
|
// Assert - Verify CTE definition
|
|
Assert.That(sql, Does.Contain("PRODUCT_TOTALS"));
|
|
Assert.That(sql, Does.Contain("AS"));
|
|
Assert.That(sql, Does.Contain("PRODUCT_ID"));
|
|
Assert.That(sql, Does.Contain("TOTAL_QUANTITY"));
|
|
}
|
|
|
|
[Test]
|
|
public void CTE_WithClauseProperties_RoundTrip()
|
|
{
|
|
// Arrange: Create original query
|
|
var cteQuery = new QueryBreakdown("ID, NAME", "EMPLOYEES", "DEPARTMENT = 'Sales'");
|
|
var mainQuery = new QueryBreakdown("*", "SALES_TEAM");
|
|
mainQuery.AddWithClause("SALES_TEAM", cteQuery);
|
|
|
|
// Act: Get SQL and verify CTE is properly included
|
|
var sql = mainQuery.GetSql();
|
|
var withClause = mainQuery.WithClauses[0];
|
|
|
|
// Assert: Verify WithClause properties
|
|
Assert.That(withClause.TableName, Is.EqualTo("SALES_TEAM"));
|
|
Assert.That(withClause.Query, Is.Not.Null);
|
|
Assert.That(withClause.Sql, Is.Not.Null);
|
|
Assert.That(withClause.Sql?.SelectClause?.Clause, Contains.Substring("ID, NAME"));
|
|
Assert.That(withClause.Sql?.FromClause?.Clause, Contains.Substring("EMPLOYEES"));
|
|
Assert.That(withClause.Sql?.WhereClause?.Clause, Contains.Substring("DEPARTMENT = 'Sales'"));
|
|
}
|
|
|
|
[Test]
|
|
public void CTE_IsUsingWithClause_ReturnsCorrectStatus()
|
|
{
|
|
// Arrange
|
|
var query = new QueryBreakdown("*", "DATA");
|
|
|
|
// Act & Assert - No WITH clauses yet
|
|
Assert.That(query.IsUsingWithClause, Is.False);
|
|
|
|
// Act - Add a CTE
|
|
var cte = new QueryBreakdown("ID, NAME", "SOURCE_TABLE");
|
|
query.AddWithClause("SOURCE_CTE", cte);
|
|
|
|
// Assert - Now using WITH clause
|
|
Assert.That(query.IsUsingWithClause, Is.True);
|
|
Assert.That(query.WithClauses.Count, Is.EqualTo(1));
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region CTE Parameter Inheritance Tests - Snowflake
|
|
|
|
[Test]
|
|
public void GetMergedParameters_WithNoCTEs_ReturnsMainQueryParameters()
|
|
{
|
|
// Arrange
|
|
var query = new QueryBreakdown("*", "USERS");
|
|
query.AddParameter("UserId", 123);
|
|
query.AddParameter("ActiveStatus", true);
|
|
|
|
// Act
|
|
var merged = query.GetMergedParameters();
|
|
|
|
// Assert
|
|
// Snowflake stores parameters in both @ and : formats, so count is doubled
|
|
Assert.That(merged.Count, Is.GreaterThanOrEqualTo(2));
|
|
Assert.That(merged.ContainsKey("@UserId") || merged.ContainsKey(":UserId"), Is.True);
|
|
Assert.That(merged.ContainsKey("@ActiveStatus") || merged.ContainsKey(":ActiveStatus"), Is.True);
|
|
}
|
|
|
|
[Test]
|
|
public void GetMergedParameters_WithSingleCTE_MergesParameters()
|
|
{
|
|
// Arrange
|
|
var cte = new QueryBreakdown("ID, NAME", "BASE_USERS", "ACTIVE = 1");
|
|
cte.AddParameter("MinDate", DateTime.Today.AddDays(-30));
|
|
|
|
var mainQuery = new QueryBreakdown("*", "ACTIVE_USERS");
|
|
mainQuery.AddParameter("ResultLimit", 100);
|
|
mainQuery.AddWithClause("ACTIVE_USERS", cte);
|
|
|
|
// Act
|
|
var merged = mainQuery.GetMergedParameters();
|
|
|
|
// Assert
|
|
// Should have parameters from both CTE and main query
|
|
var keys = merged.Keys.ToList();
|
|
Assert.That(keys.Any(k => k.Contains("MinDate")));
|
|
Assert.That(keys.Any(k => k.Contains("ResultLimit")));
|
|
}
|
|
|
|
[Test]
|
|
public void GetMergedParameters_WithMultipleCTEs_MergesAllParameters()
|
|
{
|
|
// Arrange
|
|
var cte1 = new QueryBreakdown("ID, STATUS", "USERS", "STATUS = 'active'");
|
|
cte1.AddParameter("Status1", "active");
|
|
|
|
var cte2 = new QueryBreakdown("USER_ID, CREATED", "ORDERS", "CREATED >= :DATE");
|
|
cte2.AddParameter("MinDate", DateTime.Today.AddDays(-30));
|
|
|
|
var mainQuery = new QueryBreakdown("*", "RESULTS");
|
|
mainQuery.AddParameter("ResultLimit", 50);
|
|
mainQuery.AddWithClause("ACTIVE_USERS", cte1);
|
|
mainQuery.AddWithClause("RECENT_ORDERS", cte2);
|
|
|
|
// Act
|
|
var merged = mainQuery.GetMergedParameters();
|
|
|
|
// Assert
|
|
var keys = merged.Keys.ToList();
|
|
Assert.That(keys.Any(k => k.Contains("Status1")));
|
|
Assert.That(keys.Any(k => k.Contains("MinDate")));
|
|
Assert.That(keys.Any(k => k.Contains("ResultLimit")));
|
|
}
|
|
|
|
[Test]
|
|
public void GetMergedParameters_WithParameterConflict_MainQueryTakesPrecedence()
|
|
{
|
|
// Arrange
|
|
var cte = new QueryBreakdown("ID", "USERS");
|
|
cte.AddParameter("QueryLimit", 10); // CTE sets to 10
|
|
|
|
var mainQuery = new QueryBreakdown("*", "FINAL_RESULT");
|
|
mainQuery.AddParameter("QueryLimit", 100); // Main query sets to 100
|
|
mainQuery.AddWithClause("BASE_DATA", cte);
|
|
|
|
// Act
|
|
var merged = mainQuery.GetMergedParameters();
|
|
|
|
// Assert
|
|
// Main query value should win
|
|
var keys = merged.Keys.Where(k => k.Contains("QueryLimit")).ToList();
|
|
Assert.That(keys.Count, Is.GreaterThan(0));
|
|
// All QueryLimit entries should be 100 (main query's value)
|
|
foreach (var key in keys)
|
|
{
|
|
Assert.That(merged[key], Is.EqualTo(100));
|
|
}
|
|
}
|
|
|
|
[Test]
|
|
public void GetMergedParameters_WithNestedCTEs_CollectsAllParameters()
|
|
{
|
|
// Arrange: Create deeply nested CTEs
|
|
var innerCte = new QueryBreakdown("ID", "BASE_TABLE");
|
|
innerCte.AddParameter("InnerParam", "inner_value");
|
|
|
|
var middleCte = new QueryBreakdown("ID, NAME", "LAYER1");
|
|
middleCte.AddParameter("MiddleParam", "middle_value");
|
|
middleCte.AddWithClause("LAYER1_SOURCE", innerCte);
|
|
|
|
var mainQuery = new QueryBreakdown("ID, NAME, STATUS", "LAYER2");
|
|
mainQuery.AddParameter("MainParam", "main_value");
|
|
mainQuery.AddWithClause("LAYER2_SOURCE", middleCte);
|
|
|
|
// Act
|
|
var merged = mainQuery.GetMergedParameters();
|
|
|
|
// Assert - All parameters from all levels should be present
|
|
var keys = merged.Keys.ToList();
|
|
Assert.That(keys.Any(k => k.Contains("InnerParam")));
|
|
Assert.That(keys.Any(k => k.Contains("MiddleParam")));
|
|
Assert.That(keys.Any(k => k.Contains("MainParam")));
|
|
}
|
|
|
|
[Test]
|
|
public void GetMergedParameters_PreservesParameterTypes()
|
|
{
|
|
// Arrange
|
|
var cte = new QueryBreakdown("ID", "USERS");
|
|
cte.AddParameter("IntValue", 123);
|
|
cte.AddParameter("StringValue", "test");
|
|
cte.AddParameter("DateValue", DateTime.Today);
|
|
cte.AddParameter("DecimalValue", 99.99m);
|
|
|
|
var mainQuery = new QueryBreakdown("*", "RESULTS");
|
|
mainQuery.AddWithClause("SOURCE", cte);
|
|
|
|
// Act
|
|
var merged = mainQuery.GetMergedParameters();
|
|
|
|
// Assert
|
|
// Verify all values are present and types are preserved
|
|
var intValues = merged.Values.OfType<int>().Count();
|
|
var stringValues = merged.Values.OfType<string>().Count();
|
|
var dateValues = merged.Values.OfType<DateTime>().Count();
|
|
var decimalValues = merged.Values.OfType<decimal>().Count();
|
|
|
|
Assert.That(intValues, Is.GreaterThan(0), "Should have at least one int value");
|
|
Assert.That(stringValues, Is.GreaterThan(0), "Should have at least one string value");
|
|
Assert.That(dateValues, Is.GreaterThan(0), "Should have at least one DateTime value");
|
|
Assert.That(decimalValues, Is.GreaterThan(0), "Should have at least one decimal value");
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Parse Tests - Snowflake Specific
|
|
|
|
[Test]
|
|
public void Parse_SimpleSnowflakeSelect_ParsesCorrectly()
|
|
{
|
|
// Arrange
|
|
var sql = "SELECT ID, NAME FROM USERS";
|
|
|
|
// Act
|
|
var queryBreakdown = QueryBreakdown.Parse(sql, false);
|
|
|
|
// Assert
|
|
Assert.That(queryBreakdown.SelectClause.Clause?.Trim(), Does.Contain("ID, NAME"));
|
|
Assert.That(queryBreakdown.FromClause.Clause?.Trim(), Does.Contain("USERS"));
|
|
}
|
|
|
|
[Test]
|
|
public void Parse_WithLimitClause_ParsesCorrectly()
|
|
{
|
|
// Arrange
|
|
var sql = "SELECT ID, NAME FROM USERS ORDER BY NAME LIMIT 10";
|
|
|
|
// Act
|
|
var queryBreakdown = QueryBreakdown.Parse(sql, false);
|
|
|
|
// Assert
|
|
Assert.That(queryBreakdown.OrderByClause.Clause?.Trim(), Does.Contain("NAME"));
|
|
Assert.That(queryBreakdown.OrderByClause.Clause?.Trim(), Does.Contain("LIMIT 10"));
|
|
}
|
|
|
|
[Test]
|
|
public void Parse_WithQualifyClause_ParsesCorrectly()
|
|
{
|
|
// Arrange
|
|
var sql = @"SELECT ID, NAME, ROW_NUMBER() OVER (PARTITION BY DEPT ORDER BY SALARY DESC) AS RN
|
|
FROM EMPLOYEES
|
|
QUALIFY RN = 1";
|
|
|
|
// Act
|
|
var queryBreakdown = QueryBreakdown.Parse(sql, false);
|
|
|
|
// Assert
|
|
Assert.That(queryBreakdown.SelectClause.Clause?.Trim(), Does.Contain("ROW_NUMBER()"));
|
|
// QUALIFY is typically parsed as part of WHERE or as a separate extension in Snowflake
|
|
}
|
|
|
|
[Test]
|
|
public void Parse_WithColonParameters_ExtractsSnowflakeParameters()
|
|
{
|
|
// Arrange
|
|
var sql = "SELECT ID, NAME FROM USERS WHERE USER_ID = :userId AND STATUS = :status";
|
|
|
|
// Act
|
|
var queryBreakdown = QueryBreakdown.Parse(sql, false);
|
|
|
|
// Assert
|
|
Assert.That(queryBreakdown.Parameters, Does.ContainKey(":userId"));
|
|
Assert.That(queryBreakdown.Parameters, Does.ContainKey(":status"));
|
|
}
|
|
|
|
[Test]
|
|
public void Parse_WithAtParameters_ExtractsAndNormalizesParameters()
|
|
{
|
|
// Arrange
|
|
var sql = "SELECT ID, NAME FROM USERS WHERE USER_ID = :userId AND STATUS = @status";
|
|
|
|
// Act
|
|
var queryBreakdown = QueryBreakdown.Parse(sql, false);
|
|
|
|
// Assert
|
|
Assert.That(queryBreakdown.Parameters, Does.ContainKey(":userId"));
|
|
Assert.That(queryBreakdown.Parameters, Does.Not.ContainKey("@status"));
|
|
}
|
|
|
|
[Test]
|
|
public void Parse_WithMixedParameters_ExtractsBothFormats()
|
|
{
|
|
// Arrange
|
|
var sql = "SELECT ID, NAME FROM USERS WHERE USER_ID = :userId AND STATUS = @status";
|
|
|
|
// Act
|
|
var queryBreakdown = QueryBreakdown.Parse(sql, false);
|
|
|
|
// Assert
|
|
Assert.That(queryBreakdown.Parameters.Count, Is.GreaterThan(0));
|
|
}
|
|
|
|
[Test]
|
|
public void Parse_WithMicrosoftSqlTrue_DelegatesToBaseClass()
|
|
{
|
|
// Arrange
|
|
var sql = "SELECT ID, Name FROM Users WHERE UserID = @UserId";
|
|
|
|
// Act
|
|
var queryBreakdown = QueryBreakdown.Parse(sql, true);
|
|
|
|
// Assert
|
|
Assert.That(queryBreakdown, Is.Not.Null);
|
|
Assert.That(queryBreakdown, Is.InstanceOf<QueryBreakdown>());
|
|
Assert.That(queryBreakdown.Parameters, Does.ContainKey("@UserId"));
|
|
}
|
|
|
|
[Test]
|
|
public void Parse_WithSnowflakeWithClause_ParsesCorrectly()
|
|
{
|
|
// Arrange
|
|
var sql = @"WITH PRODUCT_SUMMARY AS (
|
|
SELECT PRODUCT_ID, SUM(QUANTITY) AS TOTAL_QTY
|
|
FROM ORDER_DETAILS
|
|
GROUP BY PRODUCT_ID
|
|
)
|
|
SELECT P.*, PS.TOTAL_QTY
|
|
FROM PRODUCTS P
|
|
INNER JOIN PRODUCT_SUMMARY PS ON P.PRODUCT_ID = PS.PRODUCT_ID";
|
|
|
|
// Act
|
|
var queryBreakdown = QueryBreakdown.Parse(sql, false);
|
|
|
|
// Assert
|
|
Assert.That(queryBreakdown.IsUsingWithClause, Is.True);
|
|
Assert.That(queryBreakdown.SelectClause.Clause?.Trim(), Does.Contain("P.*, PS.TOTAL_QTY"));
|
|
}
|
|
|
|
[Test]
|
|
public void Parse_WithNullSql_ThrowsArgumentNullException()
|
|
{
|
|
// Act & Assert
|
|
Assert.Throws<ArgumentNullException>(() => QueryBreakdown.Parse(null!));
|
|
}
|
|
|
|
[Test]
|
|
public void Parse_WithInvalidSql_ThrowsFormatException()
|
|
{
|
|
// Arrange
|
|
var sql = "INVALID SNOWFLAKE SQL";
|
|
|
|
// Act & Assert
|
|
Assert.Throws<FormatException>(() => QueryBreakdown.Parse(sql, false));
|
|
}
|
|
|
|
[Test]
|
|
public void TryParse_WithValidSnowflakeSql_ReturnsTrue()
|
|
{
|
|
// Arrange
|
|
var sql = "SELECT ID FROM USERS";
|
|
|
|
// Act
|
|
var success = QueryBreakdown.TryParse(sql, out var queryBreakdown, false);
|
|
|
|
// Assert
|
|
Assert.That(success, Is.True);
|
|
Assert.That(queryBreakdown, Is.Not.Null);
|
|
Assert.That(queryBreakdown, Is.InstanceOf<QueryBreakdown>());
|
|
}
|
|
|
|
[Test]
|
|
public void TryParse_WithInvalidSql_ReturnsFalse()
|
|
{
|
|
// Arrange
|
|
var sql = "INVALID SQL";
|
|
|
|
// Act
|
|
var success = QueryBreakdown.TryParse(sql, out var queryBreakdown, out var error, false);
|
|
|
|
// Assert
|
|
Assert.That(success, Is.False);
|
|
Assert.That(queryBreakdown, Is.Null);
|
|
Assert.That(error, Is.Not.Null);
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Inheritance Tests
|
|
|
|
[Test]
|
|
public void InheritsFromQueryBreakdown_CanUseBaseClassMethods()
|
|
{
|
|
// Arrange
|
|
var queryBreakdown = new QueryBreakdown("*", "USERS");
|
|
|
|
// Act
|
|
queryBreakdown.AddParameter("UserId", 123);
|
|
queryBreakdown.AddWhereClause("ACTIVE = 1");
|
|
|
|
// Assert
|
|
Assert.That(queryBreakdown.Parameters, Does.ContainKey("@UserId"));
|
|
Assert.That(queryBreakdown.WhereClause.Clause, Does.Contain("ACTIVE = 1"));
|
|
}
|
|
|
|
[Test]
|
|
public void InheritsWithClauses_SharesSameStorage()
|
|
{
|
|
// Arrange
|
|
var queryBreakdown = new QueryBreakdown("*", "PRODUCTS");
|
|
var cte = new QueryBreakdown("*", "ORDERS");
|
|
|
|
// Act
|
|
queryBreakdown.AddWithClause("ORDER_CTE", cte);
|
|
|
|
// Assert
|
|
Assert.That(queryBreakdown.WithClauses.Count, Is.EqualTo(1));
|
|
Assert.That(queryBreakdown.IsUsingWithClause, Is.True);
|
|
}
|
|
|
|
[Test]
|
|
public void Clone_CreatesSnowflakeInstance()
|
|
{
|
|
// Arrange
|
|
var original = new QueryBreakdown("*", "USERS");
|
|
original.WhereClause.Clause = "ACTIVE = 1";
|
|
|
|
// Act
|
|
var clone = (QueryBreakdown)original.Clone();
|
|
|
|
// Assert
|
|
Assert.That(clone, Is.InstanceOf<QueryBreakdown>());
|
|
Assert.That(clone.SelectClause.Clause, Is.EqualTo(original.SelectClause.Clause));
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Integration Tests
|
|
|
|
[Test]
|
|
public void ComplexQuery_WithMultipleCTEsAndAllClauses_WorksCorrectly()
|
|
{
|
|
// Arrange
|
|
var mainQueryBreakdown = new QueryBreakdown("*", "FINAL_DATA FD");
|
|
mainQueryBreakdown.WhereClause.Clause = "FD.TOTAL_SALES > :minSales";
|
|
mainQueryBreakdown.OrderByClause.Clause = "FD.TOTAL_SALES DESC LIMIT 100";
|
|
|
|
var salesCte = new QueryBreakdown(
|
|
"REGION_ID, SUM(AMOUNT) AS TOTAL_SALES",
|
|
"SALES");
|
|
salesCte.WhereClause.Clause = "SALE_DATE >= :startDate";
|
|
salesCte.GroupByClause.Clause = "REGION_ID";
|
|
|
|
var productCte = new QueryBreakdown("*", "PRODUCTS");
|
|
productCte.WhereClause.Clause = "ACTIVE = 1";
|
|
|
|
// Act
|
|
mainQueryBreakdown.AddWithClause("SALES_SUMMARY", salesCte);
|
|
mainQueryBreakdown.AddWithClause("ACTIVE_PRODUCTS", productCte);
|
|
mainQueryBreakdown.AddParameter("minSales", 10000);
|
|
mainQueryBreakdown.AddParameter("startDate", new DateTime(2025, 1, 1, 0, 0, 0, DateTimeKind.Utc));
|
|
|
|
var sql = mainQueryBreakdown.GetSql();
|
|
|
|
// Assert
|
|
Assert.That(sql, Does.Contain("WITH"));
|
|
Assert.That(sql, Does.Contain("SALES_SUMMARY AS ("));
|
|
Assert.That(sql, Does.Contain("ACTIVE_PRODUCTS AS ("));
|
|
Assert.That(sql, Does.Contain("LIMIT 100"));
|
|
Assert.That(mainQueryBreakdown.Parameters, Does.ContainKey(":minSales"));
|
|
Assert.That(mainQueryBreakdown.Parameters, Does.ContainKey(":startDate"));
|
|
}
|
|
|
|
[Test]
|
|
public void NestedCTEs_ParseAndGenerate_WorksCorrectly()
|
|
{
|
|
// Arrange - Create nested WITH clause structure
|
|
var innerCte = new QueryBreakdown("ORDER_ID, CUSTOMER_ID", "ORDERS");
|
|
var middleCte = new QueryBreakdown("CUSTOMER_ID, COUNT(*) AS ORDER_COUNT", "INNER_CTE");
|
|
middleCte.GroupByClause.Clause = "CUSTOMER_ID";
|
|
middleCte.AddWithClause("INNER_CTE", innerCte);
|
|
|
|
var mainQueryBreakdown = new QueryBreakdown("*", "MIDDLE_CTE");
|
|
mainQueryBreakdown.AddWithClause("MIDDLE_CTE", middleCte);
|
|
|
|
// Act
|
|
var sql = mainQueryBreakdown.GetSql();
|
|
|
|
// Assert
|
|
Assert.That(sql, Does.Contain("WITH"));
|
|
Assert.That(sql, Does.Contain("MIDDLE_CTE AS ("));
|
|
Assert.That(mainQueryBreakdown.WithClauses.Count, Is.EqualTo(1));
|
|
}
|
|
|
|
[Test]
|
|
public void RoundTrip_ParseAndGetSql_ProducesSimilarSql()
|
|
{
|
|
// Arrange
|
|
var originalSql = @"SELECT ID, NAME, EMAIL
|
|
FROM USERS
|
|
WHERE ACTIVE = 1
|
|
ORDER BY NAME ASC";
|
|
|
|
// Act
|
|
var parsed = QueryBreakdown.Parse(originalSql, false);
|
|
var generatedSql = parsed.GetSql();
|
|
|
|
// Assert
|
|
Assert.That(generatedSql, Does.Contain("SELECT"));
|
|
Assert.That(generatedSql, Does.Contain("ID, NAME, EMAIL"));
|
|
Assert.That(generatedSql, Does.Contain("FROM"));
|
|
Assert.That(generatedSql, Does.Contain("USERS"));
|
|
Assert.That(generatedSql, Does.Contain("WHERE"));
|
|
Assert.That(generatedSql, Does.Contain("ACTIVE = 1"));
|
|
Assert.That(generatedSql, Does.Contain("ORDER BY"));
|
|
Assert.That(generatedSql, Does.Contain("NAME ASC"));
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Edge Cases
|
|
|
|
[Test]
|
|
public void Parse_WithComplexSubqueries_HandlesCorrectly()
|
|
{
|
|
// Arrange
|
|
var sql = @"SELECT U.ID, U.NAME,
|
|
(SELECT COUNT(*) FROM ORDERS O WHERE O.USER_ID = U.ID) AS ORDER_COUNT
|
|
FROM USERS U
|
|
WHERE U.ID IN (SELECT DISTINCT USER_ID FROM ORDERS WHERE AMOUNT > 100)";
|
|
|
|
// Act
|
|
var queryBreakdown = QueryBreakdown.Parse(sql, false);
|
|
|
|
// Assert
|
|
Assert.That(queryBreakdown, Is.Not.Null);
|
|
Assert.That(queryBreakdown.SelectClause.Clause, Does.Contain("(SELECT COUNT(*)"));
|
|
}
|
|
|
|
[Test]
|
|
public void GetSql_EmptyQuery_GeneratesMinimalSql()
|
|
{
|
|
// Arrange
|
|
var queryBreakdown = new QueryBreakdown();
|
|
|
|
// Act
|
|
var sql = queryBreakdown.GetSql();
|
|
|
|
// Assert
|
|
Assert.That(sql, Does.Contain("SELECT"));
|
|
// Should have minimal structure even when empty
|
|
}
|
|
|
|
[Test]
|
|
public void Parse_WithCommentsInSql_HandlesGracefully()
|
|
{
|
|
// Arrange
|
|
var sql = @"-- This is a comment
|
|
SELECT ID, NAME -- inline comment
|
|
FROM USERS
|
|
/* Multi-line
|
|
comment */
|
|
WHERE ACTIVE = 1";
|
|
|
|
// Act & Assert
|
|
// May throw or handle comments - test should verify behavior
|
|
Assert.DoesNotThrow(() =>
|
|
{
|
|
_ = QueryBreakdown.Parse(sql, false);
|
|
});
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Outlier Conditions - Snowflake-Specific Edge Cases
|
|
|
|
[Test]
|
|
public void Parse_WithDoubleColonCast_ParsesCorrectly()
|
|
{
|
|
// Arrange - Snowflake uses :: for casting
|
|
var sql = "SELECT ID::VARCHAR, AMOUNT::DECIMAL(10,2) FROM ORDERS";
|
|
|
|
// Act
|
|
var queryBreakdown = QueryBreakdown.Parse(sql, false);
|
|
|
|
// Assert
|
|
Assert.That(queryBreakdown.SelectClause.Clause, Does.Contain("::"));
|
|
}
|
|
|
|
[Test]
|
|
public void Parse_WithVariantDataType_ParsesCorrectly()
|
|
{
|
|
// Arrange
|
|
var sql = "SELECT JSON_DATA:name::STRING, JSON_DATA:age::NUMBER FROM JSON_TABLE";
|
|
|
|
// Act
|
|
var queryBreakdown = QueryBreakdown.Parse(sql, false);
|
|
|
|
// Assert
|
|
Assert.That(queryBreakdown.SelectClause.Clause, Does.Contain("JSON_DATA:name"));
|
|
}
|
|
|
|
[Test]
|
|
public void Parse_WithFlattenFunction_ParsesCorrectly()
|
|
{
|
|
// Arrange
|
|
var sql = @"SELECT
|
|
F.VALUE:id::NUMBER AS ID,
|
|
F.VALUE:name::STRING AS NAME
|
|
FROM JSON_TABLE,
|
|
LATERAL FLATTEN(input => JSON_DATA:items) F";
|
|
|
|
// Act
|
|
var queryBreakdown = QueryBreakdown.Parse(sql, false);
|
|
|
|
// Assert
|
|
Assert.That(queryBreakdown.SelectClause.Clause, Does.Contain("F.VALUE"));
|
|
Assert.That(queryBreakdown.FromClause.Clause, Does.Contain("FLATTEN"));
|
|
}
|
|
|
|
[Test]
|
|
public void Parse_WithSampleClause_ParsesCorrectly()
|
|
{
|
|
// Arrange
|
|
var sql = "SELECT * FROM LARGE_TABLE SAMPLE (10 ROWS)";
|
|
|
|
// Act
|
|
var queryBreakdown = QueryBreakdown.Parse(sql, false);
|
|
|
|
// Assert
|
|
Assert.That(queryBreakdown.FromClause.Clause, Does.Contain("SAMPLE"));
|
|
}
|
|
|
|
[Test]
|
|
public void Parse_WithAtTimestamp_ParsesCorrectly()
|
|
{
|
|
// Arrange - Snowflake Time Travel
|
|
var sql = "SELECT * FROM USERS AT(TIMESTAMP => '2024-01-01 00:00:00'::TIMESTAMP)";
|
|
|
|
// Act
|
|
var queryBreakdown = QueryBreakdown.Parse(sql, false);
|
|
|
|
// Assert
|
|
Assert.That(queryBreakdown.FromClause.Clause, Does.Contain("AT(TIMESTAMP"));
|
|
}
|
|
|
|
[Test]
|
|
public void Parse_WithMatchRecognize_HandlesGracefully()
|
|
{
|
|
// Arrange - Complex Snowflake pattern matching
|
|
var sql = @"SELECT * FROM EVENTS
|
|
MATCH_RECOGNIZE(
|
|
PARTITION BY USER_ID
|
|
ORDER BY EVENT_TIME
|
|
MEASURES A.EVENT_TIME AS START_TIME
|
|
PATTERN (A B+ C)
|
|
DEFINE B AS B.EVENT_TYPE = 'CLICK'
|
|
)";
|
|
|
|
// Act & Assert
|
|
Assert.DoesNotThrow(() => QueryBreakdown.Parse(sql, false));
|
|
}
|
|
|
|
[Test]
|
|
public void Parse_WithWindowFunctionAndQualify_ParsesCorrectly()
|
|
{
|
|
// Arrange
|
|
var sql = @"SELECT
|
|
ID,
|
|
NAME,
|
|
ROW_NUMBER() OVER (PARTITION BY DEPARTMENT ORDER BY SALARY DESC) AS RN
|
|
FROM EMPLOYEES
|
|
QUALIFY ROW_NUMBER() OVER (PARTITION BY DEPARTMENT ORDER BY SALARY DESC) = 1";
|
|
|
|
// Act
|
|
var queryBreakdown = QueryBreakdown.Parse(sql, false);
|
|
|
|
// Assert
|
|
Assert.That(queryBreakdown.SelectClause.Clause, Does.Contain("ROW_NUMBER()"));
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Outlier Conditions - Parameter Formats
|
|
|
|
[Test]
|
|
public void Parse_WithManyColonParameters_ExtractsAll()
|
|
{
|
|
// Arrange
|
|
var sql = @"SELECT * FROM ORDERS
|
|
WHERE USER_ID = :userId
|
|
AND ORDER_DATE >= :startDate
|
|
AND ORDER_DATE <= :endDate
|
|
AND STATUS = :status
|
|
AND AMOUNT > :minAmount";
|
|
|
|
// Act
|
|
var queryBreakdown = QueryBreakdown.Parse(sql, false);
|
|
|
|
// Assert
|
|
Assert.That(queryBreakdown.Parameters.Count, Is.GreaterThanOrEqualTo(5));
|
|
Assert.That(queryBreakdown.Parameters, Does.ContainKey(":userId"));
|
|
Assert.That(queryBreakdown.Parameters, Does.ContainKey(":startDate"));
|
|
Assert.That(queryBreakdown.Parameters, Does.ContainKey(":endDate"));
|
|
Assert.That(queryBreakdown.Parameters, Does.ContainKey(":status"));
|
|
Assert.That(queryBreakdown.Parameters, Does.ContainKey(":minAmount"));
|
|
}
|
|
|
|
[Test]
|
|
public void Parse_WithParameterInJsonPath_DoesNotExtractAsParameter()
|
|
{
|
|
// Arrange - Colon in JSON path should not be treated as parameter
|
|
var sql = "SELECT JSON_DATA:user:name FROM JSON_TABLE";
|
|
|
|
// Act
|
|
var queryBreakdown = QueryBreakdown.Parse(sql, false);
|
|
|
|
// Assert - Should not extract 'user' or 'name' as parameters
|
|
Assert.That(queryBreakdown.SelectClause.Clause, Does.Contain("JSON_DATA:user:name"));
|
|
}
|
|
|
|
[Test]
|
|
public void AddParameter_WithColonPrefix_StoresWithColon()
|
|
{
|
|
// Arrange
|
|
var queryBreakdown = new QueryBreakdown();
|
|
|
|
// Act
|
|
queryBreakdown.AddParameter(":snowflakeParam", 123);
|
|
|
|
// Assert
|
|
// Base class normalizes to @, but Snowflake might handle differently
|
|
Assert.That(queryBreakdown.Parameters.Count, Is.GreaterThan(0));
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Outlier Conditions - Snowflake String Handling
|
|
|
|
[Test]
|
|
public void Parse_WithDollarQuotedString_HandlesGracefully()
|
|
{
|
|
// Arrange - Snowflake supports $$ for string literals
|
|
var sql = "SELECT $$This is a 'quoted' string$$ AS TEXT FROM DUAL";
|
|
|
|
// Act & Assert
|
|
Assert.DoesNotThrow(() => QueryBreakdown.Parse(sql, false));
|
|
}
|
|
|
|
[Test]
|
|
public void Parse_WithEscapedSingleQuotes_ParsesCorrectly()
|
|
{
|
|
// Arrange
|
|
var sql = "SELECT NAME FROM USERS WHERE NAME = 'O''Brien'";
|
|
|
|
// Act
|
|
var queryBreakdown = QueryBreakdown.Parse(sql, false);
|
|
|
|
// Assert
|
|
Assert.That(queryBreakdown.WhereClause.Clause, Does.Contain("O''Brien"));
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Outlier Conditions - Complex Snowflake Queries
|
|
|
|
[Test]
|
|
public void Parse_WithMultipleSetOperators_HandlesGracefully()
|
|
{
|
|
// Arrange
|
|
var sql = @"SELECT ID, NAME FROM USERS_A
|
|
UNION ALL
|
|
SELECT ID, NAME FROM USERS_B
|
|
EXCEPT
|
|
SELECT ID, NAME FROM USERS_C
|
|
INTERSECT
|
|
SELECT ID, NAME FROM USERS_D";
|
|
|
|
// Act & Assert
|
|
Assert.DoesNotThrow(() => QueryBreakdown.Parse(sql, false));
|
|
}
|
|
|
|
[Test]
|
|
public void Parse_WithPivotClause_HandlesGracefully()
|
|
{
|
|
// Arrange
|
|
var sql = @"SELECT * FROM SALES
|
|
PIVOT(SUM(AMOUNT) FOR MONTH IN ('JAN', 'FEB', 'MAR'))";
|
|
|
|
// Act & Assert
|
|
Assert.DoesNotThrow(() => QueryBreakdown.Parse(sql, false));
|
|
}
|
|
|
|
[Test]
|
|
public void GetSql_WithVeryLongSnowflakeQuery_GeneratesCorrectly()
|
|
{
|
|
// Arrange - Build a complex query with many CTEs
|
|
var mainQueryBreakdown = new QueryBreakdown("*", "FINAL_RESULT");
|
|
|
|
for (int i = 0; i < 15; i++)
|
|
{
|
|
var cte = new QueryBreakdown(
|
|
$"COL{i}, COUNT(*) AS CNT{i}",
|
|
$"TABLE_{i}");
|
|
cte.WhereClause.Clause = $"ACTIVE = 1 AND TYPE = {i}";
|
|
cte.GroupByClause.Clause = $"COL{i}";
|
|
mainQueryBreakdown.AddWithClause($"CTE_{i}", cte);
|
|
}
|
|
|
|
// Act
|
|
var sql = mainQueryBreakdown.GetSql();
|
|
|
|
// Assert
|
|
Assert.That(sql, Does.Contain("CTE_0 AS ("));
|
|
Assert.That(sql, Does.Contain("CTE_14 AS ("));
|
|
Assert.That(sql.Length, Is.GreaterThan(1000));
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Outlier Conditions - Whitespace and Formatting
|
|
|
|
[Test]
|
|
public void Parse_WithExcessiveWhitespace_NormalizesCorrectly()
|
|
{
|
|
// Arrange
|
|
var sql = "SELECT ID, NAME FROM USERS WHERE ACTIVE=1";
|
|
|
|
// Act
|
|
var queryBreakdown = QueryBreakdown.Parse(sql, false);
|
|
|
|
// Assert
|
|
Assert.That(queryBreakdown, Is.Not.Null);
|
|
Assert.That(queryBreakdown.SelectClause.Clause, Does.Contain("ID"));
|
|
}
|
|
|
|
[Test]
|
|
public void GetSql_ComparesIndentationToBaseClass_UsesFourSpaces()
|
|
{
|
|
// Arrange
|
|
var QueryBreakdown = new QueryBreakdown("ID", "USERS");
|
|
var baseQueryBreakdown = new QueryBreakdown("ID", "USERS");
|
|
|
|
// Act
|
|
var snowflakeSql = QueryBreakdown.GetSql();
|
|
_ = baseQueryBreakdown.GetSql();
|
|
|
|
// Assert - Snowflake should use 4-space indent, base uses 5-space
|
|
var snowflakeLines = snowflakeSql.Split(separator, StringSplitOptions.RemoveEmptyEntries);
|
|
var snowflakeIndentedLines = snowflakeLines.Where(l => l.StartsWith(" ") && !l.StartsWith(" ")).ToList();
|
|
Assert.That(snowflakeIndentedLines.Count, Is.GreaterThan(0), "Snowflake should use 4-space indentation");
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Outlier Conditions - Boundary and Error Cases
|
|
|
|
[Test]
|
|
public void Parse_WithOnlySelectNoFrom_HandlesGracefully()
|
|
{
|
|
// Arrange - Snowflake allows SELECT without FROM using DUAL or for constants
|
|
var sql = "SELECT 1 AS ONE, CURRENT_TIMESTAMP() AS NOW";
|
|
|
|
// Act & Assert
|
|
Assert.DoesNotThrow(() => QueryBreakdown.Parse(sql, false));
|
|
}
|
|
|
|
[Test]
|
|
public void TryParse_WithMalformedSnowflakeSQL_ReturnsFalseWithError()
|
|
{
|
|
// Arrange
|
|
var sql = "SELET * FORM USERS WERE ID = 1"; // Intentional typos
|
|
|
|
// Act
|
|
var success = QueryBreakdown.TryParse(sql, out _, out var error, false);
|
|
|
|
// Assert
|
|
Assert.That(success, Is.False);
|
|
Assert.That(error, Is.Not.Null.And.Not.Empty);
|
|
}
|
|
|
|
[Test]
|
|
public void Parse_WithVeryLongTableName_ParsesCorrectly()
|
|
{
|
|
// Arrange
|
|
var longTableName = "VERY_LONG_TABLE_NAME_" + new string('X', 200);
|
|
var sql = $"SELECT ID FROM {longTableName}";
|
|
|
|
// Act
|
|
var queryBreakdown = QueryBreakdown.Parse(sql, false);
|
|
|
|
// Assert
|
|
Assert.That(queryBreakdown.FromClause.Clause, Does.Contain(longTableName));
|
|
}
|
|
|
|
[Test]
|
|
public void MergeWith_SnowflakeQueryWithBaseQuery_WorksCorrectly()
|
|
{
|
|
// Arrange
|
|
var QueryBreakdown = new QueryBreakdown("*", "USERS U");
|
|
var baseQuery = new QueryBreakdown();
|
|
baseQuery.FromClause.Clause = "INNER JOIN ORDERS O ON U.ID = O.USER_ID";
|
|
|
|
// Act
|
|
QueryBreakdown.MergeWith(baseQuery);
|
|
|
|
// Assert
|
|
Assert.That(QueryBreakdown.FromClause.Clause, Does.Contain("USERS U"));
|
|
Assert.That(QueryBreakdown.FromClause.Clause, Does.Contain("INNER JOIN ORDERS"));
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Outlier Conditions - Real-World Snowflake Scenarios
|
|
|
|
[Test]
|
|
public void Parse_WithSnowflakeStageReference_ParsesCorrectly()
|
|
{
|
|
// Arrange
|
|
var sql = "SELECT $1, $2, $3 FROM @MY_STAGE/data/file.csv";
|
|
|
|
// Act
|
|
var queryBreakdown = QueryBreakdown.Parse(sql, false);
|
|
|
|
// Assert
|
|
Assert.That(queryBreakdown.SelectClause.Clause, Does.Contain("$1"));
|
|
Assert.That(queryBreakdown.FromClause.Clause, Does.Contain("@MY_STAGE"));
|
|
}
|
|
|
|
[Test]
|
|
public void Parse_WithSequenceNextval_ParsesCorrectly()
|
|
{
|
|
// Arrange
|
|
var sql = "SELECT MY_SEQUENCE.NEXTVAL, NAME FROM USERS";
|
|
|
|
// Act
|
|
var queryBreakdown = QueryBreakdown.Parse(sql, false);
|
|
|
|
// Assert
|
|
Assert.That(queryBreakdown.SelectClause.Clause, Does.Contain("NEXTVAL"));
|
|
}
|
|
|
|
[Test]
|
|
public void ComplexRealWorldQuery_WithAllSnowflakeFeatures_ParsesAndGenerates()
|
|
{
|
|
// Arrange - Realistic complex Snowflake query
|
|
var salesCte = new QueryBreakdown(
|
|
"REGION_ID, PRODUCT_ID, SUM(AMOUNT::DECIMAL(18,2)) AS TOTAL_SALES",
|
|
"SALES_DATA");
|
|
salesCte.WhereClause.Clause = "SALE_DATE >= :startDate AND SALE_DATE < :endDate";
|
|
salesCte.GroupByClause.Clause = "REGION_ID, PRODUCT_ID";
|
|
|
|
var topProductsCte = new QueryBreakdown(
|
|
"REGION_ID, PRODUCT_ID, TOTAL_SALES, ROW_NUMBER() OVER (PARTITION BY REGION_ID ORDER BY TOTAL_SALES DESC) AS RN",
|
|
"SALES_SUMMARY");
|
|
|
|
var mainQueryBreakdown = new QueryBreakdown("R.REGION_NAME, T.PRODUCT_ID, T.TOTAL_SALES", "REGIONS R INNER JOIN TOP_PRODUCTS T ON R.REGION_ID = T.REGION_ID");
|
|
mainQueryBreakdown.WhereClause.Clause = "T.RN <= 10";
|
|
mainQueryBreakdown.OrderByClause.Clause = "R.REGION_NAME, T.TOTAL_SALES DESC LIMIT 100";
|
|
|
|
mainQueryBreakdown.AddWithClause("SALES_SUMMARY", salesCte);
|
|
mainQueryBreakdown.AddWithClause("TOP_PRODUCTS", topProductsCte);
|
|
mainQueryBreakdown.AddParameter("startDate", "2024-01-01");
|
|
mainQueryBreakdown.AddParameter("endDate", "2024-12-31");
|
|
|
|
// Act
|
|
var sql = mainQueryBreakdown.GetSql();
|
|
|
|
// Assert
|
|
Assert.That(sql, Does.Contain("WITH"));
|
|
Assert.That(sql, Does.Contain("SALES_SUMMARY AS ("));
|
|
Assert.That(sql, Does.Contain("TOP_PRODUCTS AS ("));
|
|
Assert.That(sql, Does.Contain("ROW_NUMBER()"));
|
|
Assert.That(sql, Does.Contain("LIMIT 100"));
|
|
Assert.That(mainQueryBreakdown.Parameters.Count, Is.GreaterThanOrEqualTo(2));
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Microsoft SQL Server Compatibility Tests
|
|
|
|
[Test]
|
|
public void Parse_MicrosoftSqlQuery_ReturnsQueryBreakdown()
|
|
{
|
|
// Arrange
|
|
var tsqlQuery = "SELECT Id, Name FROM Users WHERE Active = 1";
|
|
|
|
// Act
|
|
var result = QueryBreakdown.Parse(tsqlQuery, isMicrosoftSql: true);
|
|
|
|
// Assert
|
|
Assert.That(result, Is.Not.Null);
|
|
Assert.That(result, Is.InstanceOf<QueryBreakdown>(), "Should return Snowflake QueryBreakdown instance");
|
|
Assert.That(result.SelectClause.Clause?.Trim(), Does.Contain("Id, Name"));
|
|
Assert.That(result.FromClause.Clause?.Trim(), Does.Contain("Users"));
|
|
Assert.That(result.WhereClause.Clause?.Trim(), Does.Contain("Active = 1"));
|
|
}
|
|
|
|
[Test]
|
|
public void Parse_MicrosoftSqlWithParameters_ExtractsParameters()
|
|
{
|
|
// Arrange
|
|
var tsqlQuery = "SELECT ProductId, ProductName FROM Products WHERE CategoryId = @categoryId AND Price > @minPrice";
|
|
|
|
// Act
|
|
var result = QueryBreakdown.Parse(tsqlQuery, isMicrosoftSql: true);
|
|
|
|
// Assert
|
|
Assert.That(result.Parameters, Does.ContainKey("@categoryId"));
|
|
Assert.That(result.Parameters, Does.ContainKey("@minPrice"));
|
|
}
|
|
|
|
[Test]
|
|
public void Parse_MicrosoftSqlWithAllClauses_ParsesAllComponents()
|
|
{
|
|
// Arrange
|
|
var tsqlQuery = @"SELECT DepartmentId, COUNT(*) AS EmployeeCount, AVG(Salary) AS AvgSalary
|
|
FROM Employees
|
|
WHERE Active = 1
|
|
GROUP BY DepartmentId
|
|
HAVING COUNT(*) > 5
|
|
ORDER BY EmployeeCount DESC";
|
|
|
|
// Act
|
|
var result = QueryBreakdown.Parse(tsqlQuery, isMicrosoftSql: true);
|
|
|
|
// Assert
|
|
Assert.That(result.SelectClause.Clause?.Trim(), Does.Contain("DepartmentId"));
|
|
Assert.That(result.SelectClause.Clause?.Trim(), Does.Contain("COUNT(*)"));
|
|
Assert.That(result.FromClause.Clause?.Trim(), Does.Contain("Employees"));
|
|
Assert.That(result.WhereClause.Clause?.Trim(), Does.Contain("Active = 1"));
|
|
Assert.That(result.GroupByClause.Clause?.Trim(), Does.Contain("DepartmentId"));
|
|
Assert.That(result.HavingClause.Clause?.Trim(), Does.Contain("COUNT(*) > 5"));
|
|
Assert.That(result.OrderByClause.Clause?.Trim(), Does.Contain("EmployeeCount DESC"));
|
|
}
|
|
|
|
[Test]
|
|
public void GetSql_FromMicrosoftSqlParsedQuery_GeneratesSnowflakeSyntax()
|
|
{
|
|
// Arrange
|
|
var tsqlQuery = "SELECT Id, Name FROM Users WHERE Active = 1 ORDER BY Name";
|
|
var result = QueryBreakdown.Parse(tsqlQuery, isMicrosoftSql: true);
|
|
|
|
// Act
|
|
var snowflakeSql = result.GetSql();
|
|
|
|
// Assert
|
|
Assert.That(snowflakeSql, Does.Contain("SELECT"));
|
|
Assert.That(snowflakeSql, Does.Contain("FROM"));
|
|
Assert.That(snowflakeSql, Does.Contain("WHERE"));
|
|
Assert.That(snowflakeSql, Does.Contain("ORDER BY"));
|
|
// Verify Snowflake-style formatting (4-space indentation)
|
|
var lines = snowflakeSql.Split(separator, StringSplitOptions.RemoveEmptyEntries);
|
|
var indentedLines = lines.Where(l => l.StartsWith(" ")).ToList();
|
|
Assert.That(indentedLines.Count, Is.GreaterThan(0), "Should have Snowflake-style indentation");
|
|
}
|
|
|
|
[Test]
|
|
public void Parse_MicrosoftSqlWithJoins_ParsesCorrectly()
|
|
{
|
|
// Arrange
|
|
var tsqlQuery = @"SELECT o.OrderId, c.CustomerName, p.ProductName
|
|
FROM Orders o
|
|
INNER JOIN Customers c ON o.CustomerId = c.CustomerId
|
|
LEFT JOIN OrderDetails od ON o.OrderId = od.OrderId
|
|
LEFT JOIN Products p ON od.ProductId = p.ProductId
|
|
WHERE o.OrderDate >= @startDate";
|
|
|
|
// Act
|
|
var result = QueryBreakdown.Parse(tsqlQuery, isMicrosoftSql: true);
|
|
|
|
// Assert
|
|
Assert.That(result.SelectClause.Clause?.Trim(), Does.Contain("o.OrderId"));
|
|
Assert.That(result.FromClause.Clause?.Trim(), Does.Contain("INNER JOIN"));
|
|
Assert.That(result.FromClause.Clause?.Trim(), Does.Contain("LEFT JOIN"));
|
|
Assert.That(result.WhereClause.Clause?.Trim(), Does.Contain("OrderDate"));
|
|
}
|
|
|
|
[Test]
|
|
public void Parse_MicrosoftSqlWithSubquery_ParsesCorrectly()
|
|
{
|
|
// Arrange
|
|
var tsqlQuery = @"SELECT CustomerId, CustomerName
|
|
FROM Customers
|
|
WHERE CustomerId IN (SELECT CustomerId FROM Orders WHERE OrderDate >= @startDate)";
|
|
|
|
// Act
|
|
var result = QueryBreakdown.Parse(tsqlQuery, isMicrosoftSql: true);
|
|
|
|
// Assert
|
|
Assert.That(result.SelectClause.Clause?.Trim(), Does.Contain("CustomerId, CustomerName"));
|
|
Assert.That(result.WhereClause.Clause?.Trim(), Does.Contain("IN"));
|
|
Assert.That(result.WhereClause.Clause?.Trim(), Does.Contain("SELECT"));
|
|
}
|
|
|
|
[Test]
|
|
public void TryParse_MicrosoftSqlValidQuery_ReturnsTrue()
|
|
{
|
|
// Arrange
|
|
var tsqlQuery = "SELECT TOP 100 Id, Name FROM Users ORDER BY Name";
|
|
|
|
// Act
|
|
var success = QueryBreakdown.TryParse(tsqlQuery, out var result, isMicrosoftSql: true);
|
|
|
|
// Assert
|
|
Assert.That(success, Is.True);
|
|
Assert.That(result, Is.Not.Null);
|
|
Assert.That(result, Is.InstanceOf<QueryBreakdown>());
|
|
}
|
|
|
|
[Test]
|
|
public void TryParse_MicrosoftSqlInvalidQuery_ReturnsFalse()
|
|
{
|
|
// Arrange
|
|
var invalidQuery = "INVALID T-SQL QUERY";
|
|
|
|
// Act
|
|
var success = QueryBreakdown.TryParse(invalidQuery, out var result, out var errorMessage, isMicrosoftSql: true);
|
|
|
|
// Assert
|
|
Assert.That(success, Is.False);
|
|
Assert.That(result, Is.Null);
|
|
Assert.That(errorMessage, Is.Not.Null.And.Not.Empty);
|
|
}
|
|
|
|
[Test]
|
|
public void Parse_MicrosoftSqlWithBracketedIdentifiers_ParsesCorrectly()
|
|
{
|
|
// Arrange
|
|
var tsqlQuery = "SELECT [Order Id], [Customer Name] FROM [Order Details] WHERE [Active] = 1";
|
|
|
|
// Act
|
|
var result = QueryBreakdown.Parse(tsqlQuery, isMicrosoftSql: true);
|
|
|
|
// Assert
|
|
Assert.That(result.SelectClause.Clause?.Trim(), Does.Contain("[Order Id]"));
|
|
Assert.That(result.FromClause.Clause?.Trim(), Does.Contain("[Order Details]"));
|
|
Assert.That(result.WhereClause.Clause?.Trim(), Does.Contain("[Active]"));
|
|
}
|
|
|
|
[Test]
|
|
public void Parse_MicrosoftSqlWithCTE_ParsesCorrectly()
|
|
{
|
|
// Arrange
|
|
var tsqlQuery = @"WITH EmployeeCTE AS (
|
|
SELECT DepartmentId, COUNT(*) AS EmployeeCount
|
|
FROM Employees
|
|
GROUP BY DepartmentId
|
|
)
|
|
SELECT d.DepartmentName, e.EmployeeCount
|
|
FROM Departments d
|
|
INNER JOIN EmployeeCTE e ON d.DepartmentId = e.DepartmentId";
|
|
|
|
// Act
|
|
var result = QueryBreakdown.Parse(tsqlQuery, isMicrosoftSql: true);
|
|
|
|
// Assert
|
|
Assert.That(result.IsUsingWithClause, Is.True);
|
|
Assert.That(result.SelectClause.Clause?.Trim(), Does.Contain("DepartmentName"));
|
|
}
|
|
|
|
[Test]
|
|
public void GetSql_MicrosoftSqlWithCTE_GeneratesSnowflakeFormattedOutput()
|
|
{
|
|
// Arrange
|
|
var tsqlQuery = @"WITH SalesSummary AS (
|
|
SELECT ProductId, SUM(Quantity) AS TotalQty
|
|
FROM OrderDetails
|
|
GROUP BY ProductId
|
|
)
|
|
SELECT p.ProductName, s.TotalQty
|
|
FROM Products p
|
|
INNER JOIN SalesSummary s ON p.ProductId = s.ProductId";
|
|
var result = QueryBreakdown.Parse(tsqlQuery, isMicrosoftSql: true);
|
|
|
|
// Act
|
|
var snowflakeSql = result.GetSql();
|
|
|
|
// Assert
|
|
Assert.That(snowflakeSql, Does.Contain("WITH"));
|
|
Assert.That(snowflakeSql, Does.Contain("SELECT"));
|
|
Assert.That(result.IsUsingWithClause, Is.True, "Should have WITH clause parsed");
|
|
// Note: WITH clause content is stored in WithClause property when parsed via isMicrosoftSql
|
|
// The GetSql() will output it if WithClauses collection is populated or if we manually add it
|
|
}
|
|
|
|
[Test]
|
|
public void Parse_MicrosoftSqlComplexQuery_ParsesAndConvertsToSnowflake()
|
|
{
|
|
// Arrange
|
|
var tsqlQuery = @"SELECT
|
|
YEAR(o.OrderDate) AS OrderYear,
|
|
MONTH(o.OrderDate) AS OrderMonth,
|
|
c.Region,
|
|
COUNT(DISTINCT o.OrderId) AS OrderCount,
|
|
SUM(od.Quantity * od.UnitPrice) AS TotalRevenue
|
|
FROM Orders o
|
|
INNER JOIN OrderDetails od ON o.OrderId = od.OrderId
|
|
INNER JOIN Customers c ON o.CustomerId = c.CustomerId
|
|
WHERE o.OrderDate >= @startDate AND o.OrderDate < @endDate
|
|
GROUP BY YEAR(o.OrderDate), MONTH(o.OrderDate), c.Region
|
|
HAVING COUNT(DISTINCT o.OrderId) > 10
|
|
ORDER BY OrderYear DESC, OrderMonth DESC, TotalRevenue DESC";
|
|
|
|
// Act
|
|
var result = QueryBreakdown.Parse(tsqlQuery, isMicrosoftSql: true);
|
|
var snowflakeSql = result.GetSql();
|
|
|
|
// Assert - Verify parsing
|
|
Assert.That(result, Is.InstanceOf<QueryBreakdown>());
|
|
Assert.That(result.SelectClause.Clause?.Trim(), Does.Contain("YEAR"));
|
|
Assert.That(result.GroupByClause.Clause?.Trim(), Does.Contain("YEAR"));
|
|
Assert.That(result.HavingClause.Clause?.Trim(), Does.Contain("COUNT"));
|
|
Assert.That(result.Parameters, Does.ContainKey("@startDate"));
|
|
Assert.That(result.Parameters, Does.ContainKey("@endDate"));
|
|
|
|
// Assert - Verify Snowflake SQL generation
|
|
Assert.That(snowflakeSql, Does.Contain("SELECT"));
|
|
Assert.That(snowflakeSql, Does.Contain("FROM"));
|
|
Assert.That(snowflakeSql, Does.Contain("WHERE"));
|
|
Assert.That(snowflakeSql, Does.Contain("GROUP BY"));
|
|
Assert.That(snowflakeSql, Does.Contain("HAVING"));
|
|
Assert.That(snowflakeSql, Does.Contain("ORDER BY"));
|
|
}
|
|
|
|
[Test]
|
|
public void AddWithClause_ToMicrosoftSqlParsedQuery_WorksCorrectly()
|
|
{
|
|
// Arrange
|
|
var mainQuery = "SELECT * FROM ProductSummary";
|
|
var result = QueryBreakdown.Parse(mainQuery, isMicrosoftSql: true);
|
|
var cteQuery = new QueryBreakdown("ProductId, COUNT(*) AS OrderCount", "Orders");
|
|
cteQuery.GroupByClause.Clause = "ProductId";
|
|
|
|
// Act
|
|
result.AddWithClause("ProductSummary", cteQuery);
|
|
var snowflakeSql = result.GetSql();
|
|
|
|
// Assert
|
|
Assert.That(result.WithClauses.Count, Is.EqualTo(1));
|
|
Assert.That(snowflakeSql, Does.Contain("WITH"));
|
|
Assert.That(snowflakeSql, Does.Contain("ProductSummary AS ("));
|
|
}
|
|
|
|
[Test]
|
|
public void Clone_MicrosoftSqlParsedQuery_CreatesSnowflakeInstance()
|
|
{
|
|
// Arrange
|
|
var tsqlQuery = "SELECT Id, Name FROM Users WHERE Active = 1";
|
|
var original = QueryBreakdown.Parse(tsqlQuery, isMicrosoftSql: true);
|
|
|
|
// Act
|
|
var clone = (QueryBreakdown)original.Clone();
|
|
|
|
// Assert
|
|
Assert.That(clone, Is.InstanceOf<QueryBreakdown>());
|
|
Assert.That(clone.SelectClause.Clause, Is.EqualTo(original.SelectClause.Clause));
|
|
Assert.That(clone.FromClause.Clause, Is.EqualTo(original.FromClause.Clause));
|
|
Assert.That(clone.WhereClause.Clause, Is.EqualTo(original.WhereClause.Clause));
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region AddSelectExpression Tests
|
|
|
|
[Test]
|
|
public void AddSelectExpression_WithSnowflakeSyntax_GeneratesCorrectSql()
|
|
{
|
|
// Arrange
|
|
var query = new QueryBreakdown();
|
|
var expr = new Strata.SqlTools.SqlBreakdown.Expressions.GenericColumnExpression("CUSTOMER_ID", "CUSTOMERS");
|
|
|
|
// Act
|
|
query.AddSelectExpression(expr, null, isMicrosoftSql: false);
|
|
|
|
// Assert
|
|
// Snowflake doesn't use brackets
|
|
Assert.That(query.SelectClause.Clause, Is.EqualTo("CUSTOMERS.CUSTOMER_ID"));
|
|
}
|
|
|
|
[Test]
|
|
public void AddSelectExpression_WithSqlServerSyntax_GeneratesCorrectSql()
|
|
{
|
|
// Arrange
|
|
var query = new QueryBreakdown();
|
|
var expr = new Strata.SqlTools.SqlBreakdown.Expressions.GenericColumnExpression("CustomerID", "Customers");
|
|
|
|
// Act
|
|
query.AddSelectExpression(expr, null, isMicrosoftSql: true);
|
|
|
|
// Assert
|
|
// SQL Server uses brackets
|
|
Assert.That(query.SelectClause.Clause, Is.EqualTo("[Customers].[CustomerID]"));
|
|
}
|
|
|
|
[Test]
|
|
public void AddSelectExpression_DefaultBehavior_UsesSnowflakeSyntax()
|
|
{
|
|
// Arrange
|
|
var query = new QueryBreakdown();
|
|
var expr = new Strata.SqlTools.SqlBreakdown.Expressions.GenericColumnExpression("CUSTOMER_ID", "CUSTOMERS");
|
|
|
|
// Act
|
|
query.AddSelectExpression(expr); // Default should be Snowflake
|
|
|
|
// Assert
|
|
Assert.That(query.SelectClause.Clause, Is.EqualTo("CUSTOMERS.CUSTOMER_ID"));
|
|
Assert.That(query.SelectClause.Clause, Does.Not.Contain("["));
|
|
}
|
|
|
|
[Test]
|
|
public void AddSelectExpression_WithMultipleExpressions_AppendsWithComma()
|
|
{
|
|
// Arrange
|
|
var query = new QueryBreakdown();
|
|
var expr1 = new Strata.SqlTools.SqlBreakdown.Expressions.GenericColumnExpression("CUSTOMER_ID", "CUSTOMERS");
|
|
var expr2 = new Strata.SqlTools.SqlBreakdown.Expressions.GenericColumnExpression("CUSTOMER_NAME", "CUSTOMERS");
|
|
|
|
// Act
|
|
query.AddSelectExpression(expr1);
|
|
query.AddSelectExpression(expr2);
|
|
|
|
// Assert
|
|
Assert.That(query.SelectClause.Clause, Is.EqualTo("CUSTOMERS.CUSTOMER_ID, CUSTOMERS.CUSTOMER_NAME"));
|
|
}
|
|
|
|
[Test]
|
|
public void AddSelectExpression_WithComment_AddsCommentToSelectClause()
|
|
{
|
|
// Arrange
|
|
var query = new QueryBreakdown();
|
|
var expr = new Strata.SqlTools.SqlBreakdown.Expressions.GenericColumnExpression("CUSTOMER_ID", "CUSTOMERS");
|
|
|
|
// Act
|
|
query.AddSelectExpression(expr, "/* Primary key */");
|
|
|
|
// Assert
|
|
Assert.That(query.SelectClause.Clause, Is.EqualTo("CUSTOMERS.CUSTOMER_ID"));
|
|
Assert.That(query.SelectClause.Comment, Is.EqualTo("/* Primary key */"));
|
|
}
|
|
|
|
[Test]
|
|
public void AddSelectExpression_WithNullExpression_ThrowsArgumentNullException()
|
|
{
|
|
// Arrange
|
|
var query = new QueryBreakdown();
|
|
|
|
// Act & Assert
|
|
Assert.Throws<ArgumentNullException>(() => query.AddSelectExpression(null!));
|
|
}
|
|
|
|
[Test]
|
|
public void AddSelectExpression_WithBooleanLiteral_UsesSnowflakeSyntax()
|
|
{
|
|
// Arrange
|
|
var query = new QueryBreakdown();
|
|
var expr = new Strata.SqlTools.SqlBreakdown.Expressions.Literals.BooleanLiteralExpression(true);
|
|
|
|
// Act
|
|
query.AddSelectExpression(expr, null, isMicrosoftSql: false);
|
|
|
|
// Assert
|
|
// Snowflake uses TRUE/FALSE
|
|
Assert.That(query.SelectClause.Clause, Is.EqualTo("TRUE"));
|
|
}
|
|
|
|
[Test]
|
|
public void AddSelectExpression_WithBooleanLiteral_UsesSqlServerSyntax()
|
|
{
|
|
// Arrange
|
|
var query = new QueryBreakdown();
|
|
var expr = new Strata.SqlTools.SqlBreakdown.Expressions.Literals.BooleanLiteralExpression(true);
|
|
|
|
// Act
|
|
query.AddSelectExpression(expr, null, isMicrosoftSql: true);
|
|
|
|
// Assert
|
|
// SQL Server uses 1/0
|
|
Assert.That(query.SelectClause.Clause, Is.EqualTo("1"));
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region AddWhereExpression Tests
|
|
|
|
[Test]
|
|
public void AddWhereExpression_WithSnowflakeSyntax_GeneratesCorrectSql()
|
|
{
|
|
// Arrange
|
|
var query = new QueryBreakdown();
|
|
var col = new Strata.SqlTools.SqlBreakdown.Expressions.GenericColumnExpression("ACTIVE", "USERS");
|
|
var expr = col == 1;
|
|
|
|
// Act
|
|
query.AddWhereExpression(expr, null, "and", isMicrosoftSql: false);
|
|
|
|
// Assert
|
|
Assert.That(query.WhereClause.Clause, Does.Contain("USERS.ACTIVE"));
|
|
Assert.That(query.WhereClause.Clause, Does.Not.Contain("["));
|
|
}
|
|
|
|
[Test]
|
|
public void AddWhereExpression_WithSqlServerSyntax_GeneratesCorrectSql()
|
|
{
|
|
// Arrange
|
|
var query = new QueryBreakdown();
|
|
var col = new Strata.SqlTools.SqlBreakdown.Expressions.GenericColumnExpression("Active", "Users");
|
|
var expr = col == 1;
|
|
|
|
// Act
|
|
query.AddWhereExpression(expr, null, "and", isMicrosoftSql: true);
|
|
|
|
// Assert
|
|
Assert.That(query.WhereClause.Clause, Does.Contain("[Users].[Active]"));
|
|
}
|
|
|
|
[Test]
|
|
public void AddWhereExpression_DefaultBehavior_UsesSnowflakeSyntax()
|
|
{
|
|
// Arrange
|
|
var query = new QueryBreakdown();
|
|
var col = new Strata.SqlTools.SqlBreakdown.Expressions.GenericColumnExpression("ACTIVE", "USERS");
|
|
var expr = col == 1;
|
|
|
|
// Act
|
|
query.AddWhereExpression(expr); // Default should be Snowflake
|
|
|
|
// Assert
|
|
Assert.That(query.WhereClause.Clause, Does.Contain("USERS.ACTIVE"));
|
|
Assert.That(query.WhereClause.Clause, Does.Not.Contain("["));
|
|
}
|
|
|
|
[Test]
|
|
public void AddWhereExpression_WithMultipleExpressions_AppendsWithAnd()
|
|
{
|
|
// Arrange
|
|
var query = new QueryBreakdown();
|
|
var col1 = new Strata.SqlTools.SqlBreakdown.Expressions.GenericColumnExpression("ACTIVE", "USERS");
|
|
var col2 = new Strata.SqlTools.SqlBreakdown.Expressions.GenericColumnExpression("AGE", "USERS");
|
|
var expr1 = col1 == 1;
|
|
var expr2 = col2 >= 18;
|
|
|
|
// Act
|
|
query.AddWhereExpression(expr1);
|
|
query.AddWhereExpression(expr2);
|
|
|
|
// Assert
|
|
Assert.That(query.WhereClause.Clause, Does.Contain("USERS.ACTIVE"));
|
|
Assert.That(query.WhereClause.Clause, Does.Contain(" and "));
|
|
Assert.That(query.WhereClause.Clause, Does.Contain("USERS.AGE"));
|
|
}
|
|
|
|
[Test]
|
|
public void AddWhereExpression_WithOrOperation_AppendsWithOr()
|
|
{
|
|
// Arrange
|
|
var query = new QueryBreakdown();
|
|
var col1 = new Strata.SqlTools.SqlBreakdown.Expressions.GenericColumnExpression("STATUS", "ORDERS");
|
|
var col2 = new Strata.SqlTools.SqlBreakdown.Expressions.GenericColumnExpression("PRIORITY", "ORDERS");
|
|
var expr1 = col1 == "Urgent";
|
|
var expr2 = col2 > 5;
|
|
|
|
// Act
|
|
query.AddWhereExpression(expr1);
|
|
query.AddWhereExpression(expr2, null, "or");
|
|
|
|
// Assert
|
|
Assert.That(query.WhereClause.Clause, Does.Contain("ORDERS.STATUS"));
|
|
Assert.That(query.WhereClause.Clause, Does.Contain(" or "));
|
|
Assert.That(query.WhereClause.Clause, Does.Contain("ORDERS.PRIORITY"));
|
|
}
|
|
|
|
[Test]
|
|
public void AddWhereExpression_WithComment_AddsCommentToWhereClause()
|
|
{
|
|
// Arrange
|
|
var query = new QueryBreakdown();
|
|
var col = new Strata.SqlTools.SqlBreakdown.Expressions.GenericColumnExpression("ACTIVE", "USERS");
|
|
var expr = col == 1;
|
|
|
|
// Act
|
|
query.AddWhereExpression(expr, "/* Filter active users */");
|
|
|
|
// Assert
|
|
Assert.That(query.WhereClause.Comment, Is.EqualTo("/* Filter active users */"));
|
|
}
|
|
|
|
[Test]
|
|
public void AddWhereExpression_WithMultipleComments_AppendsComments()
|
|
{
|
|
// Arrange
|
|
var query = new QueryBreakdown();
|
|
var col1 = new Strata.SqlTools.SqlBreakdown.Expressions.GenericColumnExpression("ACTIVE", "USERS");
|
|
var col2 = new Strata.SqlTools.SqlBreakdown.Expressions.GenericColumnExpression("VERIFIED", "USERS");
|
|
var expr1 = col1 == 1;
|
|
var expr2 = col2 == new Strata.SqlTools.SqlBreakdown.Expressions.Literals.BooleanLiteralExpression(true);
|
|
|
|
// Act
|
|
query.AddWhereExpression(expr1, "/* Active users */");
|
|
query.AddWhereExpression(expr2, "/* Verified only */");
|
|
|
|
// Assert
|
|
Assert.That(query.WhereClause.Comment, Is.EqualTo("/* Active users */ /* Verified only */"));
|
|
}
|
|
|
|
[Test]
|
|
public void AddWhereExpression_WithNullExpression_ThrowsArgumentNullException()
|
|
{
|
|
// Arrange
|
|
var query = new QueryBreakdown();
|
|
|
|
// Act & Assert
|
|
Assert.Throws<ArgumentNullException>(() => query.AddWhereExpression(null!));
|
|
}
|
|
|
|
[Test]
|
|
public void AddWhereExpression_WithStringComparison_UsesCorrectQuotes()
|
|
{
|
|
// Arrange
|
|
var query = new QueryBreakdown();
|
|
var col = new Strata.SqlTools.SqlBreakdown.Expressions.GenericColumnExpression("NAME", "USERS");
|
|
var expr = col == "John";
|
|
|
|
// Act
|
|
query.AddWhereExpression(expr);
|
|
|
|
// Assert
|
|
Assert.That(query.WhereClause.Clause, Does.Contain("USERS.NAME"));
|
|
Assert.That(query.WhereClause.Clause, Does.Contain("'John'"));
|
|
}
|
|
|
|
[Test]
|
|
public void AddWhereExpression_MixingSyntaxModes_AllowsBothInSameQuery()
|
|
{
|
|
// Arrange
|
|
var query = new QueryBreakdown();
|
|
var col1 = new Strata.SqlTools.SqlBreakdown.Expressions.GenericColumnExpression("ACTIVE", "USERS");
|
|
var col2 = new Strata.SqlTools.SqlBreakdown.Expressions.GenericColumnExpression("Status", "Users");
|
|
var expr1 = col1 == 1;
|
|
var expr2 = col2 == "Active";
|
|
|
|
// Act
|
|
query.AddWhereExpression(expr1, null, "and", isMicrosoftSql: false); // Snowflake
|
|
query.AddWhereExpression(expr2, null, "and", isMicrosoftSql: true); // SQL Server
|
|
|
|
// Assert
|
|
// Should contain both syntaxes
|
|
Assert.That(query.WhereClause.Clause, Does.Contain("USERS.ACTIVE")); // Snowflake style
|
|
Assert.That(query.WhereClause.Clause, Does.Contain("[Users].[Status]")); // SQL Server style
|
|
}
|
|
|
|
[Test]
|
|
public void AddWhereExpression_WithComplexExpression_GeneratesCorrectSql()
|
|
{
|
|
// Arrange
|
|
var query = new QueryBreakdown();
|
|
var col1 = new Strata.SqlTools.SqlBreakdown.Expressions.GenericColumnExpression("PRICE", "PRODUCTS");
|
|
var col2 = new Strata.SqlTools.SqlBreakdown.Expressions.GenericColumnExpression("DISCOUNT", "PRODUCTS");
|
|
var expr = (col1 - col2) > 100;
|
|
|
|
// Act
|
|
query.AddWhereExpression(expr);
|
|
|
|
// Assert
|
|
Assert.That(query.WhereClause.Clause, Does.Contain("PRODUCTS.PRICE"));
|
|
Assert.That(query.WhereClause.Clause, Does.Contain("-"));
|
|
Assert.That(query.WhereClause.Clause, Does.Contain("PRODUCTS.DISCOUNT"));
|
|
Assert.That(query.WhereClause.Clause, Does.Contain(">"));
|
|
}
|
|
|
|
[Test]
|
|
public void AddSelectExpression_ToExistingClause_PreservesExistingContent()
|
|
{
|
|
// Arrange
|
|
var query = new QueryBreakdown();
|
|
query.SelectClause.Clause = "ID";
|
|
var expr = new Strata.SqlTools.SqlBreakdown.Expressions.GenericColumnExpression("NAME", "USERS");
|
|
|
|
// Act
|
|
query.AddSelectExpression(expr);
|
|
|
|
// Assert
|
|
Assert.That(query.SelectClause.Clause, Does.StartWith("ID, "));
|
|
Assert.That(query.SelectClause.Clause, Does.Contain("USERS.NAME"));
|
|
}
|
|
|
|
[Test]
|
|
public void AddWhereExpression_ToExistingClause_PreservesExistingContent()
|
|
{
|
|
// Arrange
|
|
var query = new QueryBreakdown();
|
|
query.WhereClause.Clause = "STATUS = 'ACTIVE'";
|
|
var col = new Strata.SqlTools.SqlBreakdown.Expressions.GenericColumnExpression("AGE", "USERS");
|
|
var expr = col > 18;
|
|
|
|
// Act
|
|
query.AddWhereExpression(expr);
|
|
|
|
// Assert
|
|
Assert.That(query.WhereClause.Clause, Does.StartWith("STATUS = 'ACTIVE' and "));
|
|
Assert.That(query.WhereClause.Clause, Does.Contain("USERS.AGE"));
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region AddWhereClause Parameter Extraction Tests
|
|
|
|
[Test]
|
|
public void AddWhereClause_WithSnowflakeParameters_ExtractsParametersAutomatically()
|
|
{
|
|
// Arrange
|
|
var query = new QueryBreakdown();
|
|
|
|
// Act - Use Snowflake :param syntax
|
|
query.AddWhereClause("USER_ID = :UserId AND STATUS = :Status", false);
|
|
|
|
// Assert
|
|
Assert.That(query.WhereClause.Clause, Is.EqualTo("USER_ID = :UserId AND STATUS = :Status"));
|
|
Assert.That(query.Parameters, Does.ContainKey(":UserId"));
|
|
Assert.That(query.Parameters, Does.ContainKey(":Status"));
|
|
Assert.That(query.Parameters[":UserId"], Is.Null);
|
|
Assert.That(query.Parameters[":Status"], Is.Null);
|
|
}
|
|
|
|
[Test]
|
|
public void AddWhereClause_WithMicrosoftParameters_ExtractsParametersAutomatically()
|
|
{
|
|
// Arrange
|
|
var query = new QueryBreakdown();
|
|
|
|
// Act - Use Microsoft @param syntax with isMicrosoftSql=true
|
|
query.AddWhereClause("USER_ID = @UserId AND STATUS = @Status", "and", true);
|
|
|
|
// Assert
|
|
Assert.That(query.WhereClause.Clause, Is.EqualTo("USER_ID = @UserId AND STATUS = @Status"));
|
|
Assert.That(query.Parameters, Does.ContainKey("@UserId"));
|
|
Assert.That(query.Parameters, Does.ContainKey("@Status"));
|
|
}
|
|
|
|
[Test]
|
|
public void AddWhereClause_WithExistingParameter_PreservesValue()
|
|
{
|
|
// Arrange
|
|
var query = new QueryBreakdown();
|
|
query.Parameters[":UserId"] = 123;
|
|
|
|
// Act - Add where clause with same parameter
|
|
query.AddWhereClause("USER_ID = :UserId", false);
|
|
|
|
// Assert - Original value should be preserved
|
|
Assert.That(query.Parameters[":UserId"], Is.EqualTo(123));
|
|
}
|
|
|
|
[Test]
|
|
public void AddWhereClause_WithMultipleCalls_AccumulatesSnowflakeParameters()
|
|
{
|
|
// Arrange
|
|
var query = new QueryBreakdown();
|
|
|
|
// Act
|
|
query.AddWhereClause("USER_ID = :UserId", false);
|
|
query.AddWhereClause("STATUS = :Status", "and", false);
|
|
query.AddWhereClause("CREATED_DATE > :StartDate", "and", false);
|
|
|
|
// Assert
|
|
Assert.That(query.Parameters.Count, Is.GreaterThanOrEqualTo(3));
|
|
Assert.That(query.Parameters, Does.ContainKey(":UserId"));
|
|
Assert.That(query.Parameters, Does.ContainKey(":Status"));
|
|
Assert.That(query.Parameters, Does.ContainKey(":StartDate"));
|
|
}
|
|
|
|
#endregion
|
|
}
|
|
|