chore: initial git load of code space

This commit is contained in:
Thom Lamb
2026-05-12 08:52:33 -05:00
parent 9abada692f
commit 5e467bcc9c
384 changed files with 65960 additions and 2 deletions
@@ -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>