Driven by `dotnet format analyzers --diagnostics NUnit2045`. The fixer
groups consecutive independent `Assert.That(...)` calls into
`Assert.Multiple(() => { ... })`, so a failing assertion no longer
short-circuits the block — every failure inside the group is reported,
which gives much better diagnostics on multi-property tests.
Audit confirmed no Assert.Throws / Assert.Fail / Assert.Catch / Assert.Pass
/ Assert.DoesNotThrow got pulled inside a Multiple block (those need to
short-circuit). All 1180 tests pass.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
378 lines
12 KiB
C#
378 lines
12 KiB
C#
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.Multiple(() =>
|
|
{
|
|
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.Multiple(() =>
|
|
{
|
|
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
|
|
}
|