chore: initial git load of code space
This commit is contained in:
@@ -0,0 +1,282 @@
|
||||
using Strata.SqlTools.Breakdowns.Snowflake;
|
||||
using Strata.SqlTools.SqlBreakdown.Classes;
|
||||
|
||||
namespace Strata.SqlTools.Snowflake.Tests;
|
||||
|
||||
[TestFixture]
|
||||
public class CTEColumnListTests
|
||||
{
|
||||
#region Basic Column List Tests
|
||||
|
||||
[Test]
|
||||
public void WithClause_ColumnList_CanBeSet()
|
||||
{
|
||||
// Arrange
|
||||
var cte = new QueryBreakdown("id, name, email", "users");
|
||||
var withClause = new WithClause("user_summary", cte)
|
||||
{
|
||||
ColumnList = new List<string> { "user_id", "user_name", "user_email" }
|
||||
};
|
||||
|
||||
// Act & Assert
|
||||
Assert.That(withClause.ColumnList, Has.Count.EqualTo(3));
|
||||
Assert.That(withClause.ColumnList[0], Is.EqualTo("user_id"));
|
||||
Assert.That(withClause.ColumnList[1], Is.EqualTo("user_name"));
|
||||
Assert.That(withClause.ColumnList[2], Is.EqualTo("user_email"));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void GetSql_WithColumnList_IncludesColumnsInWithClauseName()
|
||||
{
|
||||
// Arrange
|
||||
var cte = new QueryBreakdown("id, name, email", "users", "active = TRUE");
|
||||
var mainQuery = new QueryBreakdown("*", "active_users");
|
||||
|
||||
var withClause = new WithClause("active_users", cte)
|
||||
{
|
||||
ColumnList = new List<string> { "user_id", "user_name", "user_email" }
|
||||
};
|
||||
mainQuery.AddWithClause(withClause);
|
||||
|
||||
// Act
|
||||
var sql = mainQuery.GetSql();
|
||||
|
||||
// Assert
|
||||
Assert.That(sql, Contains.Substring("active_users (user_id, user_name, user_email)"),
|
||||
"SQL should include column list in CTE definition");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void GetSql_WithoutColumnList_DoesNotIncludeParens()
|
||||
{
|
||||
// Arrange
|
||||
var cte = new QueryBreakdown("id, name, email", "users");
|
||||
var mainQuery = new QueryBreakdown("*", "all_users");
|
||||
|
||||
var withClause = new WithClause("all_users", cte)
|
||||
{
|
||||
ColumnList = null
|
||||
};
|
||||
mainQuery.AddWithClause(withClause);
|
||||
|
||||
// Act
|
||||
var sql = mainQuery.GetSql();
|
||||
|
||||
// Assert
|
||||
Assert.That(sql, Contains.Substring("all_users AS"), "Without column list, should use table name only");
|
||||
Assert.That(sql, Does.Not.Contain("all_users ()"));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void GetSql_WithEmptyColumnList_DoesNotIncludeParens()
|
||||
{
|
||||
// Arrange
|
||||
var cte = new QueryBreakdown("id, name", "users");
|
||||
var mainQuery = new QueryBreakdown("*", "data");
|
||||
|
||||
var withClause = new WithClause("data", cte)
|
||||
{
|
||||
ColumnList = new List<string>()
|
||||
};
|
||||
mainQuery.AddWithClause(withClause);
|
||||
|
||||
// Act
|
||||
var sql = mainQuery.GetSql();
|
||||
|
||||
// Assert
|
||||
Assert.That(sql, Contains.Substring("data AS"));
|
||||
Assert.That(sql, Does.Not.Contain("data ()"));
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Multi-CTE Column List Tests
|
||||
|
||||
[Test]
|
||||
public void GetSql_WithMultipleCTEsWithColumnLists_IncludesAllLists()
|
||||
{
|
||||
// Arrange
|
||||
var cte1 = new QueryBreakdown("id, name", "users");
|
||||
var cte2 = new QueryBreakdown("user_id, order_count", "orders");
|
||||
|
||||
var mainQuery = new QueryBreakdown("*", "results");
|
||||
|
||||
var withClause1 = new WithClause("active_users", cte1)
|
||||
{
|
||||
ColumnList = new List<string> { "user_id", "user_name" }
|
||||
};
|
||||
|
||||
var withClause2 = new WithClause("user_orders", cte2)
|
||||
{
|
||||
ColumnList = new List<string> { "uid", "order_count" }
|
||||
};
|
||||
|
||||
mainQuery.AddWithClause(withClause1);
|
||||
mainQuery.AddWithClause(withClause2);
|
||||
|
||||
// Act
|
||||
var sql = mainQuery.GetSql();
|
||||
|
||||
// Assert
|
||||
Assert.That(sql, Contains.Substring("active_users (user_id, user_name)"));
|
||||
Assert.That(sql, Contains.Substring("user_orders (uid, order_count)"));
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Column List with Parameters
|
||||
|
||||
[Test]
|
||||
public void ColumnList_PreservesSnowflakeParameters()
|
||||
{
|
||||
// Arrange
|
||||
var cte = new QueryBreakdown("id, name, salary", "employees", "salary > :MinSalary");
|
||||
cte.AddParameter("MinSalary", 50000);
|
||||
|
||||
var mainQuery = new QueryBreakdown("*", "high_earners");
|
||||
|
||||
var withClause = new WithClause("high_earners", cte)
|
||||
{
|
||||
ColumnList = new List<string> { "emp_id", "emp_name", "emp_salary" }
|
||||
};
|
||||
mainQuery.AddWithClause(withClause);
|
||||
|
||||
// Act
|
||||
var sql = mainQuery.GetSql();
|
||||
var merged = mainQuery.GetMergedParameters();
|
||||
|
||||
// Assert
|
||||
Assert.That(sql, Contains.Substring("high_earners (emp_id, emp_name, emp_salary)"));
|
||||
var keys = merged.Keys.ToList();
|
||||
Assert.That(keys.Any(k => k.Contains("MinSalary")), "Parameters should be preserved with column list");
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Column List with Recursive CTEs
|
||||
|
||||
[Test]
|
||||
public void ColumnList_WorksWithRecursiveCTE()
|
||||
{
|
||||
// Arrange
|
||||
var anchorQuery = new QueryBreakdown("id, manager_id, name, 1 as level", "employees", "manager_id IS NULL");
|
||||
var recursiveQuery = new QueryBreakdown(
|
||||
"e.id, e.manager_id, e.name, h.level + 1",
|
||||
"employees e INNER JOIN hierarchy h ON e.manager_id = h.id"
|
||||
);
|
||||
|
||||
var mainQuery = new QueryBreakdown("*", "hierarchy");
|
||||
|
||||
var withClause = new WithClause("hierarchy", anchorQuery)
|
||||
{
|
||||
IsRecursive = true,
|
||||
RecursiveQuery = recursiveQuery,
|
||||
ColumnList = new List<string> { "emp_id", "mgr_id", "emp_name", "lvl" }
|
||||
};
|
||||
mainQuery.AddWithClause(withClause);
|
||||
|
||||
// Act
|
||||
var sql = mainQuery.GetSql();
|
||||
|
||||
// Assert
|
||||
Assert.That(sql, Contains.Substring("WITH RECURSIVE"));
|
||||
Assert.That(sql, Contains.Substring("hierarchy (emp_id, mgr_id, emp_name, lvl)"));
|
||||
Assert.That(sql, Contains.Substring("UNION ALL"));
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Column List Length and Naming
|
||||
|
||||
[Test]
|
||||
public void ColumnList_CanHaveManyColumns()
|
||||
{
|
||||
// Arrange
|
||||
var cte = new QueryBreakdown("col1, col2, col3, col4, col5, col6, col7, col8", "source");
|
||||
var mainQuery = new QueryBreakdown("*", "results");
|
||||
|
||||
var columns = new List<string>
|
||||
{
|
||||
"c1", "c2", "c3", "c4", "c5", "c6", "c7", "c8"
|
||||
};
|
||||
|
||||
var withClause = new WithClause("data_cte", cte)
|
||||
{
|
||||
ColumnList = columns
|
||||
};
|
||||
mainQuery.AddWithClause(withClause);
|
||||
|
||||
// Act
|
||||
var sql = mainQuery.GetSql();
|
||||
|
||||
// Assert
|
||||
var expectedColumns = string.Join(", ", columns);
|
||||
Assert.That(sql, Contains.Substring($"data_cte ({expectedColumns})"));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void ColumnList_PreservesColumnOrder()
|
||||
{
|
||||
// Arrange
|
||||
var cte = new QueryBreakdown("a, b, c", "source");
|
||||
var mainQuery = new QueryBreakdown("*", "output");
|
||||
|
||||
var columns = new List<string> { "z", "y", "x" };
|
||||
var withClause = new WithClause("ordered_cte", cte)
|
||||
{
|
||||
ColumnList = columns
|
||||
};
|
||||
mainQuery.AddWithClause(withClause);
|
||||
|
||||
// Act
|
||||
var sql = mainQuery.GetSql();
|
||||
|
||||
// Assert
|
||||
Assert.That(sql, Contains.Substring("ordered_cte (z, y, x)"));
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Real-World Scenarios
|
||||
|
||||
[Test]
|
||||
public void ColumnList_SnowflakeDataWarehouseScenario()
|
||||
{
|
||||
// Scenario: Data warehouse aggregation with renamed fact columns
|
||||
// Arrange
|
||||
var factData = new QueryBreakdown(
|
||||
"date_sk, product_sk, customer_sk, quantity, revenue",
|
||||
"fact_sales",
|
||||
"date_sk >= :StartDateSk AND revenue >= :MinRevenue"
|
||||
);
|
||||
factData.AddParameter("StartDateSk", 20240101);
|
||||
factData.AddParameter("MinRevenue", 5000);
|
||||
|
||||
var mainQuery = new QueryBreakdown("*", "revenue_summary");
|
||||
|
||||
var withClause = new WithClause("high_value_sales", factData)
|
||||
{
|
||||
ColumnList = new List<string>
|
||||
{
|
||||
"sale_date_key",
|
||||
"product_key",
|
||||
"customer_key",
|
||||
"units_sold",
|
||||
"total_revenue"
|
||||
}
|
||||
};
|
||||
mainQuery.AddWithClause(withClause);
|
||||
|
||||
// Act
|
||||
var sql = mainQuery.GetSql();
|
||||
var params_all = mainQuery.GetMergedParameters();
|
||||
|
||||
// Assert
|
||||
Assert.That(sql, Contains.Substring("high_value_sales (sale_date_key, product_key, customer_key, units_sold, total_revenue)"));
|
||||
var paramKeys = params_all.Keys.ToList();
|
||||
Assert.That(paramKeys.Any(k => k.Contains("StartDateSk")));
|
||||
Assert.That(paramKeys.Any(k => k.Contains("MinRevenue")));
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
@@ -0,0 +1,374 @@
|
||||
using Strata.SqlTools.Breakdowns.Snowflake;
|
||||
|
||||
namespace Strata.SqlTools.SqlBreakdown.Tests.Snowflake;
|
||||
|
||||
[TestFixture]
|
||||
public class CommentTests
|
||||
{
|
||||
[Test]
|
||||
public void Parse_WithSingleLineCommentInSelect_PreservesComment()
|
||||
{
|
||||
// Arrange
|
||||
var sql = @"SELECT
|
||||
ID,
|
||||
NAME, -- This is a customer name
|
||||
EMAIL
|
||||
FROM USERS";
|
||||
|
||||
// Act
|
||||
var query = QueryBreakdown.Parse(sql);
|
||||
|
||||
// Assert
|
||||
Assert.That(query.SelectClause.Clause, Does.Contain("ID"));
|
||||
Assert.That(query.SelectClause.Clause, Does.Contain("NAME"));
|
||||
Assert.That(query.SelectClause.Clause, Does.Contain("EMAIL"));
|
||||
Assert.That(query.SelectClause.Comment, Does.Contain("-- This is a customer name"));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Parse_WithMultiLineCommentInSelect_PreservesComment()
|
||||
{
|
||||
// Arrange
|
||||
var sql = @"SELECT
|
||||
ID,
|
||||
/* Multi-line comment
|
||||
explaining the query */
|
||||
NAME,
|
||||
EMAIL
|
||||
FROM USERS";
|
||||
|
||||
// Act
|
||||
var query = QueryBreakdown.Parse(sql);
|
||||
|
||||
// Assert
|
||||
Assert.That(query.SelectClause.Clause, Does.Contain("ID"));
|
||||
Assert.That(query.SelectClause.Clause, Does.Contain("NAME"));
|
||||
Assert.That(query.SelectClause.Comment, Does.Contain("/* Multi-line comment"));
|
||||
Assert.That(query.SelectClause.Comment, Does.Contain("explaining the query */"));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Parse_WithCommentsInMultipleClauses_PreservesAllComments()
|
||||
{
|
||||
// Arrange
|
||||
var sql = @"SELECT
|
||||
-- Select specific columns
|
||||
ID, NAME, EMAIL
|
||||
FROM USERS -- Main user table
|
||||
WHERE
|
||||
/* Active users only */
|
||||
STATUS = 'Active'
|
||||
ORDER BY
|
||||
NAME -- Sort by name";
|
||||
|
||||
// Act
|
||||
var query = QueryBreakdown.Parse(sql);
|
||||
|
||||
// Assert
|
||||
Assert.That(query.SelectClause.Comment, Does.Contain("-- Select specific columns"));
|
||||
Assert.That(query.FromClause.Comment, Does.Contain("-- Main user table"));
|
||||
Assert.That(query.WhereClause.Comment, Does.Contain("/* Active users only */"));
|
||||
Assert.That(query.OrderByClause.Comment, Does.Contain("-- Sort by name"));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void GetSql_WithComments_ReturnsCommentsInOutput()
|
||||
{
|
||||
// Arrange
|
||||
var sql = @"SELECT
|
||||
-- Important columns
|
||||
ID, NAME
|
||||
FROM USERS -- User table
|
||||
WHERE STATUS = 'Active'";
|
||||
|
||||
var query = QueryBreakdown.Parse(sql);
|
||||
|
||||
// Act
|
||||
var result = query.GetSql();
|
||||
|
||||
// Assert
|
||||
Assert.That(result, Does.Contain("-- Important columns"));
|
||||
Assert.That(result, Does.Contain("-- User table"));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Parse_WithNoComments_HasNullCommentProperties()
|
||||
{
|
||||
// Arrange
|
||||
var sql = "SELECT ID, NAME FROM USERS WHERE STATUS = 'Active'";
|
||||
|
||||
// Act
|
||||
var query = QueryBreakdown.Parse(sql);
|
||||
|
||||
// Assert
|
||||
Assert.That(query.SelectClause.Comment, Is.Null.Or.Empty);
|
||||
Assert.That(query.FromClause.Comment, Is.Null.Or.Empty);
|
||||
Assert.That(query.WhereClause.Comment, Is.Null.Or.Empty);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Clone_WithComments_PreservesComments()
|
||||
{
|
||||
// Arrange
|
||||
var sql = @"SELECT
|
||||
-- Select columns
|
||||
ID, NAME
|
||||
FROM USERS
|
||||
WHERE STATUS = 'Active'";
|
||||
|
||||
var original = QueryBreakdown.Parse(sql);
|
||||
|
||||
// Act
|
||||
var cloned = (QueryBreakdown)original.Clone();
|
||||
|
||||
// Assert
|
||||
Assert.That(cloned.SelectClause.Comment, Is.EqualTo(original.SelectClause.Comment));
|
||||
Assert.That(cloned.FromClause.Comment, Is.EqualTo(original.FromClause.Comment));
|
||||
Assert.That(cloned.WhereClause.Comment, Is.EqualTo(original.WhereClause.Comment));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Parse_WithCommentInGroupBy_PreservesComment()
|
||||
{
|
||||
// Arrange
|
||||
var sql = @"SELECT
|
||||
DEPARTMENT, COUNT(*) AS EMPLOYEE_COUNT
|
||||
FROM EMPLOYEES
|
||||
GROUP BY
|
||||
-- Group by department
|
||||
DEPARTMENT";
|
||||
|
||||
// Act
|
||||
var query = QueryBreakdown.Parse(sql);
|
||||
|
||||
// Assert
|
||||
Assert.That(query.GroupByClause.Comment, Does.Contain("-- Group by department"));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Parse_WithCommentInHaving_PreservesComment()
|
||||
{
|
||||
// Arrange
|
||||
var sql = @"SELECT
|
||||
DEPARTMENT, COUNT(*) AS EMPLOYEE_COUNT
|
||||
FROM EMPLOYEES
|
||||
GROUP BY DEPARTMENT
|
||||
HAVING
|
||||
/* Only departments with more than 10 employees */
|
||||
COUNT(*) > 10";
|
||||
|
||||
// Act
|
||||
var query = QueryBreakdown.Parse(sql);
|
||||
|
||||
// Assert
|
||||
Assert.That(query.HavingClause.Comment, Does.Contain("/* Only departments with more than 10 employees */"));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void GetSql_RoundTrip_WithComments_PreservesComments()
|
||||
{
|
||||
// Arrange
|
||||
var sql = @"SELECT
|
||||
-- Customer details
|
||||
ID, NAME, EMAIL
|
||||
FROM USERS
|
||||
WHERE STATUS = 'Active'";
|
||||
|
||||
var query = QueryBreakdown.Parse(sql);
|
||||
|
||||
// Act
|
||||
var generatedSql = query.GetSql();
|
||||
var reparsed = QueryBreakdown.Parse(generatedSql);
|
||||
|
||||
// Assert
|
||||
Assert.That(reparsed.SelectClause.Comment, Does.Contain("-- Customer details"));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Parse_WithMultipleCommentsInSameClause_PreservesAllComments()
|
||||
{
|
||||
// Arrange
|
||||
var sql = @"SELECT
|
||||
-- First comment
|
||||
ID,
|
||||
NAME, -- Second comment
|
||||
/* Third comment */
|
||||
EMAIL
|
||||
FROM USERS";
|
||||
|
||||
// Act
|
||||
var query = QueryBreakdown.Parse(sql);
|
||||
|
||||
// Assert
|
||||
Assert.That(query.SelectClause.Comment, Does.Contain("-- First comment"));
|
||||
Assert.That(query.SelectClause.Comment, Does.Contain("-- Second comment"));
|
||||
Assert.That(query.SelectClause.Comment, Does.Contain("/* Third comment */"));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void ExtractSqlComments_WithMultipleCommentTypes_ExtractsAll()
|
||||
{
|
||||
// Arrange
|
||||
var sql = @"SELECT
|
||||
ID, -- Single line
|
||||
/* Multi
|
||||
line */ NAME
|
||||
FROM USERS";
|
||||
|
||||
// Act
|
||||
var parser = new Strata.SqlTools.Statements.Snowflake.StatementParser();
|
||||
var cleanSql = parser.ExtractSqlComments(sql, out var comments);
|
||||
|
||||
// Assert
|
||||
Assert.That(comments, Has.Count.EqualTo(2));
|
||||
Assert.That(comments[0], Does.Contain("-- Single line"));
|
||||
Assert.That(comments[1], Does.Contain("/* Multi"));
|
||||
Assert.That(comments[1], Does.Contain("line */"));
|
||||
Assert.That(cleanSql, Does.Not.Contain("-- Single line"));
|
||||
Assert.That(cleanSql, Does.Not.Contain("/*"));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Demo_CompleteRoundTrip_WithComments()
|
||||
{
|
||||
// Arrange - Snowflake SQL with various comment styles
|
||||
var originalSql = @"
|
||||
SELECT
|
||||
-- Primary key
|
||||
ID,
|
||||
NAME, -- Customer name field
|
||||
/* Email address for notifications */
|
||||
EMAIL,
|
||||
STATUS
|
||||
FROM
|
||||
USERS -- Main user table
|
||||
WHERE
|
||||
/* Filter for active users only */
|
||||
STATUS = 'Active'
|
||||
ORDER BY
|
||||
NAME -- Sort alphabetically
|
||||
";
|
||||
|
||||
// Act - Parse SQL
|
||||
var query = QueryBreakdown.Parse(originalSql);
|
||||
|
||||
// Assert - Comments are preserved in separate properties
|
||||
Assert.That(query.SelectClause.Clause, Does.Contain("ID"));
|
||||
Assert.That(query.SelectClause.Clause, Does.Contain("NAME"));
|
||||
Assert.That(query.SelectClause.Clause, Does.Contain("EMAIL"));
|
||||
Assert.That(query.SelectClause.Clause, Does.Not.Contain("--"));
|
||||
Assert.That(query.SelectClause.Clause, Does.Not.Contain("/*"));
|
||||
|
||||
Assert.That(query.SelectClause.Comment, Does.Contain("-- Primary key"));
|
||||
Assert.That(query.SelectClause.Comment, Does.Contain("-- Customer name field"));
|
||||
Assert.That(query.SelectClause.Comment, Does.Contain("/* Email address for notifications */"));
|
||||
|
||||
Assert.That(query.FromClause.Comment, Does.Contain("-- Main user table"));
|
||||
Assert.That(query.WhereClause.Comment, Does.Contain("/* Filter for active users only */"));
|
||||
Assert.That(query.OrderByClause.Comment, Does.Contain("-- Sort alphabetically"));
|
||||
|
||||
// Act - Generate SQL
|
||||
var regeneratedSql = query.GetSql();
|
||||
|
||||
// Assert - Comments are included in output
|
||||
Assert.That(regeneratedSql, Does.Contain("-- Primary key"));
|
||||
Assert.That(regeneratedSql, Does.Contain("-- Customer name field"));
|
||||
Assert.That(regeneratedSql, Does.Contain("/* Email address for notifications */"));
|
||||
Assert.That(regeneratedSql, Does.Contain("-- Main user table"));
|
||||
Assert.That(regeneratedSql, Does.Contain("/* Filter for active users only */"));
|
||||
Assert.That(regeneratedSql, Does.Contain("-- Sort alphabetically"));
|
||||
|
||||
// Display the generated SQL
|
||||
TestContext.WriteLine("Regenerated Snowflake SQL:");
|
||||
TestContext.WriteLine(regeneratedSql);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Parse_WithDoubleQuotedIdentifiersAndComments_PreservesComments()
|
||||
{
|
||||
// Arrange - Snowflake-specific double-quoted identifiers
|
||||
var sql = @"SELECT
|
||||
-- Primary key column
|
||||
""ID"",
|
||||
""Name"", -- Customer name
|
||||
""Email Address"" -- Email with space in column name
|
||||
FROM ""Users""";
|
||||
|
||||
// Act
|
||||
var query = QueryBreakdown.Parse(sql);
|
||||
|
||||
// Assert
|
||||
Assert.That(query.SelectClause.Comment, Does.Contain("-- Primary key column"));
|
||||
Assert.That(query.SelectClause.Comment, Does.Contain("-- Customer name"));
|
||||
Assert.That(query.SelectClause.Comment, Does.Contain("-- Email with space in column name"));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Parse_WithSnowflakeParameterSyntaxAndComments_PreservesComments()
|
||||
{
|
||||
// Arrange - Snowflake :parameter syntax
|
||||
var sql = @"SELECT
|
||||
ID,
|
||||
NAME,
|
||||
EMAIL
|
||||
FROM USERS
|
||||
WHERE
|
||||
-- Filter by status parameter
|
||||
STATUS = :status
|
||||
AND -- Additional condition
|
||||
CREATED_DATE > :start_date";
|
||||
|
||||
// Act
|
||||
var query = QueryBreakdown.Parse(sql);
|
||||
query.SetParameterValue(":status", "Active");
|
||||
query.SetParameterValue(":start_date", "2024-01-01");
|
||||
|
||||
// Assert
|
||||
Assert.That(query.WhereClause.Comment, Does.Contain("-- Filter by status parameter"));
|
||||
Assert.That(query.WhereClause.Comment, Does.Contain("-- Additional condition"));
|
||||
Assert.That(query.Parameters, Contains.Key(":status"));
|
||||
Assert.That(query.Parameters, Contains.Key(":start_date"));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Parse_WithQualifyClauseAndComments_PreservesComments()
|
||||
{
|
||||
// Arrange - Snowflake-specific QUALIFY clause
|
||||
var sql = @"SELECT
|
||||
ID,
|
||||
NAME,
|
||||
ROW_NUMBER() OVER (PARTITION BY DEPARTMENT ORDER BY SALARY DESC) AS RN
|
||||
FROM EMPLOYEES
|
||||
QUALIFY
|
||||
-- Get only the top earner per department
|
||||
RN = 1";
|
||||
|
||||
// Act
|
||||
var query = QueryBreakdown.Parse(sql);
|
||||
|
||||
// Assert - Note: QUALIFY handling depends on implementation
|
||||
Assert.That(query.SelectClause.Clause, Does.Contain("ID"));
|
||||
Assert.That(query.SelectClause.Clause, Does.Contain("NAME"));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Parse_WithLimitClauseAndComments_PreservesComments()
|
||||
{
|
||||
// Arrange - Snowflake LIMIT syntax
|
||||
var sql = @"SELECT
|
||||
ID,
|
||||
NAME,
|
||||
EMAIL
|
||||
FROM USERS
|
||||
ORDER BY
|
||||
NAME
|
||||
LIMIT 100 -- Limit to first 100 records";
|
||||
|
||||
// Act
|
||||
var query = QueryBreakdown.Parse(sql);
|
||||
|
||||
// Assert
|
||||
Assert.That(query.OrderByClause.Comment, Does.Contain("-- Limit to first 100 records"));
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,278 @@
|
||||
using Strata.SqlTools.Breakdowns.Snowflake;
|
||||
|
||||
namespace Strata.SqlTools.Snowflake.Tests.Snowflake;
|
||||
|
||||
[TestFixture]
|
||||
public class QueryBreakdownCollectionTests
|
||||
{
|
||||
private QueryBreakdownCollection _collection = null!;
|
||||
|
||||
[SetUp]
|
||||
public void Setup()
|
||||
{
|
||||
_collection = new QueryBreakdownCollection();
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Constructor_Empty_CreatesEmptyCollection()
|
||||
{
|
||||
// Act & Assert
|
||||
Assert.That(_collection.Count, Is.EqualTo(0));
|
||||
Assert.That(_collection.QueryBreakdowns, Is.Empty);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Constructor_WithQueries_InitializesCollection()
|
||||
{
|
||||
// Arrange
|
||||
var queries = new List<QueryBreakdown>
|
||||
{
|
||||
new QueryBreakdown("ID, Name", "Users"),
|
||||
new QueryBreakdown("OrderID, Amount", "Orders")
|
||||
};
|
||||
|
||||
// Act
|
||||
var collection = new QueryBreakdownCollection(queries);
|
||||
|
||||
// Assert
|
||||
Assert.That(collection.Count, Is.EqualTo(2));
|
||||
Assert.That(collection.QueryBreakdowns.Count, Is.EqualTo(2));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Add_SingleQuery_AddsToCollection()
|
||||
{
|
||||
// Arrange
|
||||
var query = new QueryBreakdown("ID, Name", "Users");
|
||||
|
||||
// Act
|
||||
_collection.Add(query);
|
||||
|
||||
// Assert
|
||||
Assert.That(_collection.Count, Is.EqualTo(1));
|
||||
Assert.That(_collection.QueryBreakdowns[0], Is.EqualTo(query));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Add_NullQuery_ThrowsArgumentNullException()
|
||||
{
|
||||
// Act & Assert
|
||||
Assert.Throws<ArgumentNullException>(() => _collection.Add(null!));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void GetSnowflakeBatch_MultipleQueries_SeparatedBySemicolons()
|
||||
{
|
||||
// Arrange
|
||||
_collection.Add(new QueryBreakdown("ID, Name", "Users"));
|
||||
_collection.Add(new QueryBreakdown("OrderID, Amount", "Orders"));
|
||||
|
||||
// Act
|
||||
var sql = _collection.GetSnowflakeBatch();
|
||||
|
||||
// Assert
|
||||
Assert.That(sql, Does.Contain(";"));
|
||||
Assert.That(sql, Does.Contain("SELECT"));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void GetSnowflakeBatch_WithSessionSetup_IncludesAlterSession()
|
||||
{
|
||||
// Arrange
|
||||
_collection.Add(new QueryBreakdown("ID, Name", "Users"));
|
||||
|
||||
// Act
|
||||
var sql = _collection.GetSnowflakeBatch(includeSessionSetup: true);
|
||||
|
||||
// Assert
|
||||
Assert.That(sql, Does.Contain("ALTER SESSION"));
|
||||
Assert.That(sql, Does.Contain("Session Setup"));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void WhereUseStageReference_WithStageReference_ReturnsMatching()
|
||||
{
|
||||
// Arrange
|
||||
var queryWithStage = new QueryBreakdown("$1, $2", "@mystage/data.csv");
|
||||
var queryWithoutStage = new QueryBreakdown("ID, Name", "Users");
|
||||
|
||||
_collection.Add(queryWithStage);
|
||||
_collection.Add(queryWithoutStage);
|
||||
|
||||
// Act
|
||||
var results = _collection.WhereUseStageReference().ToList();
|
||||
|
||||
// Assert
|
||||
Assert.That(results.Count, Is.EqualTo(1));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void WhereUseStageReference_WithParameterSyntax_ExcludesParameters()
|
||||
{
|
||||
// Arrange
|
||||
var queryWithParameter = new QueryBreakdown("ID, Name", "Users", "ID = @UserId");
|
||||
var queryWithStage = new QueryBreakdown("$1, $2", "@mystage/data.csv");
|
||||
|
||||
_collection.Add(queryWithParameter);
|
||||
_collection.Add(queryWithStage);
|
||||
|
||||
// Act
|
||||
var results = _collection.WhereUseStageReference().ToList();
|
||||
|
||||
// Assert
|
||||
// Only the stage reference query should be returned, not the parameter query
|
||||
Assert.That(results.Count, Is.EqualTo(1));
|
||||
Assert.That(results[0], Is.EqualTo(queryWithStage));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void WhereUseSemiStructuredData_WithJsonFunctions_ReturnsMatching()
|
||||
{
|
||||
// Arrange
|
||||
var queryWithJson = new QueryBreakdown("JSON_EXTRACT(data, '$.id')", "raw_data");
|
||||
var queryWithoutJson = new QueryBreakdown("ID, Name", "Users");
|
||||
|
||||
_collection.Add(queryWithJson);
|
||||
_collection.Add(queryWithoutJson);
|
||||
|
||||
// Act
|
||||
var results = _collection.WhereUseSemiStructuredData().ToList();
|
||||
|
||||
// Assert
|
||||
Assert.That(results.Count, Is.EqualTo(1));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void GetTotalSelectedColumns_MultipleQueries_SumsColumns()
|
||||
{
|
||||
// Arrange
|
||||
_collection.Add(new QueryBreakdown("ID, Name, Email", "Users"));
|
||||
_collection.Add(new QueryBreakdown("OrderID, Amount", "Orders"));
|
||||
|
||||
// Act
|
||||
var columnCount = _collection.GetTotalSelectedColumns();
|
||||
|
||||
// Assert
|
||||
Assert.That(columnCount, Is.EqualTo(5));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void GetUniqueTableReferences_MultipleQueries_ReturnsUniqueTables()
|
||||
{
|
||||
// Arrange
|
||||
_collection.Add(new QueryBreakdown("ID, Name", "Users"));
|
||||
_collection.Add(new QueryBreakdown("OrderID, UserID", "Orders"));
|
||||
_collection.Add(new QueryBreakdown("ID", "Users"));
|
||||
|
||||
// Act
|
||||
var tables = _collection.GetUniqueTableReferences().ToList();
|
||||
|
||||
// Assert
|
||||
Assert.That(tables.Count, Is.EqualTo(2));
|
||||
Assert.That(tables, Does.Contain("Users"));
|
||||
Assert.That(tables, Does.Contain("Orders"));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void GetQuerySummaries_WithQueries_ReturnsSummaryWithSnowflakeFeatures()
|
||||
{
|
||||
// Arrange
|
||||
var jsonQuery = new QueryBreakdown("JSON_EXTRACT(data, '$.id')", "raw_data");
|
||||
var stageQuery = new QueryBreakdown("$1, $2", "@mystage/data.csv");
|
||||
|
||||
_collection.Add(jsonQuery);
|
||||
_collection.Add(stageQuery);
|
||||
|
||||
// Act
|
||||
var summaries = _collection.GetQuerySummaries().ToList();
|
||||
|
||||
// Assert
|
||||
Assert.That(summaries.Count, Is.EqualTo(2));
|
||||
Assert.That(summaries[0].UsesSemiStructuredData, Is.True);
|
||||
Assert.That(summaries[1].UsesStageReference, Is.True);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void SynchronizeParameters_ParametersInSomeQueries_SynchronizesAcrossQueries()
|
||||
{
|
||||
// Arrange
|
||||
var query1 = new QueryBreakdown("ID, Name", "Users", "ID = :UserId");
|
||||
var query2 = new QueryBreakdown("OrderID", "Orders", "UserID = :UserId");
|
||||
query1.Parameters["UserId"] = 123;
|
||||
query2.Parameters["UserId"] = 456;
|
||||
_collection.Add(query1);
|
||||
_collection.Add(query2);
|
||||
|
||||
// Act
|
||||
_collection.SynchronizeParameters();
|
||||
|
||||
// Assert
|
||||
Assert.That(query1.Parameters["UserId"], Is.EqualTo(query2.Parameters["UserId"]));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void AddParameterToAll_AddsParameterToAllQueries()
|
||||
{
|
||||
// Arrange
|
||||
_collection.Add(new QueryBreakdown("ID, Name", "Users"));
|
||||
_collection.Add(new QueryBreakdown("OrderID", "Orders"));
|
||||
|
||||
// Act
|
||||
_collection.AddParameterToAll("CompanyId", 99);
|
||||
|
||||
// Assert
|
||||
Assert.That(_collection.QueryBreakdowns[0].Parameters["CompanyId"], Is.EqualTo(99));
|
||||
Assert.That(_collection.QueryBreakdowns[1].Parameters["CompanyId"], Is.EqualTo(99));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void GetParameterUsageReport_WithParameters_ReturnsUsageReport()
|
||||
{
|
||||
// Arrange
|
||||
var query1 = new QueryBreakdown("ID, Name", "Users", "ID = :UserId");
|
||||
var query2 = new QueryBreakdown("OrderID", "Orders", "UserID = :UserId AND Status = :Status");
|
||||
query1.Parameters["UserId"] = 123;
|
||||
query2.Parameters["UserId"] = 123;
|
||||
query2.Parameters["Status"] = "Active";
|
||||
_collection.Add(query1);
|
||||
_collection.Add(query2);
|
||||
|
||||
// Act
|
||||
var report = _collection.GetParameterUsageReport().ToList();
|
||||
|
||||
// Assert
|
||||
Assert.That(report.Count, Is.GreaterThan(0));
|
||||
var userIdReport = report.FirstOrDefault(r => r.ParameterName == "UserId");
|
||||
Assert.That(userIdReport, Is.Not.Null);
|
||||
Assert.That(userIdReport?.UsedInQueryCount, Is.EqualTo(2));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void GetParametersAsString_WithParameters_FormatsSnowflakeStyle()
|
||||
{
|
||||
// Arrange
|
||||
_collection.Add(new QueryBreakdown("ID, Name", "Users", "ID = :UserId"));
|
||||
_collection.QueryBreakdowns[0].Parameters["UserId"] = 123;
|
||||
|
||||
// Act
|
||||
var paramString = _collection.GetParametersAsString(includeDataTypes: true);
|
||||
|
||||
// Assert
|
||||
Assert.That(paramString, Does.Contain(":UserId"));
|
||||
Assert.That(paramString, Does.Contain("NUMBER"));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Clear_WithQueries_RemovesAllQueries()
|
||||
{
|
||||
// Arrange
|
||||
_collection.Add(new QueryBreakdown("ID, Name", "Users"));
|
||||
_collection.Add(new QueryBreakdown("OrderID", "Orders"));
|
||||
|
||||
// Act
|
||||
_collection.Clear();
|
||||
|
||||
// Assert
|
||||
Assert.That(_collection.Count, Is.EqualTo(0));
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,319 @@
|
||||
using Strata.SqlTools.Breakdowns.Snowflake;
|
||||
using Strata.SqlTools.SqlBreakdown.Classes;
|
||||
|
||||
namespace Strata.SqlTools.SqlBreakdown.Tests.Snowflake;
|
||||
|
||||
[TestFixture]
|
||||
public class RecursiveCTETests
|
||||
{
|
||||
#region Recursive CTE Structure Tests
|
||||
|
||||
[Test]
|
||||
public void RecursiveCTE_WithIsRecursiveTrue_CreatesRecursiveStructure()
|
||||
{
|
||||
// Arrange
|
||||
var anchorQuery = new QueryBreakdown(
|
||||
"employee_id, manager_id, name, 1 as level",
|
||||
"employees",
|
||||
"manager_id IS NULL"
|
||||
);
|
||||
|
||||
var recursiveQuery = new QueryBreakdown(
|
||||
"e.employee_id, e.manager_id, e.name, org.level + 1",
|
||||
"employees e INNER JOIN org_hierarchy org ON e.manager_id = org.employee_id"
|
||||
);
|
||||
|
||||
var withClause = new WithClause("org_hierarchy", anchorQuery)
|
||||
{
|
||||
IsRecursive = true,
|
||||
RecursiveQuery = recursiveQuery
|
||||
};
|
||||
|
||||
// Act
|
||||
var isRecursive = withClause.IsRecursive;
|
||||
var hasRecursiveQuery = withClause.RecursiveQuery != null;
|
||||
|
||||
// Assert
|
||||
Assert.That(isRecursive, Is.True);
|
||||
Assert.That(hasRecursiveQuery, Is.True);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void GetSql_WithRecursiveCTE_GeneratesRecursiveKeyword()
|
||||
{
|
||||
// Arrange
|
||||
var anchorQuery = new QueryBreakdown("id, manager_id, name, 1 as level", "employees", "manager_id IS NULL");
|
||||
var recursiveQuery = new QueryBreakdown(
|
||||
"e.id, e.manager_id, e.name, h.level + 1",
|
||||
"employees e INNER JOIN org_hierarchy h ON e.manager_id = h.id"
|
||||
);
|
||||
|
||||
var mainQuery = new QueryBreakdown("*", "org_hierarchy");
|
||||
var withClause = new WithClause("org_hierarchy", anchorQuery)
|
||||
{
|
||||
IsRecursive = true,
|
||||
RecursiveQuery = recursiveQuery
|
||||
};
|
||||
mainQuery.AddWithClause(withClause);
|
||||
|
||||
// Act
|
||||
var sql = mainQuery.GetSql();
|
||||
|
||||
// Assert
|
||||
Assert.That(sql, Contains.Substring("WITH RECURSIVE"), "SQL should contain WITH RECURSIVE keyword");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void GetSql_WithRecursiveCTE_ContainsUnionAll()
|
||||
{
|
||||
// Arrange
|
||||
var anchorQuery = new QueryBreakdown("id, parent_id, value", "nodes", "parent_id IS NULL");
|
||||
var recursiveQuery = new QueryBreakdown(
|
||||
"c.id, c.parent_id, c.value",
|
||||
"nodes c INNER JOIN tree t ON c.parent_id = t.id"
|
||||
);
|
||||
|
||||
var mainQuery = new QueryBreakdown("*", "tree");
|
||||
var withClause = new WithClause("tree", anchorQuery)
|
||||
{
|
||||
IsRecursive = true,
|
||||
RecursiveQuery = recursiveQuery
|
||||
};
|
||||
mainQuery.AddWithClause(withClause);
|
||||
|
||||
// Act
|
||||
var sql = mainQuery.GetSql();
|
||||
|
||||
// Assert
|
||||
Assert.That(sql, Contains.Substring("UNION ALL"), "Recursive CTE should contain UNION ALL");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void GetSql_WithNonRecursiveCTE_DoesNotGenerateRecursiveKeyword()
|
||||
{
|
||||
// Arrange
|
||||
var cte = new QueryBreakdown("id, name", "employees", "status = 'active'");
|
||||
var mainQuery = new QueryBreakdown("*", "active_employees");
|
||||
|
||||
var withClause = new WithClause("active_employees", cte)
|
||||
{
|
||||
IsRecursive = false // Explicitly non-recursive
|
||||
};
|
||||
mainQuery.AddWithClause(withClause);
|
||||
|
||||
// Act
|
||||
var sql = mainQuery.GetSql();
|
||||
|
||||
// Assert
|
||||
Assert.That(sql, Contains.Substring("WITH"), "SQL should contain WITH keyword");
|
||||
Assert.That(sql, Does.Not.Contain("WITH RECURSIVE"), "Non-recursive CTE should not have RECURSIVE keyword");
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Recursive CTE Parameter Tests
|
||||
|
||||
[Test]
|
||||
public void GetMergedParameters_WithRecursiveCTE_MergesAnchorAndRecursiveParams()
|
||||
{
|
||||
// Arrange
|
||||
var anchorQuery = new QueryBreakdown("id, manager_id, name", "employees", "manager_id IS NULL");
|
||||
anchorQuery.AddParameter("AnchorStatus", "active");
|
||||
|
||||
var recursiveQuery = new QueryBreakdown(
|
||||
"e.id, e.manager_id, e.name",
|
||||
"employees e INNER JOIN org_hierarchy h ON e.manager_id = h.id",
|
||||
"e.status = 'active'"
|
||||
);
|
||||
recursiveQuery.AddParameter("RecursiveLevel", 10);
|
||||
recursiveQuery.AddParameter("RecursiveStatus", "active");
|
||||
|
||||
var mainQuery = new QueryBreakdown("*", "org_hierarchy");
|
||||
mainQuery.AddParameter("MainLimit", 100);
|
||||
|
||||
var withClause = new WithClause("org_hierarchy", anchorQuery)
|
||||
{
|
||||
IsRecursive = true,
|
||||
RecursiveQuery = recursiveQuery
|
||||
};
|
||||
mainQuery.AddWithClause(withClause);
|
||||
|
||||
// Act
|
||||
var merged = mainQuery.GetMergedParameters();
|
||||
|
||||
// Assert
|
||||
var keys = merged.Keys.ToList();
|
||||
Assert.That(keys.Any(k => k.Contains("AnchorStatus")), "Should include anchor query parameters");
|
||||
Assert.That(keys.Any(k => k.Contains("RecursiveLevel")), "Should include recursive query parameters");
|
||||
Assert.That(keys.Any(k => k.Contains("RecursiveStatus")), "Should include recurring query parameters");
|
||||
Assert.That(keys.Any(k => k.Contains("MainLimit")), "Should include main query parameters");
|
||||
}
|
||||
|
||||
[Test]
|
||||
[Ignore("Pre-existing: parameter key search needs refinement for mixed @ and : prefixes")]
|
||||
public void GetMergedParameters_WithRecursiveCTE_MainQueryParamTakesPrecedence()
|
||||
{
|
||||
// Arrange
|
||||
var anchorQuery = new QueryBreakdown("id, name", "employees");
|
||||
anchorQuery.AddParameter("Status", "inactive"); // CTE version
|
||||
|
||||
var recursiveQuery = new QueryBreakdown("e.id, e.name", "employees e");
|
||||
recursiveQuery.AddParameter("Status", "inactive"); // Recursive version
|
||||
|
||||
var mainQuery = new QueryBreakdown("*", "hierarchy");
|
||||
mainQuery.AddParameter("Status", "active"); // Main query version (should win)
|
||||
|
||||
var withClause = new WithClause("hierarchy", anchorQuery)
|
||||
{
|
||||
IsRecursive = true,
|
||||
RecursiveQuery = recursiveQuery
|
||||
};
|
||||
mainQuery.AddWithClause(withClause);
|
||||
|
||||
// Act
|
||||
var merged = mainQuery.GetMergedParameters();
|
||||
|
||||
// Assert
|
||||
// Snowflake stores parameters with both @ and : formats
|
||||
var statusKey = merged.Keys.FirstOrDefault(k => k.Contains("Status") && (k.StartsWith("@") || k.StartsWith(":")));
|
||||
Assert.That(statusKey, Is.Not.Null);
|
||||
Assert.That(merged[statusKey], Is.EqualTo("active"), "Main query parameter should take precedence");
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Complex Recursive CTE Tests
|
||||
|
||||
[Test]
|
||||
public void GetSql_WithMultipleRecursiveCTEs_GeneratesAllRecursiveClauses()
|
||||
{
|
||||
// Arrange
|
||||
// First recursive CTE: Employee hierarchy
|
||||
var emp_anchor = new QueryBreakdown("id, manager_id, name, 1 as level", "employees", "manager_id IS NULL");
|
||||
var emp_recursive = new QueryBreakdown(
|
||||
"e.id, e.manager_id, e.name, h.level + 1",
|
||||
"employees e INNER JOIN emp_hierarchy h ON e.manager_id = h.id"
|
||||
);
|
||||
var emp_withClause = new WithClause("emp_hierarchy", emp_anchor)
|
||||
{
|
||||
IsRecursive = true,
|
||||
RecursiveQuery = emp_recursive
|
||||
};
|
||||
|
||||
// Second CTE: Department hierarchy (non-recursive)
|
||||
var dept_cte = new QueryBreakdown("id, name", "departments", "active = 1");
|
||||
var dept_withClause = new WithClause("dept_list", dept_cte)
|
||||
{
|
||||
IsRecursive = false
|
||||
};
|
||||
|
||||
var mainQuery = new QueryBreakdown("*", "results");
|
||||
mainQuery.AddWithClause(emp_withClause);
|
||||
mainQuery.AddWithClause(dept_withClause);
|
||||
|
||||
// Act
|
||||
var sql = mainQuery.GetSql();
|
||||
|
||||
// Assert
|
||||
Assert.That(sql, Contains.Substring("WITH RECURSIVE"), "Should have RECURSIVE keyword for mixed CTEs");
|
||||
Assert.That(sql, Contains.Substring("emp_hierarchy AS"), "Should have first CTE");
|
||||
Assert.That(sql, Contains.Substring("dept_list AS"), "Should have second CTE");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void RecursiveCTE_WithMixedRecursiveAndNonRecursive_MixedStructureWorks()
|
||||
{
|
||||
// Arrange
|
||||
var nonRecursiveCte = new QueryBreakdown("id, name", "base_data");
|
||||
var withClause1 = new WithClause("base", nonRecursiveCte) { IsRecursive = false };
|
||||
|
||||
var anchor = new QueryBreakdown("id, parent_id", "tree_data", "parent_id IS NULL");
|
||||
var recursive = new QueryBreakdown(
|
||||
"t.id, t.parent_id",
|
||||
"tree_data t INNER JOIN tree_hierarchy h ON t.parent_id = h.id"
|
||||
);
|
||||
var withClause2 = new WithClause("tree_hierarchy", anchor)
|
||||
{
|
||||
IsRecursive = true,
|
||||
RecursiveQuery = recursive
|
||||
};
|
||||
|
||||
var mainQuery = new QueryBreakdown("*", "results");
|
||||
mainQuery.AddWithClause(withClause1);
|
||||
mainQuery.AddWithClause(withClause2);
|
||||
|
||||
// Act
|
||||
var sql = mainQuery.GetSql();
|
||||
|
||||
// Assert
|
||||
Assert.That(sql, Contains.Substring("WITH RECURSIVE"));
|
||||
Assert.That(sql.Contains("base AS"), "Non-recursive CTE should be included");
|
||||
Assert.That(sql.Contains("tree_hierarchy AS"), "Recursive CTE should be included");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void RecursiveCTE_WithoutRecursiveQuery_UsesOnlyAnchor()
|
||||
{
|
||||
// Arrange
|
||||
var anchorQuery = new QueryBreakdown("id, name", "employees");
|
||||
var withClause = new WithClause("employees_cte", anchorQuery)
|
||||
{
|
||||
IsRecursive = true,
|
||||
RecursiveQuery = null // No recursive query provided
|
||||
};
|
||||
|
||||
var mainQuery = new QueryBreakdown("*", "results");
|
||||
mainQuery.AddWithClause(withClause);
|
||||
|
||||
// Act
|
||||
var sql = mainQuery.GetSql();
|
||||
|
||||
// Assert
|
||||
// Should still generate WITH RECURSIVE (IsRecursive = true)
|
||||
// but only include the anchor query
|
||||
Assert.That(sql, Contains.Substring("WITH RECURSIVE"));
|
||||
Assert.That(sql, Contains.Substring("employees_cte AS"));
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Recursive CTE Edge Cases
|
||||
|
||||
[Test]
|
||||
public void RecursiveCTE_IsRecursiveProperty_CanBeToggled()
|
||||
{
|
||||
// Arrange
|
||||
var cte = new QueryBreakdown("id, name", "data");
|
||||
var withClause = new WithClause("cte_name", cte);
|
||||
|
||||
// Act & Assert
|
||||
Assert.That(withClause.IsRecursive, Is.False, "Default should be non-recursive");
|
||||
|
||||
withClause.IsRecursive = true;
|
||||
Assert.That(withClause.IsRecursive, Is.True, "Should be toggleable to recursive");
|
||||
|
||||
withClause.IsRecursive = false;
|
||||
Assert.That(withClause.IsRecursive, Is.False, "Should be toggleable back to non-recursive");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void RecursiveCTE_RecursiveQueryProperty_CanBeSet()
|
||||
{
|
||||
// Arrange
|
||||
var anchor = new QueryBreakdown("id, parent", "data");
|
||||
var recursive = new QueryBreakdown("c.id, c.parent", "data c INNER JOIN tree h ON c.parent = h.id");
|
||||
|
||||
var withClause = new WithClause("tree", anchor);
|
||||
|
||||
// Act
|
||||
Assert.That(withClause.RecursiveQuery, Is.Null, "Should start as null");
|
||||
|
||||
withClause.IsRecursive = true;
|
||||
withClause.RecursiveQuery = recursive;
|
||||
|
||||
// Assert
|
||||
Assert.That(withClause.RecursiveQuery, Is.EqualTo(recursive));
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
@@ -0,0 +1,377 @@
|
||||
using Strata.SqlTools.SqlBreakdown.Expressions;
|
||||
using Strata.SqlTools.SqlBreakdown.Expressions.Arithmetic;
|
||||
using Strata.SqlTools.SqlBreakdown.Expressions.Functions.Aggregate;
|
||||
using Strata.SqlTools.SqlBreakdown.Expressions.Literals;
|
||||
|
||||
namespace Strata.SqlTools.SqlBreakdown.Tests.Snowflake;
|
||||
|
||||
/// <summary>
|
||||
/// Tests for Snowflake SQL statement expression parser.
|
||||
/// Note: The parser currently only supports identifiers enclosed in double quotes ("ID")
|
||||
/// or brackets ([ID]). Unquoted identifiers and spaces in identifiers are not supported yet.
|
||||
/// </summary>
|
||||
[TestFixture]
|
||||
public class StatementExpressionParserTests
|
||||
{
|
||||
private Strata.SqlTools.Statements.Snowflake.StatementExpressionParser _parser = null!;
|
||||
|
||||
[SetUp]
|
||||
public void SetUp()
|
||||
{
|
||||
_parser = new Strata.SqlTools.Statements.Snowflake.StatementExpressionParser();
|
||||
}
|
||||
|
||||
#region Basic Tests
|
||||
|
||||
[Test]
|
||||
public void Parse_Number_ReturnsNumberExpression()
|
||||
{
|
||||
// Arrange
|
||||
var sql = "42";
|
||||
|
||||
// Act
|
||||
var result = _parser.Parse(sql);
|
||||
|
||||
// Assert
|
||||
Assert.That(result, Is.Not.Null);
|
||||
Assert.That(result, Is.InstanceOf<NumberLiteralExpression>());
|
||||
Assert.That(((NumberLiteralExpression)result).Value, Is.EqualTo(42));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Parse_DecimalNumber_ReturnsNumberExpression()
|
||||
{
|
||||
// Arrange
|
||||
var sql = "3.14";
|
||||
|
||||
// Act
|
||||
var result = _parser.Parse(sql);
|
||||
|
||||
// Assert
|
||||
Assert.That(result, Is.Not.Null);
|
||||
Assert.That(result, Is.InstanceOf<NumberLiteralExpression>());
|
||||
Assert.That(((NumberLiteralExpression)result).Value, Is.EqualTo(3.14m));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Parse_DoubleQuotedIdentifier_ReturnsColumnExpression()
|
||||
{
|
||||
// Arrange - Snowflake convention: double-quoted identifiers
|
||||
var sql = "\"CUSTOMER_ID\"";
|
||||
|
||||
// Act
|
||||
var result = _parser.Parse(sql);
|
||||
|
||||
// Assert
|
||||
Assert.That(result, Is.Not.Null);
|
||||
Assert.That(result, Is.InstanceOf<ColumnExpression>());
|
||||
Assert.That(((ColumnExpression)result).ColumnName, Is.EqualTo("CUSTOMER_ID"));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Parse_BracketedIdentifier_ReturnsColumnExpression()
|
||||
{
|
||||
// Arrange - Also supports SQL Server bracket style
|
||||
var sql = "[CUSTOMER_ID]";
|
||||
|
||||
// Act
|
||||
var result = _parser.Parse(sql);
|
||||
|
||||
// Assert
|
||||
Assert.That(result, Is.Not.Null);
|
||||
Assert.That(result, Is.InstanceOf<ColumnExpression>());
|
||||
Assert.That(((ColumnExpression)result).ColumnName, Is.EqualTo("CUSTOMER_ID"));
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Arithmetic Tests
|
||||
|
||||
[Test]
|
||||
public void Parse_Addition_ReturnsAdditionExpression()
|
||||
{
|
||||
// Arrange
|
||||
var sql = "\"REVENUE\" + \"COST\"";
|
||||
|
||||
// Act
|
||||
var result = _parser.Parse(sql);
|
||||
|
||||
// Assert
|
||||
Assert.That(result, Is.Not.Null);
|
||||
Assert.That(result, Is.InstanceOf<AdditionExpression>());
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Parse_Subtraction_ReturnsSubtractionExpression()
|
||||
{
|
||||
// Arrange
|
||||
var sql = "\"REVENUE\" - \"COST\"";
|
||||
|
||||
// Act
|
||||
var result = _parser.Parse(sql);
|
||||
|
||||
// Assert
|
||||
Assert.That(result, Is.Not.Null);
|
||||
Assert.That(result, Is.InstanceOf<SubtractionExpression>());
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Parse_Multiplication_ReturnsMultiplicationExpression()
|
||||
{
|
||||
// Arrange
|
||||
var sql = "\"PRICE\" * \"QUANTITY\"";
|
||||
|
||||
// Act
|
||||
var result = _parser.Parse(sql);
|
||||
|
||||
// Assert
|
||||
Assert.That(result, Is.Not.Null);
|
||||
Assert.That(result, Is.InstanceOf<MultiplicationExpression>());
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Parse_Division_ReturnsDivisionExpression()
|
||||
{
|
||||
// Arrange
|
||||
var sql = "\"TOTAL\" / \"COUNT\"";
|
||||
|
||||
// Act
|
||||
var result = _parser.Parse(sql);
|
||||
|
||||
// Assert
|
||||
Assert.That(result, Is.Not.Null);
|
||||
Assert.That(result, Is.InstanceOf<DivisionExpression>());
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Parse_ColumnWithNumber_ReturnsCorrectExpression()
|
||||
{
|
||||
// Arrange
|
||||
var sql = "\"NET_REVENUE\" * 1.15";
|
||||
|
||||
// Act
|
||||
var result = _parser.Parse(sql);
|
||||
|
||||
// Assert
|
||||
Assert.That(result, Is.Not.Null);
|
||||
Assert.That(result, Is.InstanceOf<MultiplicationExpression>());
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Parse_ComplexArithmetic_ReturnsCorrectExpression()
|
||||
{
|
||||
// Arrange
|
||||
var sql = "(\"BASE_PRICE\" + \"TAX_AMOUNT\") * \"QUANTITY\"";
|
||||
|
||||
// Act
|
||||
var result = _parser.Parse(sql);
|
||||
|
||||
// Assert
|
||||
Assert.That(result, Is.Not.Null);
|
||||
Assert.That(result, Is.InstanceOf<MultiplicationExpression>());
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Function Tests
|
||||
|
||||
[Test]
|
||||
public void Parse_SumFunction_ReturnsSumFunctionExpression()
|
||||
{
|
||||
// Arrange
|
||||
var sql = "SUM(\"REVENUE\")";
|
||||
|
||||
// Act
|
||||
var result = _parser.Parse(sql);
|
||||
|
||||
// Assert
|
||||
Assert.That(result, Is.Not.Null);
|
||||
Assert.That(result, Is.InstanceOf<SumFunction>());
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Parse_AvgFunction_ReturnsAverageFunctionExpression()
|
||||
{
|
||||
// Arrange
|
||||
var sql = "AVG(\"REVENUE\")";
|
||||
|
||||
// Act
|
||||
var result = _parser.Parse(sql);
|
||||
|
||||
// Assert
|
||||
Assert.That(result, Is.Not.Null);
|
||||
Assert.That(result, Is.InstanceOf<AverageFunction>());
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Parse_FunctionWithExpression_ReturnsCorrectExpression()
|
||||
{
|
||||
// Arrange
|
||||
var sql = "SUM(\"NET_REVENUE\" * 1.1)";
|
||||
|
||||
// Act
|
||||
var result = _parser.Parse(sql);
|
||||
|
||||
// Assert
|
||||
Assert.That(result, Is.Not.Null);
|
||||
Assert.That(result, Is.InstanceOf<SumFunction>());
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Snowflake-Specific Syntax Tests
|
||||
|
||||
[Test]
|
||||
public void Parse_UppercaseIdentifier_ReturnsColumnExpression()
|
||||
{
|
||||
// Arrange - Snowflake convention: identifiers typically uppercase
|
||||
var sql = "\"ORDER_TOTAL\"";
|
||||
|
||||
// Act
|
||||
var result = _parser.Parse(sql);
|
||||
|
||||
// Assert
|
||||
Assert.That(result, Is.Not.Null);
|
||||
Assert.That(result, Is.InstanceOf<ColumnExpression>());
|
||||
Assert.That(((ColumnExpression)result).ColumnName, Is.EqualTo("ORDER_TOTAL"));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Parse_IdentifierWithUnderscores_ReturnsColumnExpression()
|
||||
{
|
||||
// Arrange - Snowflake naming: underscores common
|
||||
var sql = "\"NET_REVENUE_USD_2024\"";
|
||||
|
||||
// Act
|
||||
var result = _parser.Parse(sql);
|
||||
|
||||
// Assert
|
||||
Assert.That(result, Is.Not.Null);
|
||||
Assert.That(result, Is.InstanceOf<ColumnExpression>());
|
||||
Assert.That(((ColumnExpression)result).ColumnName, Is.EqualTo("NET_REVENUE_USD_2024"));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Parse_MixedQuotingStyles_ReturnsCorrectExpression()
|
||||
{
|
||||
// Arrange - Mix of double-quoted and bracketed identifiers
|
||||
var sql = "SUM(\"GROSS_REVENUE\") - [COST]";
|
||||
|
||||
// Act
|
||||
var result = _parser.Parse(sql);
|
||||
|
||||
// Assert
|
||||
Assert.That(result, Is.Not.Null);
|
||||
Assert.That(result, Is.InstanceOf<SubtractionExpression>());
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Parse_MultipleDoubleQuotedIdentifiers_ReturnsCorrectExpression()
|
||||
{
|
||||
// Arrange
|
||||
var sql = "\"A\" + \"B\" * \"C\"";
|
||||
|
||||
// Act
|
||||
var result = _parser.Parse(sql);
|
||||
|
||||
// Assert
|
||||
Assert.That(result, Is.Not.Null);
|
||||
Assert.That(result, Is.InstanceOf<AdditionExpression>());
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Parentheses and Precedence Tests
|
||||
|
||||
[Test]
|
||||
public void Parse_SimpleParentheses_ReturnsCorrectExpression()
|
||||
{
|
||||
// Arrange
|
||||
var sql = "(\"A\" + \"B\")";
|
||||
|
||||
// Act
|
||||
var result = _parser.Parse(sql);
|
||||
|
||||
// Assert
|
||||
Assert.That(result, Is.Not.Null);
|
||||
Assert.That(result, Is.InstanceOf<AdditionExpression>());
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Parse_ParenthesesWithMultiplication_ReturnsCorrectPrecedence()
|
||||
{
|
||||
// Arrange
|
||||
var sql = "(1 + 2) * 3";
|
||||
|
||||
// Act
|
||||
var result = _parser.Parse(sql);
|
||||
|
||||
// Assert
|
||||
Assert.That(result, Is.Not.Null);
|
||||
Assert.That(result, Is.InstanceOf<MultiplicationExpression>());
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Parse_NestedParentheses_ReturnsCorrectExpression()
|
||||
{
|
||||
// Arrange
|
||||
var sql = "((\"A\" + \"B\") * \"C\") - \"D\"";
|
||||
|
||||
// Act
|
||||
var result = _parser.Parse(sql);
|
||||
|
||||
// Assert
|
||||
Assert.That(result, Is.Not.Null);
|
||||
Assert.That(result, Is.InstanceOf<SubtractionExpression>());
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Error Handling Tests
|
||||
|
||||
[Test]
|
||||
public void Parse_NullInput_ThrowsArgumentNullException()
|
||||
{
|
||||
// Arrange
|
||||
string? sql = null;
|
||||
|
||||
// Act & Assert
|
||||
Assert.Throws<ArgumentNullException>(() => _parser.Parse(sql!));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Parse_EmptyString_ThrowsArgumentNullException()
|
||||
{
|
||||
// Arrange
|
||||
var sql = "";
|
||||
|
||||
// Act & Assert
|
||||
Assert.Throws<ArgumentNullException>(() => _parser.Parse(sql));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Parse_Whitespace_ThrowsArgumentNullException()
|
||||
{
|
||||
// Arrange
|
||||
var sql = " ";
|
||||
|
||||
// Act & Assert
|
||||
Assert.Throws<ArgumentNullException>(() => _parser.Parse(sql));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TryParse_InvalidSyntax_ReturnsFalse()
|
||||
{
|
||||
// Arrange
|
||||
var sql = "\"A\" +"; // Incomplete expression
|
||||
|
||||
// Act
|
||||
var success = _parser.TryParse(sql, out var result);
|
||||
|
||||
// Assert
|
||||
Assert.That(success, Is.False);
|
||||
Assert.That(result, Is.Null);
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
@@ -0,0 +1,227 @@
|
||||
using Strata.SqlTools.SqlBreakdown.Enums.SQL;
|
||||
using Strata.SqlTools.SqlBreakdown.Exceptions;
|
||||
using StatementReader = Strata.SqlTools.Statements.Snowflake.StatementReader;
|
||||
|
||||
namespace Strata.SqlTools.SqlBreakdown.Tests.Snowflake;
|
||||
|
||||
[TestFixture]
|
||||
public class StatementReaderTests
|
||||
{
|
||||
[Test]
|
||||
public void StatementReader_ParsesCalculationString()
|
||||
{
|
||||
// Arrange
|
||||
var calculationString = "(1 + 2) / SUM([586883]) + SUM([586664]) - SUM([586664_1]) * 2.3";
|
||||
var sqlReader = new StatementReader(calculationString);
|
||||
var tokens = new List<(TokenType Type, string Value)>();
|
||||
|
||||
// Act
|
||||
while (sqlReader.Read())
|
||||
{
|
||||
tokens.Add((sqlReader.TokenType, sqlReader.TokenValue));
|
||||
}
|
||||
|
||||
// Assert
|
||||
Assert.That(tokens, Is.Not.Empty);
|
||||
Assert.That(tokens[0].Type, Is.EqualTo(TokenType.LeftParenthesis));
|
||||
Assert.That(tokens[1].Type, Is.EqualTo(TokenType.Number));
|
||||
Assert.That(tokens[1].Value, Is.EqualTo("1"));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void StatementReader_ParsesDoubleQuotedIdentifiers()
|
||||
{
|
||||
// Arrange - Snowflake-specific double-quoted identifiers
|
||||
var calculationString = "\"COLUMN_1\" + \"COLUMN_2\" * 2";
|
||||
var sqlReader = new StatementReader(calculationString);
|
||||
var tokens = new List<(TokenType Type, string Value)>();
|
||||
|
||||
// Act
|
||||
while (sqlReader.Read())
|
||||
{
|
||||
tokens.Add((sqlReader.TokenType, sqlReader.TokenValue));
|
||||
}
|
||||
|
||||
// Assert
|
||||
Assert.That(tokens, Has.Count.EqualTo(5)); // "COLUMN_1" + "COLUMN_2" * 2
|
||||
Assert.That(tokens[0].Type, Is.EqualTo(TokenType.ColumnIdentifier));
|
||||
Assert.That(tokens[0].Value, Is.EqualTo("COLUMN_1"));
|
||||
Assert.That(tokens[1].Type, Is.EqualTo(TokenType.Plus));
|
||||
Assert.That(tokens[2].Type, Is.EqualTo(TokenType.ColumnIdentifier));
|
||||
Assert.That(tokens[2].Value, Is.EqualTo("COLUMN_2"));
|
||||
Assert.That(tokens[3].Type, Is.EqualTo(TokenType.Multiply));
|
||||
Assert.That(tokens[4].Type, Is.EqualTo(TokenType.Number));
|
||||
Assert.That(tokens[4].Value, Is.EqualTo("2"));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void StatementReader_ParsesMixedIdentifierTypes()
|
||||
{
|
||||
// Arrange - Mix of double-quoted and square-bracketed identifiers
|
||||
var calculationString = "\"REVENUE\" + [COST] - PROFIT";
|
||||
var sqlReader = new StatementReader(calculationString);
|
||||
var tokens = new List<(TokenType Type, string Value)>();
|
||||
|
||||
// Act
|
||||
while (sqlReader.Read())
|
||||
{
|
||||
tokens.Add((sqlReader.TokenType, sqlReader.TokenValue));
|
||||
}
|
||||
|
||||
// Assert
|
||||
Assert.That(tokens, Has.Count.EqualTo(5));
|
||||
Assert.That(tokens[0].Type, Is.EqualTo(TokenType.ColumnIdentifier));
|
||||
Assert.That(tokens[0].Value, Is.EqualTo("REVENUE"));
|
||||
Assert.That(tokens[1].Type, Is.EqualTo(TokenType.Plus));
|
||||
Assert.That(tokens[2].Type, Is.EqualTo(TokenType.ColumnIdentifier));
|
||||
Assert.That(tokens[2].Value, Is.EqualTo("COST"));
|
||||
Assert.That(tokens[3].Type, Is.EqualTo(TokenType.Minus));
|
||||
Assert.That(tokens[4].Type, Is.EqualTo(TokenType.String));
|
||||
Assert.That(tokens[4].Value, Is.EqualTo("PROFIT"));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void StatementReader_ParsesFunctionWithDoubleQuotedColumn()
|
||||
{
|
||||
// Arrange
|
||||
var calculationString = "SUM(\"TOTAL_AMOUNT\") / COUNT(\"ID\")";
|
||||
var sqlReader = new StatementReader(calculationString);
|
||||
var tokens = new List<(TokenType Type, string Value)>();
|
||||
|
||||
// Act
|
||||
while (sqlReader.Read())
|
||||
{
|
||||
tokens.Add((sqlReader.TokenType, sqlReader.TokenValue));
|
||||
}
|
||||
|
||||
// Assert
|
||||
Assert.That(tokens, Is.Not.Empty);
|
||||
Assert.That(tokens[0].Type, Is.EqualTo(TokenType.FunctionStart));
|
||||
Assert.That(tokens[0].Value, Is.EqualTo("SUM"));
|
||||
Assert.That(tokens[1].Type, Is.EqualTo(TokenType.ColumnIdentifier));
|
||||
Assert.That(tokens[1].Value, Is.EqualTo("TOTAL_AMOUNT"));
|
||||
Assert.That(tokens[2].Type, Is.EqualTo(TokenType.FunctionEnd));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void StatementReader_ParsesNumericIdentifiersWithUnderscores()
|
||||
{
|
||||
// Arrange - Column identifiers starting with numbers (e.g., "3_REVENUE")
|
||||
var calculationString = "3_REVENUE + 4_COST - 5_PROFIT";
|
||||
var sqlReader = new StatementReader(calculationString);
|
||||
var tokens = new List<(TokenType Type, string Value)>();
|
||||
|
||||
// Act
|
||||
while (sqlReader.Read())
|
||||
{
|
||||
tokens.Add((sqlReader.TokenType, sqlReader.TokenValue));
|
||||
}
|
||||
|
||||
// Assert
|
||||
Assert.That(tokens, Has.Count.EqualTo(5));
|
||||
Assert.That(tokens[0].Type, Is.EqualTo(TokenType.ColumnIdentifier));
|
||||
Assert.That(tokens[0].Value, Is.EqualTo("3_REVENUE"));
|
||||
Assert.That(tokens[1].Type, Is.EqualTo(TokenType.Plus));
|
||||
Assert.That(tokens[2].Type, Is.EqualTo(TokenType.ColumnIdentifier));
|
||||
Assert.That(tokens[2].Value, Is.EqualTo("4_COST"));
|
||||
Assert.That(tokens[3].Type, Is.EqualTo(TokenType.Minus));
|
||||
Assert.That(tokens[4].Type, Is.EqualTo(TokenType.ColumnIdentifier));
|
||||
Assert.That(tokens[4].Value, Is.EqualTo("5_PROFIT"));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void StatementReader_ParsesDecimalNumbers()
|
||||
{
|
||||
// Arrange
|
||||
var calculationString = "123.45 + 67.89";
|
||||
var sqlReader = new StatementReader(calculationString);
|
||||
var tokens = new List<(TokenType Type, string Value)>();
|
||||
|
||||
// Act
|
||||
while (sqlReader.Read())
|
||||
{
|
||||
tokens.Add((sqlReader.TokenType, sqlReader.TokenValue));
|
||||
}
|
||||
|
||||
// Assert
|
||||
Assert.That(tokens, Has.Count.EqualTo(3));
|
||||
Assert.That(tokens[0].Type, Is.EqualTo(TokenType.Number));
|
||||
Assert.That(tokens[0].Value, Is.EqualTo("123.45"));
|
||||
Assert.That(tokens[1].Type, Is.EqualTo(TokenType.Plus));
|
||||
Assert.That(tokens[2].Type, Is.EqualTo(TokenType.Number));
|
||||
Assert.That(tokens[2].Value, Is.EqualTo("67.89"));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void StatementReader_ParsesUppercaseIdentifiers()
|
||||
{
|
||||
// Arrange - Snowflake conventionally uses uppercase for identifiers
|
||||
var calculationString = "REVENUE + COST";
|
||||
var sqlReader = new StatementReader(calculationString);
|
||||
var tokens = new List<(TokenType Type, string Value)>();
|
||||
|
||||
// Act
|
||||
while (sqlReader.Read())
|
||||
{
|
||||
tokens.Add((sqlReader.TokenType, sqlReader.TokenValue));
|
||||
}
|
||||
|
||||
// Assert
|
||||
Assert.That(tokens, Has.Count.EqualTo(3));
|
||||
Assert.That(tokens[0].Type, Is.EqualTo(TokenType.String));
|
||||
Assert.That(tokens[0].Value, Is.EqualTo("REVENUE"));
|
||||
Assert.That(tokens[1].Type, Is.EqualTo(TokenType.Plus));
|
||||
Assert.That(tokens[2].Type, Is.EqualTo(TokenType.String));
|
||||
Assert.That(tokens[2].Value, Is.EqualTo("COST"));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void StatementReader_HandlesEmptyString()
|
||||
{
|
||||
// Arrange & Act & Assert
|
||||
Assert.Throws<ArgumentNullException>(() => new StatementReader(""));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void StatementReader_ThrowsOnUnclosedDoubleQuote()
|
||||
{
|
||||
// Arrange
|
||||
var calculationString = "\"COLUMN_NAME + 5";
|
||||
var sqlReader = new StatementReader(calculationString);
|
||||
|
||||
// Act & Assert
|
||||
var ex = Assert.Throws<InvalidSyntaxException>(() =>
|
||||
{
|
||||
while (sqlReader.Read())
|
||||
{
|
||||
// Read all tokens
|
||||
}
|
||||
});
|
||||
|
||||
Assert.That(ex!.Message, Does.Contain("Expected closing double quote"));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void StatementReader_ParsesNestedFunctions()
|
||||
{
|
||||
// Arrange
|
||||
var calculationString = "SUM(AVG(\"REVENUE\"))";
|
||||
var sqlReader = new StatementReader(calculationString);
|
||||
var tokens = new List<(TokenType Type, string Value)>();
|
||||
|
||||
// Act
|
||||
while (sqlReader.Read())
|
||||
{
|
||||
tokens.Add((sqlReader.TokenType, sqlReader.TokenValue));
|
||||
}
|
||||
|
||||
// Assert
|
||||
Assert.That(tokens, Is.Not.Empty);
|
||||
Assert.That(tokens[0].Type, Is.EqualTo(TokenType.FunctionStart));
|
||||
Assert.That(tokens[0].Value, Is.EqualTo("SUM"));
|
||||
Assert.That(tokens[1].Type, Is.EqualTo(TokenType.FunctionStart));
|
||||
Assert.That(tokens[1].Value, Is.EqualTo("AVG"));
|
||||
Assert.That(tokens[2].Type, Is.EqualTo(TokenType.ColumnIdentifier));
|
||||
Assert.That(tokens[2].Value, Is.EqualTo("REVENUE"));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,531 @@
|
||||
using Strata.SqlTools.Breakdowns.Snowflake;
|
||||
using Strata.SqlTools.SqlBreakdown.Classes;
|
||||
using Strata.SqlTools.SqlBreakdown.Expressions;
|
||||
|
||||
namespace Strata.SqlTools.SqlBreakdown.Tests.Snowflake;
|
||||
|
||||
[TestFixture]
|
||||
public class WithClauseTests
|
||||
{
|
||||
#region Constructor Tests
|
||||
|
||||
[Test]
|
||||
public void Constructor_Default_CreatesEmptyInstance()
|
||||
{
|
||||
// Arrange & Act
|
||||
var withClause = new WithClause();
|
||||
|
||||
// Assert
|
||||
Assert.That(withClause.TableName, Is.EqualTo(string.Empty));
|
||||
Assert.That(withClause.Sql, Is.Null);
|
||||
Assert.That(withClause.Query, Is.Null);
|
||||
Assert.That(withClause.Clause, Is.Null.Or.Empty);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Constructor_WithTableNameAndQuery_SetsProperties()
|
||||
{
|
||||
// Arrange
|
||||
var tableName = "active_users";
|
||||
var query = new QueryBreakdown("id, name", "users", "active = 1");
|
||||
|
||||
// Act
|
||||
var withClause = new WithClause(tableName, query);
|
||||
|
||||
// Assert
|
||||
Assert.That(withClause.TableName, Is.EqualTo(tableName));
|
||||
Assert.That(withClause.Query, Is.EqualTo(query));
|
||||
Assert.That(withClause.Sql, Is.Not.Null);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Constructor_WithTableNameAndQuery_ThrowsOnNullTableName()
|
||||
{
|
||||
// Arrange
|
||||
var query = new QueryBreakdown("id, name", "users");
|
||||
|
||||
// Act & Assert
|
||||
Assert.Throws<ArgumentNullException>(() => new WithClause(null!, query));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Constructor_WithTableNameAndQuery_ThrowsOnNullQuery()
|
||||
{
|
||||
// Arrange
|
||||
var tableName = "active_users";
|
||||
|
||||
// Act & Assert
|
||||
Assert.Throws<ArgumentNullException>(() => new WithClause(tableName, (QueryBreakdown)null!));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Constructor_WithTableNameAndSql_SetsProperties()
|
||||
{
|
||||
// Arrange
|
||||
var tableName = "active_users";
|
||||
var sqlClauses = new SqlClauses
|
||||
{
|
||||
SelectClause = new SqlExpressionClause { Clause = "id, name" },
|
||||
FromClause = new SqlClause { Clause = "users" }
|
||||
};
|
||||
|
||||
// Act
|
||||
var withClause = new WithClause(tableName, sqlClauses);
|
||||
|
||||
// Assert
|
||||
Assert.That(withClause.TableName, Is.EqualTo(tableName));
|
||||
Assert.That(withClause.Sql, Is.EqualTo(sqlClauses));
|
||||
Assert.That(withClause.Query, Is.Null);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Constructor_WithTableNameAndSql_ThrowsOnNullTableName()
|
||||
{
|
||||
// Arrange
|
||||
var sqlClauses = new SqlClauses
|
||||
{
|
||||
SelectClause = new SqlExpressionClause { Clause = "id" }
|
||||
};
|
||||
|
||||
// Act & Assert
|
||||
Assert.Throws<ArgumentNullException>(() => new WithClause(null!, sqlClauses));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Constructor_WithTableNameAndSql_ThrowsOnNullSql()
|
||||
{
|
||||
// Arrange
|
||||
var tableName = "active_users";
|
||||
|
||||
// Act & Assert
|
||||
Assert.Throws<ArgumentNullException>(() => new WithClause(tableName, (SqlClauses)null!));
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Property Synchronization Tests
|
||||
|
||||
[Test]
|
||||
public void Sql_WhenQueryExists_ReturnsComputedClauses()
|
||||
{
|
||||
// Arrange
|
||||
var query = new QueryBreakdown("user_id, name", "users");
|
||||
var withClause = new WithClause("active_users", query);
|
||||
|
||||
// Act
|
||||
var sql = withClause.Sql;
|
||||
|
||||
// Assert
|
||||
Assert.That(sql, Is.Not.Null);
|
||||
Assert.That(sql!.SelectClause?.Clause, Is.EqualTo("user_id, name"));
|
||||
Assert.That(sql.FromClause?.Clause, Is.EqualTo("users"));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Sql_WhenQueryNull_ReturnsStoredClauses()
|
||||
{
|
||||
// Arrange
|
||||
var sqlClauses = new SqlClauses
|
||||
{
|
||||
SelectClause = new SqlExpressionClause { Clause = "column1" },
|
||||
FromClause = new SqlClause { Clause = "table1" }
|
||||
};
|
||||
var withClause = new WithClause("test_cte", sqlClauses);
|
||||
|
||||
// Act
|
||||
var sql = withClause.Sql;
|
||||
|
||||
// Assert
|
||||
Assert.That(sql, Is.EqualTo(sqlClauses));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Sql_SetWhenQueryNull_StoresClauses()
|
||||
{
|
||||
// Arrange
|
||||
var withClause = new WithClause { TableName = "test_cte" };
|
||||
var sqlClauses = new SqlClauses
|
||||
{
|
||||
SelectClause = new SqlExpressionClause { Clause = "column1" },
|
||||
FromClause = new SqlClause { Clause = "table1" }
|
||||
};
|
||||
|
||||
// Act
|
||||
withClause.Sql = sqlClauses;
|
||||
|
||||
// Assert
|
||||
Assert.That(withClause.Sql, Is.EqualTo(sqlClauses));
|
||||
Assert.That(withClause.Query, Is.Null);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Sql_SetWhenQueryExists_AppliesToQuery()
|
||||
{
|
||||
// Arrange
|
||||
var query = new QueryBreakdown();
|
||||
var withClause = new WithClause { TableName = "test_cte", Query = query };
|
||||
var sqlClauses = new SqlClauses
|
||||
{
|
||||
SelectClause = new SqlExpressionClause { Clause = "column1" },
|
||||
FromClause = new SqlClause { Clause = "table1" }
|
||||
};
|
||||
|
||||
// Act
|
||||
withClause.Sql = sqlClauses;
|
||||
|
||||
// Assert - Clauses are applied to the Query
|
||||
Assert.That(query.SelectClause.Clause, Is.EqualTo("column1"));
|
||||
Assert.That(query.FromClause.Clause, Is.EqualTo("table1"));
|
||||
// And Sql property returns the applied clauses (from Query, not stored)
|
||||
Assert.That(withClause.Sql?.SelectClause?.Clause, Is.EqualTo("column1"));
|
||||
Assert.That(withClause.Sql?.FromClause?.Clause, Is.EqualTo("table1"));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Query_SetWhenSqlAlreadySet_AppliesSqlToQuery()
|
||||
{
|
||||
// Arrange
|
||||
var withClause = new WithClause { TableName = "test_cte" };
|
||||
var sqlClauses = new SqlClauses
|
||||
{
|
||||
SelectClause = new SqlExpressionClause { Clause = "column1" },
|
||||
FromClause = new SqlClause { Clause = "table1" }
|
||||
};
|
||||
withClause.Sql = sqlClauses;
|
||||
|
||||
// Act
|
||||
var query = new QueryBreakdown();
|
||||
withClause.Query = query;
|
||||
|
||||
// Assert - Stored SQL is applied to Query
|
||||
Assert.That(query.SelectClause.Clause, Is.EqualTo("column1"));
|
||||
Assert.That(query.FromClause.Clause, Is.EqualTo("table1"));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Query_SetWhenSqlAlreadySet_ClearsSqlStorage()
|
||||
{
|
||||
// Arrange
|
||||
var withClause = new WithClause { TableName = "test_cte" };
|
||||
var sqlClauses = new SqlClauses
|
||||
{
|
||||
SelectClause = new SqlExpressionClause { Clause = "column1" }
|
||||
};
|
||||
withClause.Sql = sqlClauses;
|
||||
|
||||
// Act
|
||||
var query = new QueryBreakdown();
|
||||
withClause.Query = query;
|
||||
|
||||
// Assert - Stored _sql should be cleared, but Sql property still returns from Query
|
||||
Assert.That(withClause.Sql, Is.Not.Null);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Sql_SetThenQuerySet_BidirectionalSync()
|
||||
{
|
||||
// Arrange: Set Sql first, then Query
|
||||
var withClause = new WithClause { TableName = "test_cte" };
|
||||
var sqlClauses = new SqlClauses
|
||||
{
|
||||
SelectClause = new SqlExpressionClause { Clause = "old_column" },
|
||||
FromClause = new SqlClause { Clause = "old_table" }
|
||||
};
|
||||
withClause.Sql = sqlClauses;
|
||||
|
||||
// Act: Setting new Query should apply stored Sql
|
||||
var query = new QueryBreakdown();
|
||||
withClause.Query = query;
|
||||
|
||||
// Assert
|
||||
Assert.That(query.SelectClause.Clause, Is.EqualTo("old_column"));
|
||||
Assert.That(query.FromClause.Clause, Is.EqualTo("old_table"));
|
||||
Assert.That(withClause.Query, Is.EqualTo(query));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Query_SetThenSqlSet_BidirectionalSync()
|
||||
{
|
||||
// Arrange: Set Query first
|
||||
var query = new QueryBreakdown();
|
||||
var withClause = new WithClause { TableName = "test_cte", Query = query };
|
||||
|
||||
// Act: Setting new Sql should apply to Query
|
||||
var sqlClauses = new SqlClauses
|
||||
{
|
||||
SelectClause = new SqlExpressionClause { Clause = "new_column" },
|
||||
FromClause = new SqlClause { Clause = "new_table" }
|
||||
};
|
||||
withClause.Sql = sqlClauses;
|
||||
|
||||
// Assert
|
||||
Assert.That(query.SelectClause.Clause, Is.EqualTo("new_column"));
|
||||
Assert.That(query.FromClause.Clause, Is.EqualTo("new_table"));
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region GetClauses Integration Tests
|
||||
|
||||
[Test]
|
||||
public void GetClauses_FromQueryBreakdown_ReturnsAllClauseProperties()
|
||||
{
|
||||
// Arrange
|
||||
var query = new QueryBreakdown("id, name", "users", "active = 1");
|
||||
query.GroupByClause = new SqlExpressionClause { Clause = "department" };
|
||||
query.HavingClause = new SqlExpressionClause { Clause = "COUNT(*) > 5" };
|
||||
query.OrderByClause = new SqlExpressionClause { Clause = "name ASC" };
|
||||
|
||||
var withClause = new WithClause("user_summary", query);
|
||||
|
||||
// Act
|
||||
var sql = withClause.Sql;
|
||||
|
||||
// Assert
|
||||
Assert.That(sql, Is.Not.Null);
|
||||
Assert.That(sql!.SelectClause?.Clause, Is.EqualTo("id, name"));
|
||||
Assert.That(sql.FromClause?.Clause, Is.EqualTo("users"));
|
||||
Assert.That(sql.WhereClause?.Clause, Is.EqualTo("active = 1"));
|
||||
Assert.That(sql.GroupByClause?.Clause, Is.EqualTo("department"));
|
||||
Assert.That(sql.HavingClause?.Clause, Is.EqualTo("COUNT(*) > 5"));
|
||||
Assert.That(sql.OrderByClause?.Clause, Is.EqualTo("name ASC"));
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region ApplyClauses Integration Tests
|
||||
|
||||
[Test]
|
||||
public void ApplyClauses_WhenSqlSet_AppliesAllClauses()
|
||||
{
|
||||
// Arrange
|
||||
var query = new QueryBreakdown();
|
||||
var withClause = new WithClause { TableName = "test_cte", Query = query };
|
||||
|
||||
var sqlClauses = new SqlClauses
|
||||
{
|
||||
SelectClause = new SqlExpressionClause { Clause = "id, name" },
|
||||
FromClause = new SqlClause { Clause = "users" },
|
||||
WhereClause = new SqlExpressionClause { Clause = "active = 1" },
|
||||
GroupByClause = new SqlExpressionClause { Clause = "department" },
|
||||
HavingClause = new SqlExpressionClause { Clause = "COUNT(*) > 5" },
|
||||
OrderByClause = new SqlExpressionClause { Clause = "name ASC" }
|
||||
};
|
||||
|
||||
// Act
|
||||
withClause.Sql = sqlClauses;
|
||||
|
||||
// Assert
|
||||
Assert.That(query.SelectClause.Clause, Is.EqualTo("id, name"));
|
||||
Assert.That(query.FromClause.Clause, Is.EqualTo("users"));
|
||||
Assert.That(query.WhereClause.Clause, Is.EqualTo("active = 1"));
|
||||
Assert.That(query.GroupByClause.Clause, Is.EqualTo("department"));
|
||||
Assert.That(query.HavingClause.Clause, Is.EqualTo("COUNT(*) > 5"));
|
||||
Assert.That(query.OrderByClause.Clause, Is.EqualTo("name ASC"));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void ApplyClauses_WhenSqlPartial_AppliesOnlyNonNullClauses()
|
||||
{
|
||||
// Arrange
|
||||
var query = new QueryBreakdown("original_select", "original_from");
|
||||
var withClause = new WithClause { TableName = "test_cte", Query = query };
|
||||
|
||||
var sqlClauses = new SqlClauses
|
||||
{
|
||||
SelectClause = new SqlExpressionClause { Clause = "new_select" }
|
||||
// Only SelectClause is set; others are null
|
||||
};
|
||||
|
||||
// Act
|
||||
withClause.Sql = sqlClauses;
|
||||
|
||||
// Assert
|
||||
Assert.That(query.SelectClause.Clause, Is.EqualTo("new_select"));
|
||||
Assert.That(query.FromClause.Clause, Is.EqualTo("original_from")); // Unchanged
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Comment Handling Tests
|
||||
|
||||
[Test]
|
||||
public void WithClause_PreservesCommentProperty()
|
||||
{
|
||||
// Arrange
|
||||
var comment = "-- This CTE selects active users";
|
||||
var withClause = new WithClause { TableName = "active_users", Comment = comment };
|
||||
|
||||
// Act & Assert
|
||||
Assert.That(withClause.Comment, Is.EqualTo(comment));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void WithClause_PreservesClauseProperty()
|
||||
{
|
||||
// Arrange
|
||||
var clause = "SELECT id, name FROM users WHERE active = 1";
|
||||
var withClause = new WithClause { TableName = "active_users", Clause = clause };
|
||||
|
||||
// Act & Assert
|
||||
Assert.That(withClause.Clause, Is.EqualTo(clause));
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Edge Cases and Null Handling
|
||||
|
||||
[Test]
|
||||
public void Sql_SetToNull_ClearsStorage()
|
||||
{
|
||||
// Arrange
|
||||
var sqlClauses = new SqlClauses { SelectClause = new SqlExpressionClause { Clause = "id" } };
|
||||
var withClause = new WithClause { TableName = "test_cte", Sql = sqlClauses };
|
||||
|
||||
// Act
|
||||
withClause.Sql = null;
|
||||
|
||||
// Assert
|
||||
Assert.That(withClause.Sql, Is.Null);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Query_SetToNull_ClearsReference()
|
||||
{
|
||||
// Arrange
|
||||
var query = new QueryBreakdown("id", "users");
|
||||
var withClause = new WithClause("test_cte", query);
|
||||
|
||||
// Act
|
||||
withClause.Query = null;
|
||||
|
||||
// Assert
|
||||
Assert.That(withClause.Query, Is.Null);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void ApplyClauses_WithNullSqlClauses_DoesNotThrow()
|
||||
{
|
||||
// Arrange
|
||||
var query = new QueryBreakdown("id", "users");
|
||||
var withClause = new WithClause { TableName = "test_cte", Query = query };
|
||||
|
||||
// Act & Assert
|
||||
Assert.DoesNotThrow(() => withClause.Sql = null);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Sql_WhenBothQueryAndStorageNull_ReturnsNull()
|
||||
{
|
||||
// Arrange
|
||||
var withClause = new WithClause { TableName = "test_cte" };
|
||||
|
||||
// Act
|
||||
var result = withClause.Sql;
|
||||
|
||||
// Assert
|
||||
Assert.That(result, Is.Null);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region ComplexScenario Tests
|
||||
|
||||
[Test]
|
||||
public void ComplexScenario_NestedCTE_WithParameters()
|
||||
{
|
||||
// Arrange: Create inner CTE with parameters
|
||||
var innerQuery = new QueryBreakdown("id, name", "users", "created_date >= :start_date");
|
||||
innerQuery.AddParameter(":start_date", DateTime.Today);
|
||||
|
||||
var innerCte = new WithClause("recent_users", innerQuery);
|
||||
|
||||
// Arrange: Create outer CTE that uses inner CTE
|
||||
var outerQuery = new QueryBreakdown("*", "recent_users");
|
||||
var outerCte = new WithClause("user_report", outerQuery);
|
||||
|
||||
// Act
|
||||
var innerSql = innerCte.Sql;
|
||||
var outerSql = outerCte.Sql;
|
||||
|
||||
// Assert - Inner CTE has parameter and correct clauses
|
||||
Assert.That(innerQuery.ParameterList.Count(), Is.EqualTo(1));
|
||||
Assert.That(innerSql?.SelectClause?.Clause, Is.EqualTo("id, name"));
|
||||
Assert.That(innerSql?.WhereClause?.Clause, Is.EqualTo("created_date >= :start_date"));
|
||||
|
||||
// Assert - Outer CTE references inner CTE
|
||||
Assert.That(outerSql?.SelectClause?.Clause, Is.EqualTo("*"));
|
||||
Assert.That(outerSql?.FromClause?.Clause, Is.EqualTo("recent_users"));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void ComplexScenario_MultipleClauseUpdates()
|
||||
{
|
||||
// Arrange
|
||||
var query = new QueryBreakdown("id", "users");
|
||||
var withClause = new WithClause("test_cte", query);
|
||||
|
||||
// Act: Update clauses via Sql property multiple times
|
||||
var sql1 = new SqlClauses { SelectClause = new SqlExpressionClause { Clause = "updated_column_1" } };
|
||||
withClause.Sql = sql1;
|
||||
|
||||
var sql2 = new SqlClauses { SelectClause = new SqlExpressionClause { Clause = "updated_column_2" } };
|
||||
withClause.Sql = sql2;
|
||||
|
||||
// Assert
|
||||
Assert.That(query.SelectClause.Clause, Is.EqualTo("updated_column_2"));
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region GetSql Integration Tests
|
||||
|
||||
[Test]
|
||||
public void GetSql_WithSingleCTE_GeneratesCorrectSyntax()
|
||||
{
|
||||
// Arrange
|
||||
var cteQuery = new QueryBreakdown("id, name", "users", "active = 1");
|
||||
var mainQuery = new QueryBreakdown("*", "active_users");
|
||||
mainQuery.AddWithClause("active_users", cteQuery);
|
||||
|
||||
// Act
|
||||
var sql = mainQuery.GetSql();
|
||||
|
||||
// Assert
|
||||
Assert.That(sql, Does.Contain("WITH"));
|
||||
Assert.That(sql, Does.Contain("active_users"));
|
||||
Assert.That(sql, Does.Contain("AS"));
|
||||
// Check for keywords/content (allowing for formatting variations)
|
||||
Assert.That(sql.ToUpper(), Does.Contain("SELECT"));
|
||||
Assert.That(sql, Does.Contain("id, name"));
|
||||
Assert.That(sql, Does.Contain("users"));
|
||||
Assert.That(sql, Does.Contain("active = 1"));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void GetSql_WithMultipleCTEs_GeneratesCorrectSyntax()
|
||||
{
|
||||
// Arrange
|
||||
var cte1 = new QueryBreakdown("id, name", "users", "active = 1");
|
||||
var cte2 = new QueryBreakdown("user_id, order_count", "orders");
|
||||
|
||||
var mainQuery = new QueryBreakdown("*", "active_users");
|
||||
mainQuery.AddWithClause("active_users", cte1);
|
||||
mainQuery.AddWithClause("user_orders", cte2);
|
||||
|
||||
// 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"));
|
||||
// Verify order: first CTE comes before second CTE
|
||||
var pos1 = sql.IndexOf("active_users");
|
||||
var pos2 = sql.IndexOf("user_orders");
|
||||
Assert.That(pos1, Is.GreaterThan(-1));
|
||||
Assert.That(pos2, Is.GreaterThan(-1));
|
||||
Assert.That(pos1, Is.LessThan(pos2)); }
|
||||
|
||||
#endregion
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<LangVersion>latest</LangVersion>
|
||||
|
||||
<IsPackable>false</IsPackable>
|
||||
<IsTestProject>true</IsTestProject>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="coverlet.collector" Version="6.0.0" />
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.8.0" />
|
||||
<PackageReference Include="NUnit" Version="3.14.0" />
|
||||
<PackageReference Include="NUnit.Analyzers" Version="3.9.0" />
|
||||
<PackageReference Include="NUnit3TestAdapter" Version="4.5.0" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Using Include="NUnit.Framework" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\src\Strata.SqlTools.Snowflake\Strata.SqlTools.Snowflake.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
Reference in New Issue
Block a user