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,221 @@
using NUnit.Framework;
using Strata.SqlTools.Breakdowns.SqlServer;
using Strata.SqlTools.SqlBreakdown.Classes;
namespace Strata.SqlTools.SqlBreakdown.Tests.SqlServer.Performance;
[TestFixture]
public class GetClausesCachingTests
{
[Test]
public void GetClauses_CalledMultipleTimes_ReturnsSameInstance()
{
// Arrange
var query = new QueryBreakdown("id, name", "users");
// Act
var clauses1 = query.GetClauses();
var clauses2 = query.GetClauses();
// Assert
Assert.That(clauses2, Is.SameAs(clauses1), "GetClauses should return the same cached instance when clauses haven't changed");
}
[Test]
public void GetClauses_AfterSelectClauseChange_ReturnsNewInstance()
{
// Arrange
var query = new QueryBreakdown("id", "users");
var clauses1 = query.GetClauses();
// Act - Replace entire clause object to trigger property setter
query.SelectClause = new SqlExpressionClause { Clause = "id, name, email" };
var clauses2 = query.GetClauses();
// Assert
Assert.That(clauses2, Is.Not.SameAs(clauses1), "GetClauses should return a new instance after SelectClause changes");
Assert.That(clauses2.SelectClause.Clause, Is.EqualTo("id, name, email"));
}
[Test]
public void GetClauses_AfterFromClauseChange_ReturnsNewInstance()
{
// Arrange
var query = new QueryBreakdown("id", "users");
var clauses1 = query.GetClauses();
// Act - Replace entire clause object to trigger property setter
query.FromClause = new SqlClause { Clause = "customers" };
var clauses2 = query.GetClauses();
// Assert
Assert.That(clauses2, Is.Not.SameAs(clauses1), "GetClauses should return a new instance after FromClause changes");
Assert.That(clauses2.FromClause.Clause, Is.EqualTo("customers"));
}
[Test]
public void GetClauses_AfterWhereClauseChange_ReturnsNewInstance()
{
// Arrange
var query = new QueryBreakdown("id", "users", "active = 1");
var clauses1 = query.GetClauses();
// Act - Replace entire clause object to trigger property setter
query.WhereClause = new SqlExpressionClause { Clause = "active = 1 AND deleted = 0" };
var clauses2 = query.GetClauses();
// Assert
Assert.That(clauses2, Is.Not.SameAs(clauses1), "GetClauses should return a new instance after WhereClause changes");
Assert.That(clauses2.WhereClause.Clause, Is.EqualTo("active = 1 AND deleted = 0"));
}
[Test]
public void GetClauses_AfterGroupByClauseChange_ReturnsNewInstance()
{
// Arrange
var query = new QueryBreakdown("COUNT(*) AS total", "users");
query.GroupByClause = new SqlExpressionClause { Clause = "status" };
var clauses1 = query.GetClauses();
// Act - Replace entire clause object to trigger property setter
query.GroupByClause = new SqlExpressionClause { Clause = "status, type" };
var clauses2 = query.GetClauses();
// Assert
Assert.That(clauses2, Is.Not.SameAs(clauses1), "GetClauses should return a new instance after GroupByClause changes");
Assert.That(clauses2.GroupByClause.Clause, Is.EqualTo("status, type"));
}
[Test]
public void GetClauses_AfterHavingClauseChange_ReturnsNewInstance()
{
// Arrange
var query = new QueryBreakdown("COUNT(*) AS total", "users");
query.HavingClause = new SqlExpressionClause { Clause = "COUNT(*) > 10" };
var clauses1 = query.GetClauses();
// Act - Replace entire clause object to trigger property setter
query.HavingClause = new SqlExpressionClause { Clause = "COUNT(*) > 100" };
var clauses2 = query.GetClauses();
// Assert
Assert.That(clauses2, Is.Not.SameAs(clauses1), "GetClauses should return a new instance after HavingClause changes");
Assert.That(clauses2.HavingClause.Clause, Is.EqualTo("COUNT(*) > 100"));
}
[Test]
public void GetClauses_AfterOrderByClauseChange_ReturnsNewInstance()
{
// Arrange
var query = new QueryBreakdown("id, name", "users");
query.OrderByClause = new SqlExpressionClause { Clause = "id ASC" };
var clauses1 = query.GetClauses();
// Act - Replace entire clause object to trigger property setter
query.OrderByClause = new SqlExpressionClause { Clause = "name DESC" };
var clauses2 = query.GetClauses();
// Assert
Assert.That(clauses2, Is.Not.SameAs(clauses1), "GetClauses should return a new instance after OrderByClause changes");
Assert.That(clauses2.OrderByClause.Clause, Is.EqualTo("name DESC"));
}
[Test]
public void GetClauses_AfterMultipleChanges_CacheInvalidatedEachTime()
{
// Arrange
var query = new QueryBreakdown("id", "users");
var clauses1 = query.GetClauses();
// Act & Assert - Change 1
query.SelectClause = new SqlExpressionClause { Clause = "id, name" };
var clauses2 = query.GetClauses();
Assert.That(clauses2, Is.Not.SameAs(clauses1));
// Get again without changes - should return cached
var clauses2b = query.GetClauses();
Assert.That(clauses2b, Is.SameAs(clauses2));
// Act & Assert - Change 2
query.WhereClause = new SqlExpressionClause { Clause = "active = 1" };
var clauses3 = query.GetClauses();
Assert.That(clauses3, Is.Not.SameAs(clauses2));
// Get again without changes - should return cached
var clauses3b = query.GetClauses();
Assert.That(clauses3b, Is.SameAs(clauses3));
}
[Test]
public void GetClauses_ReplacingEntireClauseObject_ReturnsNewInstance()
{
// Arrange
var query = new QueryBreakdown("id", "users");
var clauses1 = query.GetClauses();
// Act - Replace entire clause object
query.SelectClause = new SqlExpressionClause { Clause = "id, name, email" };
var clauses2 = query.GetClauses();
// Assert
Assert.That(clauses2, Is.Not.SameAs(clauses1), "GetClauses should return a new instance when clause object is replaced");
Assert.That(clauses2.SelectClause.Clause, Is.EqualTo("id, name, email"));
}
[Test]
public void GetClauses_ApplyClauses_InvalidatesCache()
{
// Arrange
var query = new QueryBreakdown("id", "users");
var clauses1 = query.GetClauses();
var newClauses = new SqlClauses
{
SelectClause = new SqlExpressionClause { Clause = "id, name" },
FromClause = new SqlClause { Clause = "customers" }
};
// Act
query.ApplyClauses(newClauses);
var clauses2 = query.GetClauses();
// Assert
Assert.That(clauses2, Is.Not.SameAs(clauses1), "GetClauses should return a new instance after ApplyClauses");
Assert.That(clauses2.SelectClause.Clause, Is.EqualTo("id, name"));
Assert.That(clauses2.FromClause.Clause, Is.EqualTo("customers"));
}
[Test]
public void GetClauses_InitialCall_ReturnsPopulatedObject()
{
// Arrange
var query = new QueryBreakdown("id, name", "users", "active = 1");
query.OrderByClause.Clause = "name ASC";
// Act
var clauses = query.GetClauses();
// Assert
Assert.That(clauses, Is.Not.Null);
Assert.That(clauses.SelectClause.Clause, Is.EqualTo("id, name"));
Assert.That(clauses.FromClause.Clause, Is.EqualTo("users"));
Assert.That(clauses.WhereClause.Clause, Is.EqualTo("active = 1"));
Assert.That(clauses.OrderByClause.Clause, Is.EqualTo("name ASC"));
}
[Test]
public void GetClauses_AfterNoChanges_ReturnsSameCachedInstance()
{
// Arrange
var query = new QueryBreakdown("id, name", "users");
// Act - Call GetClauses multiple times without any modifications
var clauses1 = query.GetClauses();
var clauses2 = query.GetClauses();
var clauses3 = query.GetClauses();
// Assert - All should be the same instance
Assert.That(clauses2, Is.SameAs(clauses1));
Assert.That(clauses3, Is.SameAs(clauses1));
}
}
@@ -0,0 +1,371 @@
using Strata.SqlTools.Breakdowns.SqlServer;
using Strata.SqlTools.SqlBreakdown.Classes;
namespace Strata.SqlTools.SqlBreakdown.Tests.SqlServer;
[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 = 1");
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 // No column list
};
mainQuery.AddWithClause(withClause);
// Act
var sql = mainQuery.GetSql();
// Assert
Assert.That(sql, Contains.Substring("all_users AS"), "Without column list, should just have table name");
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>() // Empty list
};
mainQuery.AddWithClause(withClause);
// Act
var sql = mainQuery.GetSql();
// Assert
Assert.That(sql, Contains.Substring("data AS"), "Empty column list should not generate parentheses");
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)"));
}
[Test]
public void GetSql_WithMixedColumnListPresence_HandlesCorrectly()
{
// Arrange
var cte1 = new QueryBreakdown("id, name", "users");
var cte2 = new QueryBreakdown("dept_id, dept_name", "departments");
var mainQuery = new QueryBreakdown("*", "combined");
var withClause1 = new WithClause("user_data", cte1)
{
ColumnList = new List<string> { "uid", "uname" }
};
var withClause2 = new WithClause("dept_data", cte2)
{
// No column list for second CTE
};
mainQuery.AddWithClause(withClause1);
mainQuery.AddWithClause(withClause2);
// Act
var sql = mainQuery.GetSql();
// Assert
Assert.That(sql, Contains.Substring("user_data (uid, uname)"), "First CTE should have column list");
Assert.That(sql, Contains.Substring("dept_data AS"), "Second CTE should not have column list");
Assert.That(sql, Does.Not.Contain("dept_data ()"), "Second CTE should not have empty parentheses");
}
#endregion
#region Column List with Parameters
[Test]
public void ColumnList_PreservesParameters()
{
// Arrange
var cte = new QueryBreakdown("id, name, salary", "employees", "salary > @MinSalary");
cte.AddParameter("MinSalary", 50000);
var mainQuery = new QueryBreakdown("*", "high_earners");
cte.AddParameter("CompanyId", 1);
var withClause = new WithClause("high_earners", cte)
{
ColumnList = new List<string> { "emp_id", "emp_name", "emp_salary" }
};
mainQuery.AddWithClause(withClause);
// Act
var merged = mainQuery.GetMergedParameters();
var sql = mainQuery.GetSql();
// 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 Nested CTE Column Lists
[Test]
public void ColumnList_WithNestedCTEs()
{
// Arrange
var innerCte = new QueryBreakdown("id, dept_id", "employees");
var outerCte = new QueryBreakdown("*", "emp_summary");
var innerWithClause = new WithClause("emp_data", innerCte)
{
ColumnList = new List<string> { "emp_id", "dept_id" }
};
outerCte.AddWithClause(innerWithClause);
var mainQuery = new QueryBreakdown("*", "final_result");
var outerWithClause = new WithClause("emp_hierarchy", outerCte)
{
ColumnList = new List<string> { "info" }
};
mainQuery.AddWithClause(outerWithClause);
// Act
var sql = mainQuery.GetSql();
// Assert
Assert.That(sql, Contains.Substring("emp_hierarchy (info)"), "Outer CTE should have column list");
Assert.That(sql, Contains.Substring("emp_data (emp_id, dept_id)"), "Inner CTE should have column list");
}
#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, col9, col10", "source");
var mainQuery = new QueryBreakdown("*", "results");
var columns = new List<string>
{
"c1", "c2", "c3", "c4", "c5", "c6", "c7", "c8", "c9", "c10"
};
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" }; // Different order from source
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)"), "Column order should be preserved");
}
[Test]
public void ColumnList_AllowsSpecialCharacterNames()
{
// Arrange
var cte = new QueryBreakdown("col1, col2", "source");
var mainQuery = new QueryBreakdown("*", "output");
var columns = new List<string> { "[column 1]", "[column-2]" };
var withClause = new WithClause("special_cte", cte)
{
ColumnList = columns
};
mainQuery.AddWithClause(withClause);
// Act
var sql = mainQuery.GetSql();
// Assert
Assert.That(sql, Contains.Substring("special_cte ([column 1], [column-2])"));
}
#endregion
#region Real-World Scenarios
[Test]
public void ColumnList_CompleteBIScenario()
{
// Scenario: Business Intelligence query with renamed columns
// Arrange
var salesData = new QueryBreakdown(
"customer_id, order_id, order_date, order_amount",
"sales",
"order_date >= @StartDate AND order_amount >= @MinAmount"
);
salesData.AddParameter("StartDate", DateTime.Today.AddMonths(-12));
salesData.AddParameter("MinAmount", 1000);
var mainQuery = new QueryBreakdown(
"customer_id, order_count, total_revenue",
"sales_summary"
);
mainQuery.AddParameter("CurrencyConversion", 1.1);
var withClause = new WithClause("recent_sales", salesData)
{
ColumnList = new List<string>
{
"cust_id", // Maps to customer_id
"order_id", // Maps to order_id
"sale_date", // Maps to order_date
"amount" // Maps to order_amount
}
};
mainQuery.AddWithClause(withClause);
// Act
var sql = mainQuery.GetSql();
var params_all = mainQuery.GetMergedParameters();
// Assert
Assert.That(sql, Contains.Substring("recent_sales (cust_id, order_id, sale_date, amount)"));
var paramKeys = params_all.Keys.ToList();
Assert.That(paramKeys.Any(k => k.Contains("StartDate")));
Assert.That(paramKeys.Any(k => k.Contains("MinAmount")));
Assert.That(paramKeys.Any(k => k.Contains("CurrencyConversion")));
}
#endregion
}
@@ -0,0 +1,285 @@
using Strata.SqlTools.Breakdowns.SqlServer;
namespace Strata.SqlTools.SqlBreakdown.Tests.SqlServer;
[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 EmployeeCount
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 EmployeeCount
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.SqlServer.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 - 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 SQL:");
TestContext.WriteLine(regeneratedSql);
}
}
@@ -0,0 +1,371 @@
using Strata.SqlTools.Breakdowns.SqlServer;
namespace Strata.SqlTools.SqlServer.Tests.SqlServer;
[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 AddRange_MultipleQueries_AddsAllQueries()
{
// Arrange
var queries = new List<QueryBreakdown>
{
new QueryBreakdown("ID, Name", "Users"),
new QueryBreakdown("OrderID, Amount", "Orders")
};
// Act
_collection.AddRange(queries);
// Assert
Assert.That(_collection.Count, Is.EqualTo(2));
}
[Test]
public void Remove_ExistingQuery_RemovesFromCollection()
{
// Arrange
var query = new QueryBreakdown("ID, Name", "Users");
_collection.Add(query);
// Act
var removed = _collection.Remove(query);
// Assert
Assert.That(removed, Is.True);
Assert.That(_collection.Count, Is.EqualTo(0));
}
[Test]
public void Remove_NonExistentQuery_ReturnsFalse()
{
// Arrange
var query1 = new QueryBreakdown("ID, Name", "Users");
var query2 = new QueryBreakdown("OrderID, Amount", "Orders");
_collection.Add(query1);
// Act
var removed = _collection.Remove(query2);
// Assert
Assert.That(removed, Is.False);
Assert.That(_collection.Count, Is.EqualTo(1));
}
[Test]
public void Clear_WithQueries_RemovesAllQueries()
{
// Arrange
_collection.Add(new QueryBreakdown("ID, Name", "Users"));
_collection.Add(new QueryBreakdown("OrderID, Amount", "Orders"));
// Act
_collection.Clear();
// Assert
Assert.That(_collection.Count, Is.EqualTo(0));
}
[Test]
public void GetSqlServerBatch_MultipleQueries_ContainsGoSeparators()
{
// Arrange
_collection.Add(new QueryBreakdown("ID, Name", "Users"));
_collection.Add(new QueryBreakdown("OrderID, Amount", "Orders"));
// Act
var sql = _collection.GetSqlServerBatch();
// Assert
Assert.That(sql, Does.Contain("GO"));
Assert.That(sql, Does.Contain("SELECT"));
}
[Test]
public void GetSqlServerBatch_WithTransaction_WrapsInTransaction()
{
// Arrange
_collection.Add(new QueryBreakdown("ID, Name", "Users"));
// Act
var sql = _collection.GetSqlServerBatch(includeTransaction: true);
// Assert
Assert.That(sql, Does.Contain("BEGIN TRANSACTION"));
Assert.That(sql, Does.Contain("COMMIT TRANSACTION"));
}
[Test]
public void WhereSelectContains_MatchingColumn_ReturnsQuery()
{
// Arrange
var query1 = new QueryBreakdown("ID, Name, Email", "Users");
var query2 = new QueryBreakdown("OrderID, Amount", "Orders");
_collection.Add(query1);
_collection.Add(query2);
// Act
var results = _collection.WhereSelectContains("Email").ToList();
// Assert
Assert.That(results.Count, Is.EqualTo(1));
Assert.That(results[0], Is.EqualTo(query1));
}
[Test]
public void WhereSelectContains_CaseInsensitive_ReturnsQuery()
{
// Arrange
var query = new QueryBreakdown("ID, Name, Email", "Users");
_collection.Add(query);
// Act
var results = _collection.WhereSelectContains("email").ToList();
// Assert
Assert.That(results.Count, Is.EqualTo(1));
}
[Test]
public void WhereTableContains_MatchingTable_ReturnsQuery()
{
// Arrange
var query1 = new QueryBreakdown("ID, Name", "Users");
var query2 = new QueryBreakdown("OrderID", "Orders");
_collection.Add(query1);
_collection.Add(query2);
// Act
var results = _collection.WhereTableContains("Users").ToList();
// Assert
Assert.That(results.Count, Is.EqualTo(1));
Assert.That(results[0], Is.EqualTo(query1));
}
[Test]
public void WhereHaveWhereClause_WithWhereClauses_ReturnsMatchingQueries()
{
// Arrange
var query1 = new QueryBreakdown("ID, Name", "Users", "ID > 10");
var query2 = new QueryBreakdown("OrderID", "Orders");
_collection.Add(query1);
_collection.Add(query2);
// Act
var results = _collection.WhereHaveWhereClause().ToList();
// Assert
Assert.That(results.Count, Is.EqualTo(1));
Assert.That(results[0], Is.EqualTo(query1));
}
[Test]
public void WhereHaveNoWhereClause_WithoutWhereClauses_ReturnsMatchingQueries()
{
// Arrange
var query1 = new QueryBreakdown("ID, Name", "Users", "ID > 10");
var query2 = new QueryBreakdown("OrderID", "Orders");
_collection.Add(query1);
_collection.Add(query2);
// Act
var results = _collection.WhereHaveNoWhereClause().ToList();
// Assert
Assert.That(results.Count, Is.EqualTo(1));
Assert.That(results[0], Is.EqualTo(query2));
}
[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_MultipleQueries_ReturnsSummaryForEach()
{
// Arrange
_collection.Add(new QueryBreakdown("ID, Name", "Users"));
_collection.Add(new QueryBreakdown("OrderID, Amount", "Orders", "Amount > 100"));
// Act
var summaries = _collection.GetQuerySummaries().ToList();
// Assert
Assert.That(summaries.Count, Is.EqualTo(2));
Assert.That(summaries[0].HasSelectClause, Is.True);
Assert.That(summaries[1].HasWhereClause, 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
// Both should have the same value (the last one)
Assert.That(query1.Parameters["UserId"], Is.EqualTo(query2.Parameters["UserId"]));
}
[Test]
public void SynchronizeParameters_OnlyUpdatesExistingParameters()
{
// Arrange
var query1 = new QueryBreakdown("ID, Name", "Users", "ID = @UserId");
var query2 = new QueryBreakdown("OrderID", "Orders");
query1.Parameters["UserId"] = 123;
_collection.Add(query1);
_collection.Add(query2);
// Act
_collection.SynchronizeParameters();
// Assert
// query2 should NOT have UserId parameter added
Assert.That(query2.Parameters.ContainsKey("UserId"), Is.False);
}
[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_FormatsWithDataTypes()
{
// 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("INT"));
}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,318 @@
using Strata.SqlTools.Breakdowns.SqlServer;
using Strata.SqlTools.SqlBreakdown.Classes;
namespace Strata.SqlTools.SqlBreakdown.Tests.SqlServer;
[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
var statusKey = merged.Keys.FirstOrDefault(k => k.Contains("Status") && !k.Contains("@") && !k.Contains("$"));
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,907 @@
using Strata.SqlTools.SqlBreakdown.Expressions;
using Strata.SqlTools.SqlBreakdown.Expressions.Functions.Aggregate;
using Strata.SqlTools.SqlBreakdown.Expressions.Literals;
using Strata.SqlTools.Statements.SqlServer;
namespace Strata.SqlTools.SqlBreakdown.Tests.SqlServer;
[TestFixture]
public class StatementExpressionParserTests
{
private StatementExpressionParser _parser = null!;
[SetUp]
public void Setup()
{
_parser = new StatementExpressionParser();
}
#region Parse Method Tests
[Test]
public void Parse_SimpleNumber_ReturnsNumberLiteralExpression()
{
// Arrange
var sql = "42";
// Act
var result = _parser.Parse(sql);
// Assert
Assert.That(result, Is.Not.Null);
Assert.That(result, Is.InstanceOf<NumberLiteralExpression>());
}
[Test]
public void Parse_SimpleAddition_ReturnsCorrectExpression()
{
// Arrange
var sql = "10 + 20";
// Act
var result = _parser.Parse(sql);
// Assert
Assert.That(result, Is.Not.Null);
Assert.That(result, Is.InstanceOf<Expression>());
}
[Test]
public void Parse_SimpleSubtraction_ReturnsCorrectExpression()
{
// Arrange
var sql = "50 - 30";
// Act
var result = _parser.Parse(sql);
// Assert
Assert.That(result, Is.Not.Null);
Assert.That(result, Is.InstanceOf<Expression>());
}
[Test]
public void Parse_SimpleMultiplication_ReturnsCorrectExpression()
{
// Arrange
var sql = "5 * 3";
// Act
var result = _parser.Parse(sql);
// Assert
Assert.That(result, Is.Not.Null);
Assert.That(result, Is.InstanceOf<Expression>());
}
[Test]
public void Parse_SimpleDivision_ReturnsCorrectExpression()
{
// Arrange
var sql = "100 / 4";
// Act
var result = _parser.Parse(sql);
// Assert
Assert.That(result, Is.Not.Null);
Assert.That(result, Is.InstanceOf<Expression>());
}
[Test]
public void Parse_ComplexExpression_ReturnsCorrectExpression()
{
// Arrange
var sql = "10 + 20 * 3 - 5";
// Act
var result = _parser.Parse(sql);
// Assert
Assert.That(result, Is.Not.Null);
Assert.That(result, Is.InstanceOf<Expression>());
}
[Test]
public void Parse_ParenthesizedExpression_ReturnsCorrectExpression()
{
// Arrange
var sql = "(10 + 20) * 3";
// Act
var result = _parser.Parse(sql);
// Assert
Assert.That(result, Is.Not.Null);
Assert.That(result, Is.InstanceOf<Expression>());
}
[Test]
public void Parse_NestedParentheses_ReturnsCorrectExpression()
{
// Arrange
var sql = "((10 + 5) * 2) / 3";
// Act
var result = _parser.Parse(sql);
// Assert
Assert.That(result, Is.Not.Null);
Assert.That(result, Is.InstanceOf<Expression>());
}
[Test]
public void Parse_SumFunction_ReturnsSumFunctionExpression()
{
// Arrange
var sql = "SUM(3_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(3_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(3_REVENUE) + 100";
// Act
var result = _parser.Parse(sql);
// Assert
Assert.That(result, Is.Not.Null);
Assert.That(result, Is.InstanceOf<Expression>());
}
[Test]
public void Parse_ColumnIdentifier_ReturnsColumnExpression()
{
// Arrange
var sql = "1_DEPARTMENT_ID";
// Act
var result = _parser.Parse(sql);
// Assert
Assert.That(result, Is.Not.Null);
Assert.That(result, Is.InstanceOf<RegisteredTableColumnExpression>());
}
[Test]
public void Parse_ColumnWithArithmetic_ReturnsCorrectExpression()
{
// Arrange
var sql = "3_REVENUE * 2";
// Act
var result = _parser.Parse(sql);
// Assert
Assert.That(result, Is.Not.Null);
Assert.That(result, Is.InstanceOf<Expression>());
}
[Test]
public void Parse_DecimalNumber_ReturnsNumberLiteralExpression()
{
// Arrange
var sql = "123.456";
// Act
var result = _parser.Parse(sql);
// Assert
Assert.That(result, Is.Not.Null);
Assert.That(result, Is.InstanceOf<NumberLiteralExpression>());
}
[Test]
public void Parse_WithSingleLineComment_ParsesSuccessfully()
{
// Arrange
var sql = "10 + 20 -- This is a comment";
// Act
var result = _parser.Parse(sql);
// Assert
Assert.That(result, Is.Not.Null);
Assert.That(result, Is.InstanceOf<Expression>());
}
[Test]
public void Parse_WithMultiLineComment_ParsesSuccessfully()
{
// Arrange
var sql = "10 /* this is a comment */ + 20";
// Act
var result = _parser.Parse(sql);
// Assert
Assert.That(result, Is.Not.Null);
Assert.That(result, Is.InstanceOf<Expression>());
}
[Test]
public void Parse_WithExtraWhitespace_ParsesSuccessfully()
{
// Arrange
var sql = " 10 + 20 ";
// Act
var result = _parser.Parse(sql);
// Assert
Assert.That(result, Is.Not.Null);
Assert.That(result, Is.InstanceOf<Expression>());
}
[Test]
public void Parse_WithNewlines_ParsesSuccessfully()
{
// Arrange
var sql = @"10
+
20";
// Act
var result = _parser.Parse(sql);
// Assert
Assert.That(result, Is.Not.Null);
Assert.That(result, Is.InstanceOf<Expression>());
}
[Test]
public void Parse_NullInput_ThrowsArgumentNullException()
{
// Arrange
string sql = null!;
// Act & Assert
var ex = Assert.Throws<ArgumentNullException>(() => _parser.Parse(sql));
Assert.That(ex.ParamName, Is.EqualTo("sqlStatement"));
Assert.That(ex.Message, Does.Contain("SQL statement cannot be null or empty"));
}
[Test]
public void Parse_EmptyString_ThrowsArgumentNullException()
{
// Arrange
var sql = string.Empty;
// Act & Assert
var ex = Assert.Throws<ArgumentNullException>(() => _parser.Parse(sql));
Assert.That(ex.ParamName, Is.EqualTo("sqlStatement"));
}
[Test]
public void Parse_WhitespaceOnly_ThrowsArgumentNullException()
{
// Arrange
var sql = " ";
// Act & Assert
var ex = Assert.Throws<ArgumentNullException>(() => _parser.Parse(sql));
Assert.That(ex.ParamName, Is.EqualTo("sqlStatement"));
}
[Test]
public void Parse_UnclosedParenthesis_ThrowsFormatException()
{
// Arrange
var sql = "(10 + 20";
// Act & Assert
var ex = Assert.Throws<FormatException>(() => _parser.Parse(sql));
Assert.That(ex.Message, Does.Contain("Failed to parse SQL statement"));
}
[Test]
public void Parse_MismatchedParenthesis_ThrowsFormatException()
{
// Arrange
var sql = "10 + 20)";
// Act & Assert
Assert.Throws<FormatException>(() => _parser.Parse(sql));
}
[Test]
public void Parse_UnrecognizedFunction_ThrowsFormatException()
{
// Arrange
var sql = "UNKNOWN(100)";
// Act & Assert
var ex = Assert.Throws<FormatException>(() => _parser.Parse(sql));
Assert.That(ex.Message, Does.Contain("Failed to parse SQL statement"));
}
#endregion
#region TryParse Method Tests
[Test]
public void TryParse_ValidSimpleExpression_ReturnsTrue()
{
// Arrange
var sql = "42";
// Act
var success = _parser.TryParse(sql, out var result);
// Assert
Assert.That(success, Is.True);
Assert.That(result, Is.Not.Null);
Assert.That(result, Is.InstanceOf<NumberLiteralExpression>());
}
[Test]
public void TryParse_ValidComplexExpression_ReturnsTrue()
{
// Arrange
var sql = "(10 + 20) * 3 - 5";
// Act
var success = _parser.TryParse(sql, out var result);
// Assert
Assert.That(success, Is.True);
Assert.That(result, Is.Not.Null);
}
[Test]
public void TryParse_ValidFunction_ReturnsTrue()
{
// Arrange
var sql = "SUM(3_REVENUE)";
// Act
var success = _parser.TryParse(sql, out var result);
// Assert
Assert.That(success, Is.True);
Assert.That(result, Is.Not.Null);
Assert.That(result, Is.InstanceOf<SumFunction>());
}
[Test]
public void TryParse_NullInput_ReturnsFalse()
{
// Arrange
string sql = null!;
// Act
var success = _parser.TryParse(sql, out var result);
// Assert
Assert.That(success, Is.False);
Assert.That(result, Is.Null);
}
[Test]
public void TryParse_EmptyString_ReturnsFalse()
{
// Arrange
var sql = string.Empty;
// Act
var success = _parser.TryParse(sql, out var result);
// Assert
Assert.That(success, Is.False);
Assert.That(result, Is.Null);
}
[Test]
public void TryParse_WhitespaceOnly_ReturnsFalse()
{
// Arrange
var sql = " ";
// Act
var success = _parser.TryParse(sql, out var result);
// Assert
Assert.That(success, Is.False);
Assert.That(result, Is.Null);
}
[Test]
public void TryParse_InvalidSyntax_ReturnsFalse()
{
// Arrange
var sql = "(10 + 20";
// Act
var success = _parser.TryParse(sql, out var result);
// Assert
Assert.That(success, Is.False);
Assert.That(result, Is.Null);
}
[Test]
public void TryParse_WithErrorMessage_ValidInput_ReturnsTrue()
{
// Arrange
var sql = "100 + 50";
// Act
var success = _parser.TryParse(sql, out var result, out var errorMessage);
// Assert
Assert.That(success, Is.True);
Assert.That(result, Is.Not.Null);
Assert.That(errorMessage, Is.Null);
}
[Test]
public void TryParse_WithErrorMessage_NullInput_ReturnsFalseWithMessage()
{
// Arrange
string sql = null!;
// Act
var success = _parser.TryParse(sql, out var result, out var errorMessage);
// Assert
Assert.That(success, Is.False);
Assert.That(result, Is.Null);
Assert.That(errorMessage, Is.Not.Null);
Assert.That(errorMessage, Does.Contain("SQL statement cannot be null or empty"));
}
[Test]
public void TryParse_WithErrorMessage_InvalidSyntax_ReturnsFalseWithMessage()
{
// Arrange
var sql = "(10 + 20";
// Act
var success = _parser.TryParse(sql, out var result, out var errorMessage);
// Assert
Assert.That(success, Is.False);
Assert.That(result, Is.Null);
Assert.That(errorMessage, Is.Not.Null);
Assert.That(errorMessage, Does.Contain("Invalid syntax"));
}
[Test]
public void TryParse_WithErrorMessage_UnsupportedToken_ReturnsFalseWithMessage()
{
// Arrange
var sql = "UNKNOWN(100)";
// Act
var success = _parser.TryParse(sql, out var result, out var errorMessage);
// Assert
Assert.That(success, Is.False);
Assert.That(result, Is.Null);
Assert.That(errorMessage, Is.Not.Null);
Assert.That(errorMessage, Does.Contain("not recognized"));
}
#endregion
#region Order of Operations Tests
[Test]
public void Parse_MultiplicationBeforeAddition_CorrectPrecedence()
{
// Arrange
var sql = "2 + 3 * 4"; // Should be 2 + (3 * 4) = 14, not (2 + 3) * 4 = 20
// Act
var result = _parser.Parse(sql);
// Assert
Assert.That(result, Is.Not.Null);
// Note: We can't easily verify the calculation result without evaluating the expression tree
// But we can verify it parses without error
}
[Test]
public void Parse_DivisionBeforeSubtraction_CorrectPrecedence()
{
// Arrange
var sql = "20 - 10 / 2"; // Should be 20 - (10 / 2) = 15, not (20 - 10) / 2 = 5
// Act
var result = _parser.Parse(sql);
// Assert
Assert.That(result, Is.Not.Null);
}
[Test]
public void Parse_ParenthesesOverridePrecedence_CorrectPrecedence()
{
// Arrange
var sql = "(2 + 3) * 4"; // Should be (2 + 3) * 4 = 20
// Act
var result = _parser.Parse(sql);
// Assert
Assert.That(result, Is.Not.Null);
}
[Test]
public void Parse_MultipleOperationsLeftToRight_CorrectAssociativity()
{
// Arrange
var sql = "10 - 5 - 2"; // Should be (10 - 5) - 2 = 3, not 10 - (5 - 2) = 7
// Act
var result = _parser.Parse(sql);
// Assert
Assert.That(result, Is.Not.Null);
}
#endregion
#region Comment and Whitespace Normalization Tests
[Test]
public void Parse_SingleLineCommentAtEnd_IgnoresComment()
{
// Arrange
var sql = "10 + 20 -- This should be ignored";
// Act
var result = _parser.Parse(sql);
// Assert
Assert.That(result, Is.Not.Null);
}
[Test]
public void Parse_SingleLineCommentInMiddle_IgnoresComment()
{
// Arrange
var sql = @"10 + -- comment here
20";
// Act
var result = _parser.Parse(sql);
// Assert
Assert.That(result, Is.Not.Null);
}
[Test]
public void Parse_MultiLineCommentInMiddle_IgnoresComment()
{
// Arrange
var sql = "10 /* this is a\n multi-line\n comment */ + 20";
// Act
var result = _parser.Parse(sql);
// Assert
Assert.That(result, Is.Not.Null);
}
[Test]
public void Parse_MultipleComments_IgnoresAllComments()
{
// Arrange
var sql = "10 /* comment 1 */ + /* comment 2 */ 20 -- comment 3";
// Act
var result = _parser.Parse(sql);
// Assert
Assert.That(result, Is.Not.Null);
}
[Test]
public void Parse_CommentLikeTextInString_PreservesString()
{
// Arrange - Although this parser might not handle strings, testing the comment removal logic
var sql = "10 + 20"; // Simple case without strings as parser may not support them
// Act
var result = _parser.Parse(sql);
// Assert
Assert.That(result, Is.Not.Null);
}
[Test]
public void Parse_TabsAndSpaces_NormalizesCorrectly()
{
// Arrange
var sql = "10\t+\t 20";
// Act
var result = _parser.Parse(sql);
// Assert
Assert.That(result, Is.Not.Null);
}
[Test]
public void Parse_MultipleNewlines_NormalizesCorrectly()
{
// Arrange
var sql = "10\n\n\n+\n\n20";
// Act
var result = _parser.Parse(sql);
// Assert
Assert.That(result, Is.Not.Null);
}
#endregion
#region Edge Cases and Complex Scenarios
[Test]
public void Parse_VeryLongExpression_ParsesSuccessfully()
{
// Arrange
var sql = "1 + 2 + 3 + 4 + 5 + 6 + 7 + 8 + 9 + 10";
// Act
var result = _parser.Parse(sql);
// Assert
Assert.That(result, Is.Not.Null);
}
[Test]
public void Parse_DeeplyNestedParentheses_ParsesSuccessfully()
{
// Arrange
var sql = "((((10 + 5) * 2) - 3) / 4)";
// Act
var result = _parser.Parse(sql);
// Assert
Assert.That(result, Is.Not.Null);
}
[Test]
public void Parse_MixedColumnsNumbersAndFunctions_ParsesSuccessfully()
{
// Arrange
var sql = "SUM(3_REVENUE) + 1_DEPARTMENT_ID * 100";
// Act
var result = _parser.Parse(sql);
// Assert
Assert.That(result, Is.Not.Null);
}
[Test]
public void Parse_FunctionInsideParentheses_ParsesSuccessfully()
{
// Arrange
var sql = "(SUM(3_REVENUE) + 100) * 2";
// Act
var result = _parser.Parse(sql);
// Assert
Assert.That(result, Is.Not.Null);
}
[Test]
public void Parse_LargeDecimalNumber_ParsesSuccessfully()
{
// Arrange
var sql = "123456789.987654321";
// Act
var result = _parser.Parse(sql);
// Assert
Assert.That(result, Is.Not.Null);
Assert.That(result, Is.InstanceOf<NumberLiteralExpression>());
}
[Test]
public void Parse_ZeroValue_ParsesSuccessfully()
{
// Arrange
var sql = "0";
// Act
var result = _parser.Parse(sql);
// Assert
Assert.That(result, Is.Not.Null);
Assert.That(result, Is.InstanceOf<NumberLiteralExpression>());
}
[Test]
public void Parse_NegativeNumber_WithSubtraction_ParsesSuccessfully()
{
// Arrange
var sql = "0 - 5";
// Act
var result = _parser.Parse(sql);
// Assert
Assert.That(result, Is.Not.Null);
}
[Test]
public void Parse_ConsecutiveOperators_ThrowsFormatException()
{
// Arrange
var sql = "10 + + 20";
// Act & Assert
Assert.Throws<FormatException>(() => _parser.Parse(sql));
}
[Test]
public void Parse_TrailingOperator_ThrowsFormatException()
{
// Arrange
var sql = "10 + 20 +";
// Act & Assert
Assert.Throws<FormatException>(() => _parser.Parse(sql));
}
[Test]
public void Parse_LeadingOperator_ThrowsFormatException()
{
// Arrange
var sql = "+ 10 + 20";
// Act & Assert
Assert.Throws<FormatException>(() => _parser.Parse(sql));
}
#endregion
#region Function-Specific Tests
[Test]
public void Parse_SumFunctionLowercase_ParsesSuccessfully()
{
// Arrange
var sql = "sum(3_REVENUE)";
// Act
var result = _parser.Parse(sql);
// Assert
Assert.That(result, Is.Not.Null);
Assert.That(result, Is.InstanceOf<SumFunction>());
}
[Test]
public void Parse_AvgFunctionMixedCase_ParsesSuccessfully()
{
// Arrange
var sql = "AvG(3_REVENUE)";
// Act
var result = _parser.Parse(sql);
// Assert
Assert.That(result, Is.Not.Null);
Assert.That(result, Is.InstanceOf<AverageFunction>());
}
[Test]
public void Parse_MultipleFunctions_ParsesSuccessfully()
{
// Arrange
var sql = "SUM(3_REVENUE) + AVG(3_REVENUE)";
// Act
var result = _parser.Parse(sql);
// Assert
Assert.That(result, Is.Not.Null);
}
[Test]
public void Parse_FunctionWithNumberArgument_ParsesSuccessfully()
{
// Arrange
var sql = "SUM(100)";
// Act
var result = _parser.Parse(sql);
// Assert
Assert.That(result, Is.Not.Null);
Assert.That(result, Is.InstanceOf<SumFunction>());
}
[Test]
public void Parse_FunctionWithComplexExpression_ParsesSuccessfully()
{
// Arrange
var sql = "SUM(3_REVENUE * 2)";
// Act
var result = _parser.Parse(sql);
// Assert
Assert.That(result, Is.Not.Null);
Assert.That(result, Is.InstanceOf<SumFunction>());
}
#endregion
#region Column Expression Tests
[Test]
public void Parse_VariousColumnIds_ParsesSuccessfully()
{
// Arrange & Act & Assert
var testCases = new[] { "1_DEPARTMENT_ID", "2_NAME", "3_REVENUE", "4_DISCHARGE_DATE", "586883_FIXED_COST", "586664_VARIABLE_COST" };
foreach (var sql in testCases)
{
var result = _parser.Parse(sql);
Assert.That(result, Is.Not.Null);
Assert.That(result, Is.InstanceOf<RegisteredTableColumnExpression>());
}
}
[Test]
public void Parse_ColumnInArithmeticExpression_ParsesSuccessfully()
{
// Arrange
var sql = "(3_REVENUE - 586883_FIXED_COST) / 586664_VARIABLE_COST";
// Act
var result = _parser.Parse(sql);
// Assert
Assert.That(result, Is.Not.Null);
}
[Test]
public void Parse_ColumnWithFunction_ParsesSuccessfully()
{
// Arrange
var sql = "SUM(3_REVENUE) - AVG(586883_FIXED_COST)";
// Act
var result = _parser.Parse(sql);
// Assert
Assert.That(result, Is.Not.Null);
}
#endregion
}
@@ -0,0 +1,29 @@
using Strata.SqlTools.SqlBreakdown.Enums.SQL;
using Strata.SqlTools.Statements.SqlServer;
namespace Strata.SqlTools.SqlBreakdown.Tests.SqlServer;
[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"));
}
}
@@ -0,0 +1,531 @@
using Strata.SqlTools.Breakdowns.SqlServer;
using Strata.SqlTools.SqlBreakdown.Classes;
using Strata.SqlTools.SqlBreakdown.Expressions;
namespace Strata.SqlTools.SqlBreakdown.Tests.SqlServer;
[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 >= @StartDate");
innerQuery.AddParameter("@StartDate", 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 >= @StartDate"));
// 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.SqlServer\Strata.SqlTools.SqlServer.csproj" />
</ItemGroup>
</Project>