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,277 @@
using Strata.SqlTools.Breakdowns.PostgreSql;
using Strata.SqlTools.SqlBreakdown.Classes;
namespace Strata.SqlTools.PostgreSql.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"));
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 (PostgreSQL-style positional parameters)
[Test]
public void ColumnList_PreservesPostgresqlParameters()
{
// Arrange
var cte = new QueryBreakdown("id, name, salary", "employees", "salary > $1");
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_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)"));
}
[Test]
public void ColumnList_AllowsQuotedIdentifiers()
{
// 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("quoted_cte", cte)
{
ColumnList = columns
};
mainQuery.AddWithClause(withClause);
// Act
var sql = mainQuery.GetSql();
// Assert
Assert.That(sql, Contains.Substring("quoted_cte (\"Column 1\", \"column-2\")"));
}
#endregion
#region Real-World Scenarios
[Test]
public void ColumnList_PostgresqlAnalyticsScenario()
{
// Scenario: PostgreSQL analytics with window functions and renamed columns
// Arrange
var analyticsData = new QueryBreakdown(
"transaction_date, product_id, customer_id, amount, ROW_NUMBER() OVER (PARTITION BY customer_id ORDER BY transaction_date) as rank",
"transactions",
"transaction_date >= $1 AND amount >= $2"
);
analyticsData.AddParameter("StartDate", DateTime.Today.AddMonths(-6));
analyticsData.AddParameter("MinAmount", 100);
var mainQuery = new QueryBreakdown("*", "ranked_transactions");
var withClause = new WithClause("customer_transactions", analyticsData)
{
ColumnList = new List<string>
{
"trans_date",
"prod_id",
"cust_id",
"trans_amount",
"transaction_rank"
}
};
mainQuery.AddWithClause(withClause);
// Act
var sql = mainQuery.GetSql();
var params_all = mainQuery.GetMergedParameters();
// Assert
Assert.That(sql, Contains.Substring("customer_transactions (trans_date, prod_id, cust_id, trans_amount, transaction_rank)"));
var paramKeys = params_all.Keys.ToList();
Assert.That(paramKeys.Any(k => k.Contains("StartDate")));
Assert.That(paramKeys.Any(k => k.Contains("MinAmount")));
}
#endregion
}
@@ -0,0 +1,343 @@
using Strata.SqlTools.Breakdowns.PostgreSql;
namespace Strata.SqlTools.SqlBreakdown.Tests.PostgreSql;
[TestFixture]
public class QueryBreakdownCollectionTests
{
private QueryBreakdownCollection _collection = null!;
[SetUp]
public void Setup()
{
_collection = new QueryBreakdownCollection();
}
#region Constructor Tests
[Test]
public void Constructor_Default_CreatesEmptyCollection()
{
// Arrange & Act
var collection = new QueryBreakdownCollection();
// Assert
Assert.That(collection.Count, Is.EqualTo(0));
}
#endregion
#region Add Tests
[Test]
public void Add_SingleQueryBreakdown_AddsSuccessfully()
{
// Arrange
var query = new QueryBreakdown("id, name", "users");
// Act
_collection.Add(query);
// Assert
Assert.That(_collection.Count, Is.GreaterThan(0));
}
[Test]
public void Add_MultipleQueryBreakdowns_AddsMultiple()
{
// Arrange
var query1 = new QueryBreakdown("id, name", "users");
var query2 = new QueryBreakdown("order_id, amount", "orders");
var query3 = new QueryBreakdown("product_id, price", "products");
// Act
_collection.Add(query1);
_collection.Add(query2);
_collection.Add(query3);
// Assert
Assert.That(_collection.Count, Is.EqualTo(3));
}
#endregion
#region GetPostgreSqlBatch Tests
[Test]
public void GetPostgreSqlBatch_EmptyCollection_ReturnsEmptyString()
{
// Arrange - empty collection created in SetUp
// Act
var sql = _collection.GetPostgreSqlBatch();
// Assert
Assert.That(sql, Is.Not.Null);
}
[Test]
public void GetPostgreSqlBatch_SingleQuery_GeneratesValidSql()
{
// Arrange
var query = new QueryBreakdown("id, name", "users");
_collection.Add(query);
// Act
var sql = _collection.GetPostgreSqlBatch();
// Assert
Assert.That(sql, Is.Not.Null);
Assert.That(sql, Does.Contain("SELECT").Or.Contain("select"));
}
[Test]
public void GetPostgreSqlBatch_MultipleQueries_SeparatesWithSemicolon()
{
// Arrange
var query1 = new QueryBreakdown("id", "users");
var query2 = new QueryBreakdown("id", "orders");
_collection.Add(query1);
_collection.Add(query2);
// Act
var sql = _collection.GetPostgreSqlBatch();
// Assert
Assert.That(sql, Is.Not.Null);
// PostgreSQL batch should contain semicolons separating statements
Assert.That(sql, Does.Contain(";"));
}
[Test]
public void GetPostgreSqlBatch_WithWhereClause_IncludesConditions()
{
// Arrange
var query = new QueryBreakdown("id, name", "users");
query.WhereClause.Clause = "active = true";
_collection.Add(query);
// Act
var sql = _collection.GetPostgreSqlBatch();
// Assert
Assert.That(sql, Does.Contain("active"));
}
[Test]
public void GetPostgreSqlBatch_WithOrderBy_IncludesOrderBy()
{
// Arrange
var query = new QueryBreakdown("id, name", "users");
query.OrderByClause.Clause = "name ASC";
_collection.Add(query);
// Act
var sql = _collection.GetPostgreSqlBatch();
// Assert
Assert.That(sql, Does.Contain("ORDER BY").Or.Contain("order by"));
}
[Test]
public void GetPostgreSqlBatch_WithGroupBy_IncludesGroupBy()
{
// Arrange
var query = new QueryBreakdown("category, COUNT(*) as count", "products");
query.GroupByClause.Clause = "category";
_collection.Add(query);
// Act
var sql = _collection.GetPostgreSqlBatch();
// Assert
Assert.That(sql, Does.Contain("GROUP BY").Or.Contain("group by"));
}
[Test]
public void GetPostgreSqlBatch_WithHaving_IncludesHaving()
{
// Arrange
var query = new QueryBreakdown("category, COUNT(*) as count", "products");
query.GroupByClause.Clause = "category";
query.HavingClause.Clause = "COUNT(*) > 5";
_collection.Add(query);
// Act
var sql = _collection.GetPostgreSqlBatch();
// Assert
Assert.That(sql, Does.Contain("HAVING").Or.Contain("having"));
}
#endregion
#region ParseBatch Tests
[Test]
public void ParseBatch_WithValidSql_Succeeds()
{
// Arrange
var batchSql = "SELECT id, name FROM users WHERE active = true; SELECT order_id FROM orders;";
// Act
var result = _collection.ParseBatch(batchSql);
// Assert
Assert.That(result, Is.True);
Assert.That(_collection.Count, Is.GreaterThan(0));
}
[Test]
public void ParseBatch_WithMultipleStatements_ParsesAllQueries()
{
// Arrange
var batchSql = @"
SELECT id, name FROM users WHERE active = true;
SELECT order_id, amount FROM orders WHERE user_id = $1;
";
// Act
var result = _collection.ParseBatch(batchSql);
// Assert
Assert.That(result, Is.True);
Assert.That(_collection.Count, Is.GreaterThan(0));
}
[Test]
public void ParseBatch_WithSingleStatement_ParsesSingleQuery()
{
// Arrange
var batchSql = "SELECT id, name FROM users";
// Act
var result = _collection.ParseBatch(batchSql);
// Assert
Assert.That(result, Is.True);
}
#endregion
#region Complex Query Tests
[Test]
public void GetPostgreSqlBatch_ComplexQueryWithMultipleClauses_GeneratesValidBatch()
{
// Arrange
var query1 = new QueryBreakdown("id, name", "users");
query1.WhereClause.Clause = "id = $1";
var query2 = new QueryBreakdown("order_id, amount", "orders");
query2.WhereClause.Clause = "user_id = $1";
query2.OrderByClause.Clause = "created_at DESC";
_collection.Add(query1);
_collection.Add(query2);
// Act
var sql = _collection.GetPostgreSqlBatch();
// Assert
Assert.That(sql, Does.Contain(";"));
Assert.That(sql, Does.Contain("SELECT"));
Assert.That(sql, Does.Contain("WHERE"));
}
[Test]
public void GetPostgreSqlBatch_WithParameters_IncludesParameterValues()
{
// Arrange
var query = new QueryBreakdown("id, name", "users");
query.AddParameter("userId", 123);
_collection.Add(query);
// Act
var sql = _collection.GetPostgreSqlBatch();
// Assert
Assert.That(sql, Does.Contain("SELECT"));
Assert.That(sql, Is.Not.Null);
}
[Test]
public void GetPostgreSqlBatch_MultipleQueriesWithDifferentClauses_GeneratesValidBatch()
{
// Arrange
var query1 = new QueryBreakdown("id, name", "users");
query1.WhereClause.Clause = "active = true";
var query2 = new QueryBreakdown("order_id, total", "orders");
query2.WhereClause.Clause = "status = 'pending'";
query2.OrderByClause.Clause = "created_at DESC";
var query3 = new QueryBreakdown("product_id, price", "products");
query3.WhereClause.Clause = "in_stock = true";
query3.OrderByClause.Clause = "price ASC";
_collection.Add(query1);
_collection.Add(query2);
_collection.Add(query3);
// Act
var sql = _collection.GetPostgreSqlBatch();
// Assert
Assert.That(sql, Does.Contain("SELECT"));
Assert.That(sql, Does.Contain(";"));
Assert.That(_collection.Count, Is.EqualTo(3));
}
#endregion
#region Edge Cases
[Test]
public void GetPostgreSqlBatch_MultipleQueriesWithJoins_GeneratesValidBatch()
{
// Arrange
var query = new QueryBreakdown("u.id, u.name, o.order_id", "users u LEFT JOIN orders o ON u.id = o.user_id");
query.WhereClause.Clause = "u.active = true";
_collection.Add(query);
// Act
var sql = _collection.GetPostgreSqlBatch();
// Assert
Assert.That(sql, Does.Contain("JOIN"));
}
[Test]
public void GetPostgreSqlBatch_WithDoubleQuotedIdentifiers_PreservesQuotes()
{
// Arrange
var query = new QueryBreakdown("\"Id\", \"Name\"", "\"Users\"");
_collection.Add(query);
// Act
var sql = _collection.GetPostgreSqlBatch();
// Assert
Assert.That(sql, Does.Contain("\""));
}
[Test]
public void ParseBatch_WithComments_IgnoresComments()
{
// Arrange
var batchSql = @"
-- This is a user query
SELECT id, name FROM users;
-- This is an orders query
SELECT order_id FROM orders;
";
// Act
var result = _collection.ParseBatch(batchSql);
// Assert
Assert.That(result, Is.True);
}
#endregion
}
@@ -0,0 +1,577 @@
using Strata.SqlTools.Breakdowns.PostgreSql;
namespace Strata.SqlTools.SqlBreakdown.Tests.PostgreSql;
[TestFixture]
public class QueryBreakdownTests
{
#region Constructor Tests
[Test]
public void Constructor_Default_InitializesEmptyProperties()
{
// Arrange & Act
var queryBreakdown = new QueryBreakdown();
// Assert
Assert.That(queryBreakdown.SelectClause.Clause, Is.EqualTo(string.Empty));
Assert.That(queryBreakdown.FromClause.Clause, Is.EqualTo(string.Empty));
Assert.That(queryBreakdown.WhereClause.Clause, Is.EqualTo(string.Empty));
Assert.That(queryBreakdown.OrderByClause.Clause, Is.EqualTo(string.Empty));
}
[Test]
public void Constructor_WithSelectAndFrom_SetsProperties()
{
// Arrange
var selectClause = "id, name";
var fromClause = "users";
// Act
var queryBreakdown = new QueryBreakdown(selectClause, fromClause);
// Assert
Assert.That(queryBreakdown.SelectClause.Clause, Is.EqualTo(selectClause));
Assert.That(queryBreakdown.FromClause.Clause, Is.EqualTo(fromClause));
}
[Test]
public void Constructor_WithSelectFromAndWhere_SetsProperties()
{
// Arrange
var selectClause = "id, name";
var fromClause = "users";
var whereClause = "active = true";
// Act
var queryBreakdown = new QueryBreakdown(selectClause, fromClause, whereClause);
// Assert
Assert.That(queryBreakdown.SelectClause.Clause, Is.EqualTo(selectClause));
Assert.That(queryBreakdown.FromClause.Clause, Is.EqualTo(fromClause));
Assert.That(queryBreakdown.WhereClause.Clause, Is.EqualTo(whereClause));
}
[Test]
public void Constructor_WithAllBasicClauses_SetsProperties()
{
// Arrange
var selectClause = "id, name";
var fromClause = "users";
var whereClause = "active = true";
var orderByClause = "id ASC";
// Act
var queryBreakdown = new QueryBreakdown(selectClause, fromClause, whereClause, orderByClause);
// Assert
Assert.That(queryBreakdown.SelectClause.Clause, Is.EqualTo(selectClause));
Assert.That(queryBreakdown.FromClause.Clause, Is.EqualTo(fromClause));
Assert.That(queryBreakdown.WhereClause.Clause, Is.EqualTo(whereClause));
Assert.That(queryBreakdown.OrderByClause.Clause, Is.EqualTo(orderByClause));
}
#endregion
#region GetSql Tests
[Test]
public void GetSql_WithBasicQuery_GeneratesValidSql()
{
// Arrange
var queryBreakdown = new QueryBreakdown("id, name", "users");
// Act
var sql = queryBreakdown.GetSql();
// Assert
Assert.That(sql, Is.Not.Null);
Assert.That(sql, Does.Contain("SELECT").Or.Contain("select"));
Assert.That(sql, Does.Contain("users"));
}
[Test]
public void GetSql_WithWhereClause_GeneratesValidSql()
{
// Arrange
var queryBreakdown = new QueryBreakdown("id, name", "users", "active = true");
// Act
var sql = queryBreakdown.GetSql();
// Assert
Assert.That(sql, Does.Contain("active"));
Assert.That(sql, Does.Contain("true"));
}
[Test]
public void GetSql_WithOrderByClause_GeneratesValidSql()
{
// Arrange
var queryBreakdown = new QueryBreakdown("id, name", "users", "active = true", "name ASC");
// Act
var sql = queryBreakdown.GetSql();
// Assert
Assert.That(sql, Does.Contain("ORDER BY").Or.Contain("order by"));
}
#endregion
#region Parameters Tests
[Test]
public void AddParameter_SingleParameter_StoresParameter()
{
// Arrange
var queryBreakdown = new QueryBreakdown("id, name", "users");
// Act
queryBreakdown.AddParameter("userId", 123);
// Assert
Assert.That(queryBreakdown.Parameters.ContainsKey("userId"), Is.True);
Assert.That(queryBreakdown.Parameters["userId"], Is.EqualTo(123));
}
[Test]
public void AddParameter_MultipleParameters_StoresAll()
{
// Arrange
var queryBreakdown = new QueryBreakdown("id, name", "users");
// Act
queryBreakdown.AddParameter("param1", "value1");
queryBreakdown.AddParameter("param2", 123);
queryBreakdown.AddParameter("param3", new DateTime(2025, 1, 1));
// Assert
Assert.That(queryBreakdown.Parameters.Count, Is.GreaterThanOrEqualTo(3));
Assert.That(queryBreakdown.Parameters.ContainsKey("param1"), Is.True);
Assert.That(queryBreakdown.Parameters.ContainsKey("param2"), Is.True);
Assert.That(queryBreakdown.Parameters.ContainsKey("param3"), Is.True);
}
[Test]
public void AddParameter_ParameterWithAtPrefix_NormalizesName()
{
// Arrange
var queryBreakdown = new QueryBreakdown("id", "users");
// Act
queryBreakdown.AddParameter("@userId", 999);
// Assert
// The @ prefix is stripped
Assert.That(queryBreakdown.Parameters.Count, Is.GreaterThan(0));
}
#endregion
#region Parse Tests
[Test]
public void Parse_WithValidSelectStement_Succeeds()
{
// Arrange
var sql = "SELECT id, name FROM users";
// Act
var queryBreakdown = QueryBreakdown.Parse(sql);
// Assert
Assert.That(queryBreakdown, Is.Not.Null);
Assert.That(queryBreakdown.SelectClause.Clause, Is.Not.Empty);
Assert.That(queryBreakdown.FromClause.Clause, Is.Not.Empty);
}
[Test]
public void Parse_WithWhereClause_ExtractsCorrectly()
{
// Arrange
var sql = "SELECT id, name FROM users WHERE active = true";
// Act
var queryBreakdown = QueryBreakdown.Parse(sql);
// Assert
Assert.That(queryBreakdown.WhereClause.Clause, Is.Not.Empty);
Assert.That(queryBreakdown.FromClause.Clause, Does.Contain("users"));
}
[Test]
public void Parse_WithOrderBy_ExtractsCorrectly()
{
// Arrange
var sql = "SELECT id, name FROM users ORDER BY name ASC";
// Act
var queryBreakdown = QueryBreakdown.Parse(sql);
// Assert
Assert.That(queryBreakdown.OrderByClause.Clause, Is.Not.Empty);
Assert.That(queryBreakdown.OrderByClause.Clause, Does.Contain("name"));
}
[Test]
public void Parse_WithPostgreSqlPositionalParameters_ExtractsParameters()
{
// Arrange
var sql = "SELECT id, name FROM users WHERE user_id = $1 AND status = $2";
// Act
var queryBreakdown = QueryBreakdown.Parse(sql);
// Assert
Assert.That(queryBreakdown.Parameters.Count, Is.GreaterThanOrEqualTo(0));
}
[Test]
public void Parse_WithComplexQuery_ExtractsAllClauses()
{
// Arrange
var sql = @"
SELECT u.id, u.name, COUNT(o.id) AS order_count
FROM users u
LEFT JOIN orders o ON u.id = o.user_id
WHERE u.active = true
GROUP BY u.id, u.name
HAVING COUNT(o.id) > 0
ORDER BY order_count DESC
";
// Act
var queryBreakdown = QueryBreakdown.Parse(sql);
// Assert
Assert.That(queryBreakdown.SelectClause.Clause, Is.Not.Empty);
Assert.That(queryBreakdown.FromClause.Clause, Is.Not.Empty);
Assert.That(queryBreakdown.WhereClause.Clause, Is.Not.Empty);
Assert.That(queryBreakdown.GroupByClause.Clause, Is.Not.Empty);
Assert.That(queryBreakdown.HavingClause.Clause, Is.Not.Empty);
Assert.That(queryBreakdown.OrderByClause.Clause, Is.Not.Empty);
}
[Test]
public void TryParse_WithValidSql_Succeeds()
{
// Arrange
var sql = "SELECT id, name FROM users";
// Act
var success = QueryBreakdown.TryParse(sql, out var queryBreakdown, out var errorMessage);
// Assert
Assert.That(success, Is.True);
Assert.That(queryBreakdown, Is.Not.Null);
Assert.That(errorMessage, Is.Empty);
}
[Test]
public void TryParse_WithEmptySql_ReturnsFalse()
{
// Arrange
var sql = "";
// Act
var success = QueryBreakdown.TryParse(sql, out var queryBreakdown, out var errorMessage);
// Assert
Assert.That(success, Is.False);
Assert.That(errorMessage, Is.Not.Empty);
}
#endregion
#region PostgreSQL-Specific Features Tests
[Test]
public void GetSql_WithDoubleQuotedIdentifiers_PreservesQuotes()
{
// Arrange
var queryBreakdown = new QueryBreakdown("\"Id\", \"Name\"", "\"Users\"");
// Act
var sql = queryBreakdown.GetSql();
// Assert
Assert.That(sql, Does.Contain("\""));
}
[Test]
public void Parse_WithDoubleQuotedIdentifiers_Succeeds()
{
// Arrange
var sql = "SELECT \"Id\", \"Name\" FROM \"Users\" WHERE \"Id\" = $1";
// Act
var queryBreakdown = QueryBreakdown.Parse(sql);
// Assert
Assert.That(queryBreakdown.SelectClause.Clause, Is.Not.Empty);
Assert.That(queryBreakdown.FromClause.Clause, Is.Not.Empty);
}
[Test]
public void Parse_WithLimitInOrderBy_ExtractsLimit()
{
// Arrange
var sql = "SELECT id, name FROM users ORDER BY id LIMIT 10";
// Act
var queryBreakdown = QueryBreakdown.Parse(sql);
// Assert
// LIMIT is appended to ORDER BY clause in PostgreSQL parsing
Assert.That(queryBreakdown.OrderByClause.Clause, Does.Contain("LIMIT").Or.Contain("Limit").Or.Contain("limit"));
}
[Test]
public void Parse_WithLimitAndOffset_ExtractsCorrectly()
{
// Arrange
var sql = "SELECT id, name FROM users ORDER BY id LIMIT 10 OFFSET 5";
// Act
var queryBreakdown = QueryBreakdown.Parse(sql);
// Assert
// Both LIMIT and OFFSET are appended to ORDER BY
Assert.That(queryBreakdown.OrderByClause.Clause, Is.Not.Empty);
}
#endregion
#region Edge Cases
[Test]
public void Constructor_DefaultConstructor_AllowsManualClauseAssignment()
{
// Arrange & Act
var queryBreakdown = new QueryBreakdown();
queryBreakdown.SelectClause.Clause = "id, name";
queryBreakdown.FromClause.Clause = "users";
// Assert
Assert.That(queryBreakdown.SelectClause.Clause, Is.EqualTo("id, name"));
Assert.That(queryBreakdown.FromClause.Clause, Is.EqualTo("users"));
}
[Test]
public void GetSql_EmptyQueryBreakdown_GeneratesBasicSelect()
{
// Arrange
var queryBreakdown = new QueryBreakdown();
// Act
var sql = queryBreakdown.GetSql();
// Assert
Assert.That(sql, Is.Not.Null);
}
#endregion
#region WITH Clause Tests
[Test]
public void AddWithClause_WithCTE_AddsToWithClauses()
{
// Arrange
var cte = new QueryBreakdown("id, name", "base_users");
var mainQuery = new QueryBreakdown("*", "active_users_cte");
// Act
mainQuery.AddWithClause("active_users_cte", cte);
// Assert
Assert.That(mainQuery.WithClauses, Has.Count.EqualTo(1));
Assert.That(mainQuery.WithClauses[0].TableName, Is.EqualTo("active_users_cte"));
Assert.That(mainQuery.WithClauses[0].Query, Is.EqualTo(cte));
}
[Test]
public void GetSql_WithSimpleCTE_GeneratesWithClause()
{
// Arrange
var cte = new QueryBreakdown("id, name", "base_users");
var mainQuery = new QueryBreakdown("*", "active_users_cte");
mainQuery.AddWithClause("active_users_cte", cte);
// Act
var sql = mainQuery.GetSql();
// Assert
Assert.That(sql, Contains.Substring("WITH"));
Assert.That(sql, Contains.Substring("active_users_cte AS"));
Assert.That(sql, Contains.Substring("id, name"));
Assert.That(sql, Contains.Substring("base_users"));
}
[Test]
public void GetSql_WithMultipleCTEs_GeneratesAllCTEs()
{
// Arrange
var cte1 = new QueryBreakdown("id, status", "users", "status = 'active'");
var cte2 = new QueryBreakdown("user_id, count(*)", "orders", "created >= CURRENT_DATE");
var mainQuery = new QueryBreakdown("*", "results");
mainQuery.AddWithClause("active_users", cte1);
mainQuery.AddWithClause("recent_orders", cte2);
// Act
var sql = mainQuery.GetSql();
// Assert
Assert.That(sql, Contains.Substring("WITH"));
Assert.That(sql, Contains.Substring("active_users AS"));
Assert.That(sql, Contains.Substring("recent_orders AS"));
}
#endregion
#region CTE Parameter Inheritance Tests - PostgreSQL
[Test]
public void GetMergedParameters_WithNoCTEs_ReturnsMainQueryParameters()
{
// Arrange
var query = new QueryBreakdown("*", "USERS");
query.AddParameter("UserId", 123);
query.AddParameter("ActiveStatus", true);
// Act
var merged = query.GetMergedParameters();
// Assert
// PostgreSQL stores parameters with positional and name-based formats
Assert.That(merged.Count, Is.GreaterThanOrEqualTo(2));
var keys = merged.Keys.ToList();
Assert.That(keys.Any(k => k.Contains("UserId")));
Assert.That(keys.Any(k => k.Contains("ActiveStatus")));
}
[Test]
public void GetMergedParameters_WithSingleCTE_MergesParameters()
{
// Arrange
var cte = new QueryBreakdown("ID, NAME", "BASE_USERS", "ACTIVE = true");
cte.AddParameter("MinDate", DateTime.Today.AddDays(-30));
var mainQuery = new QueryBreakdown("*", "ACTIVE_USERS");
mainQuery.AddParameter("ResultLimit", 100);
mainQuery.AddWithClause("ACTIVE_USERS", cte);
// Act
var merged = mainQuery.GetMergedParameters();
// Assert
var keys = merged.Keys.ToList();
Assert.That(keys.Any(k => k.Contains("MinDate")));
Assert.That(keys.Any(k => k.Contains("ResultLimit")));
}
[Test]
public void GetMergedParameters_WithMultipleCTEs_MergesAllParameters()
{
// Arrange
var cte1 = new QueryBreakdown("ID, STATUS", "USERS", "STATUS = 'active'");
cte1.AddParameter("Status1", "active");
var cte2 = new QueryBreakdown("USER_ID, CREATED", "ORDERS", "CREATED >= CURRENT_DATE");
cte2.AddParameter("MinDate", DateTime.Today.AddDays(-30));
var mainQuery = new QueryBreakdown("*", "RESULTS");
mainQuery.AddParameter("ResultLimit", 50);
mainQuery.AddWithClause("ACTIVE_USERS", cte1);
mainQuery.AddWithClause("RECENT_ORDERS", cte2);
// Act
var merged = mainQuery.GetMergedParameters();
// Assert
var keys = merged.Keys.ToList();
Assert.That(keys.Any(k => k.Contains("Status1")));
Assert.That(keys.Any(k => k.Contains("MinDate")));
Assert.That(keys.Any(k => k.Contains("ResultLimit")));
}
[Test]
public void GetMergedParameters_WithParameterConflict_MainQueryTakesPrecedence()
{
// Arrange
var cte = new QueryBreakdown("ID, NAME", "BASE_USERS");
cte.AddParameter("UserId", 999); // CTE version
var mainQuery = new QueryBreakdown("*", "ACTIVE_USERS");
mainQuery.AddParameter("UserId", 123); // Main query version (should win)
mainQuery.AddWithClause("ACTIVE_USERS", cte);
// Act
var merged = mainQuery.GetMergedParameters();
// Assert
// Find the UserId parameter value
var userIdKey = merged.Keys.FirstOrDefault(k => k.Contains("UserId") && !k.Contains("$"));
Assert.That(userIdKey, Is.Not.Null);
Assert.That(merged[userIdKey], Is.EqualTo(123)); // Main query value, not CTE value
}
[Test]
public void GetMergedParameters_WithNestedCTEs_CollectsAllParameters()
{
// Arrange
var cte1 = new QueryBreakdown("id, name", "base_users");
cte1.AddParameter("Status", "active");
var cte2 = new QueryBreakdown("*", "NESTED_USERS");
cte2.AddParameter("Level", 1);
cte2.AddWithClause("ACTIVE_USERS", cte1);
var mainQuery = new QueryBreakdown("*", "FINAL_RESULTS");
mainQuery.AddParameter("Limit", 100);
mainQuery.AddWithClause("FILTERED_USERS", cte2);
// Act
var merged = mainQuery.GetMergedParameters();
// Assert
var keys = merged.Keys.ToList();
Assert.That(keys.Any(k => k.Contains("Status")), "Should include Status from nested CTE");
Assert.That(keys.Any(k => k.Contains("Level")), "Should include Level from middle CTE");
Assert.That(keys.Any(k => k.Contains("Limit")), "Should include Limit from main query");
}
[Test]
public void GetMergedParameters_PreservesParameterTypes()
{
// Arrange
var cte = new QueryBreakdown("id, name", "users");
cte.AddParameter("CreatedDate", new DateTime(2024, 1, 1));
var mainQuery = new QueryBreakdown("*", "results");
mainQuery.AddParameter("IsActive", true);
mainQuery.AddParameter("Threshold", 42.5);
mainQuery.AddWithClause("users_cte", cte);
// Act
var merged = mainQuery.GetMergedParameters();
// Assert
var dateKey = merged.Keys.FirstOrDefault(k => k.Contains("CreatedDate") && !k.Contains("$"));
var boolKey = merged.Keys.FirstOrDefault(k => k.Contains("IsActive") && !k.Contains("$"));
var doubleKey = merged.Keys.FirstOrDefault(k => k.Contains("Threshold") && !k.Contains("$"));
Assert.That(dateKey, Is.Not.Null);
Assert.That(merged[dateKey], Is.TypeOf<DateTime>());
Assert.That(boolKey, Is.Not.Null);
Assert.That(merged[boolKey], Is.TypeOf<bool>());
Assert.That(doubleKey, Is.Not.Null);
Assert.That(merged[doubleKey], Is.TypeOf<double>());
}
#endregion
}
@@ -0,0 +1,319 @@
using Strata.SqlTools.Breakdowns.PostgreSql;
using Strata.SqlTools.SqlBreakdown.Classes;
namespace Strata.SqlTools.SqlBreakdown.Tests.PostgreSql;
[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 positional $ parameters")]
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
// PostgreSQL stores parameters with positional syntax
var statusKey = merged.Keys.FirstOrDefault(k => k.Contains("Status") && !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,357 @@
using Strata.SqlTools.Statements.PostgreSql;
namespace Strata.SqlTools.SqlBreakdown.Tests.PostgreSql;
[TestFixture]
public class StatementExpressionParserTests
{
[Test]
public void Parse_NumberExpression_ReturnNum()
{
// Arrange
var parser = new StatementExpressionParser();
var sql = "42";
// Act
var result = parser.Parse(sql);
// Assert
Assert.That(result, Is.Not.Null);
}
[Test]
public void Parse_NegativeNumber_Succeeds()
{
// Arrange
var parser = new StatementExpressionParser();
var sql = "-42";
// Act
var result = parser.Parse(sql);
// Assert
Assert.That(result, Is.Not.Null);
}
[Test]
public void Parse_DecimalNumber_Succeeds()
{
// Arrange
var parser = new StatementExpressionParser();
var sql = "3.14159";
// Act
var result = parser.Parse(sql);
// Assert
Assert.That(result, Is.Not.Null);
}
[Test]
public void Parse_StringExpression_ReturnsString()
{
// Arrange
var parser = new StatementExpressionParser();
var sql = "'hello world'";
// Act
var result = parser.Parse(sql);
// Assert
Assert.That(result, Is.Not.Null);
}
[Test]
public void Parse_StringWithEscapedQuotes_Succeeds()
{
// Arrange
var parser = new StatementExpressionParser();
var sql = "'it''s working'"; // PostgreSQL escape style
// Act
var result = parser.Parse(sql);
// Assert
Assert.That(result, Is.Not.Null);
}
[Test]
public void Parse_ColumnIdentifier_Succeeds()
{
// Arrange
var parser = new StatementExpressionParser();
var sql = "user_id";
// Act
var result = parser.Parse(sql);
// Assert
Assert.That(result, Is.Not.Null);
}
[Test]
public void Parse_QuotedIdentifier_Succeeds()
{
// Arrange
var parser = new StatementExpressionParser();
var sql = "\"UserId\"";
// Act
var result = parser.Parse(sql);
// Assert
Assert.That(result, Is.Not.Null);
}
[Test]
public void Parse_ColumnWithTablePrefix_Succeeds()
{
// Arrange
var parser = new StatementExpressionParser();
var sql = "users.id";
// Act
var result = parser.Parse(sql);
// Assert
Assert.That(result, Is.Not.Null);
}
[Test]
public void Parse_QuotedColumnWithTablePrefix_Succeeds()
{
// Arrange
var parser = new StatementExpressionParser();
var sql = "\"Users\".\"Id\"";
// Act
var result = parser.Parse(sql);
// Assert
Assert.That(result, Is.Not.Null);
}
[Test]
public void Parse_SimpleAddition_Succeeds()
{
// Arrange
var parser = new StatementExpressionParser();
var sql = "42 + 8";
// Act
var result = parser.Parse(sql);
// Assert
Assert.That(result, Is.Not.Null);
}
[Test]
public void Parse_SimpleSubtraction_Succeeds()
{
// Arrange
var parser = new StatementExpressionParser();
var sql = "42 - 8";
// Act
var result = parser.Parse(sql);
// Assert
Assert.That(result, Is.Not.Null);
}
[Test]
public void Parse_SimpleMultiplication_Succeeds()
{
// Arrange
var parser = new StatementExpressionParser();
var sql = "42 * 8";
// Act
var result = parser.Parse(sql);
// Assert
Assert.That(result, Is.Not.Null);
}
[Test]
public void Parse_SimpleDivision_Succeeds()
{
// Arrange
var parser = new StatementExpressionParser();
var sql = "42 / 8";
// Act
var result = parser.Parse(sql);
// Assert
Assert.That(result, Is.Not.Null);
}
[Test]
public void Parse_ComplexArithmeticExpression_Succeeds()
{
// Arrange
var parser = new StatementExpressionParser();
var sql = "(42 + 8) * (10 - 2) / 4";
// Act
var result = parser.Parse(sql);
// Assert
Assert.That(result, Is.Not.Null);
}
[Test]
public void Parse_FunctionCall_Succeeds()
{
// Arrange
var parser = new StatementExpressionParser();
var sql = "COUNT(id)";
// Act
var result = parser.Parse(sql);
// Assert
Assert.That(result, Is.Not.Null);
}
[Test]
public void Parse_CountExpression_Succeeds()
{
// Arrange
var parser = new StatementExpressionParser();
var sql = "COUNT(*)";
// Act
var result = parser.Parse(sql);
// Assert
Assert.That(result, Is.Not.Null);
}
[Test]
public void Parse_SumExpression_Succeeds()
{
// Arrange
var parser = new StatementExpressionParser();
var sql = "SUM(amount)";
// Act
var result = parser.Parse(sql);
// Assert
Assert.That(result, Is.Not.Null);
}
[Test]
public void Parse_CaseInsensitiveFunctionName_Succeeds()
{
// Arrange
var parser = new StatementExpressionParser();
var sql = "sum(amount)";
// Act
var result = parser.Parse(sql);
// Assert
Assert.That(result, Is.Not.Null);
}
[Test]
public void Parse_FunctionWithMultipleArguments_Succeeds()
{
// Arrange
var parser = new StatementExpressionParser();
var sql = "SUBSTRING(name, 1, 10)";
// Act
var result = parser.Parse(sql);
// Assert
Assert.That(result, Is.Not.Null);
}
[Test]
public void Parse_NestedFunctionCalls_Succeeds()
{
// Arrange
var parser = new StatementExpressionParser();
var sql = "UPPER(SUBSTRING(name, 1, 10))";
// Act
var result = parser.Parse(sql);
// Assert
Assert.That(result, Is.Not.Null);
}
[Test]
public void Parse_ColumnWithAlias_Succeeds()
{
// Arrange
var parser = new StatementExpressionParser();
var sql = "COUNT(id) AS total_count";
// Act
var result = parser.Parse(sql);
// Assert
Assert.That(result, Is.Not.Null);
}
[Test]
public void Parse_CaseExpression_Succeeds()
{
// Arrange
var parser = new StatementExpressionParser();
var sql = "CASE WHEN status = 'active' THEN 1 ELSE 0 END";
// Act
var result = parser.Parse(sql);
// Assert
Assert.That(result, Is.Not.Null);
}
[Test]
public void Parse_PostgreSqlPositionalParameter_Succeeds()
{
// Arrange
var parser = new StatementExpressionParser();
var sql = "$1";
// Act
var result = parser.Parse(sql);
// Assert
Assert.That(result, Is.Not.Null);
}
[Test]
public void Parse_NamedParameter_Succeeds()
{
// Arrange
var parser = new StatementExpressionParser();
var sql = "@userId";
// Act
var result = parser.Parse(sql);
// Assert
Assert.That(result, Is.Not.Null);
}
[Test]
public void Parse_ColonNamedParameter_Succeeds()
{
// Arrange
var parser = new StatementExpressionParser();
var sql = ":userId";
// Act
var result = parser.Parse(sql);
// Assert
Assert.That(result, Is.Not.Null);
}
}
@@ -0,0 +1,223 @@
using Strata.SqlTools.SqlBreakdown.Enums.SQL;
using Strata.SqlTools.SqlBreakdown.Exceptions;
using Strata.SqlTools.SqlBreakdown.Exceptions;
using StatementReader = Strata.SqlTools.Statements.PostgreSql.StatementReader;
namespace Strata.SqlTools.SqlBreakdown.Tests.PostgreSql;
[TestFixture]
public class StatementReaderTests
{
[Test]
public void StatementReader_ParsesCalculationString()
{
// Arrange
var calculationString = "(1 + 2) / SUM(column_id) + SUM(other_column) - SUM(another_column) * 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 - PostgreSQL-specific double-quoted identifiers (case-sensitive)
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));
}
[Test]
public void StatementReader_ParsesDoubleQuotedIdentifiersWithCase()
{
// Arrange - PostgreSQL preserves case in double-quoted identifiers
var calculationString = "\"MyColumn\" + \"AnotherColumn\"";
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[0].Type, Is.EqualTo(TokenType.ColumnIdentifier));
Assert.That(tokens[0].Value, Is.EqualTo("MyColumn"));
Assert.That(tokens[2].Type, Is.EqualTo(TokenType.ColumnIdentifier));
Assert.That(tokens[2].Value, Is.EqualTo("AnotherColumn"));
}
[Test]
public void StatementReader_ParsesPositionalParameters()
{
// Arrange - PostgreSQL positional parameters ($1, $2, etc.)
var calculationString = "column_id = $1 AND status = $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.Count, Is.GreaterThan(0));
var paramTokens = tokens.Where(t => t.Type == TokenType.Parameter).ToList();
Assert.That(paramTokens.Count, Is.EqualTo(2));
Assert.That(paramTokens[0].Value, Is.EqualTo("$1"));
Assert.That(paramTokens[1].Value, Is.EqualTo("$2"));
}
[Test]
public void StatementReader_ParsesUnderscorePrefixedIdentifiers()
{
// Arrange - PostgreSQL allows underscores to start identifiers
var calculationString = "_internal_column + _private_field";
var sqlReader = new StatementReader(calculationString);
var tokens = new List<(TokenType Type, string Value)>();
// Act
while (sqlReader.Read())
{
tokens.Add((sqlReader.TokenType, sqlReader.TokenValue));
}
// Assert
var identifierTokens = tokens.Where(t => t.Type == TokenType.ColumnIdentifier).ToList();
Assert.That(identifierTokens.Count, Is.GreaterThanOrEqualTo(2));
}
[Test]
public void StatementReader_ParsesComplexExpression()
{
// Arrange - Complex PostgreSQL expression
var calculationString = "(\"Price\" * quantity) - discount WHERE user_id = $1";
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);
var identifierCount = tokens.Count(t => t.Type == TokenType.ColumnIdentifier);
Assert.That(identifierCount, Is.GreaterThan(0));
}
[Test]
public void StatementReader_ThrowsOnUnclosedDoubleQuote()
{
// Arrange
var calculationString = "\"UNCLOSED_IDENTIFIER + 1";
var sqlReader = new StatementReader(calculationString);
// Act & Assert
Assert.Throws<InvalidSyntaxException>(() =>
{
while (sqlReader.Read()) { }
});
}
[Test]
public void StatementReader_ParsesWhitespace()
{
// Arrange
var calculationString = " column_1 + column_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.Count, Is.GreaterThan(0));
}
[Test]
public void StatementReader_ParsesOperators()
{
// Arrange
var calculationString = "a = b";
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.Count, Is.GreaterThan(0));
}
[Test]
public void StatementReader_ParsesStrings()
{
// Arrange
var calculationString = "column = 'string value' AND other = 'another string'";
var sqlReader = new StatementReader(calculationString);
var tokens = new List<(TokenType Type, string Value)>();
// Act
while (sqlReader.Read())
{
tokens.Add((sqlReader.TokenType, sqlReader.TokenValue));
}
// Assert
var stringTokens = tokens.Where(t => t.Type == TokenType.String).ToList();
Assert.That(stringTokens.Count, Is.GreaterThanOrEqualTo(2));
}
[Test]
public void StatementReader_ParsesNumbers()
{
// Arrange
var calculationString = "42 + 3.14 - 100";
var sqlReader = new StatementReader(calculationString);
var tokens = new List<(TokenType Type, string Value)>();
// Act
while (sqlReader.Read())
{
tokens.Add((sqlReader.TokenType, sqlReader.TokenValue));
}
// Assert
var numberTokens = tokens.Where(t => t.Type == TokenType.Number).ToList();
Assert.That(numberTokens.Count, Is.GreaterThan(0));
}
}