chore: initial git load of code space
This commit is contained in:
@@ -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));
|
||||
}
|
||||
}
|
||||
@@ -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.PostgreSql\Strata.SqlTools.PostgreSql.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
Reference in New Issue
Block a user