From 0e56757591e4f0b5d0506f8168fe6b7bc2f66cca Mon Sep 17 00:00:00 2001 From: Ahmad Date: Wed, 14 Oct 2026 12:23:33 +0300 Subject: [PATCH 1/2] refactor(compiler): separate select prefix compilation to fix parameter binding order with pagination modifiers --- QueryBuilder.Tests/GeneralTests.cs | 24 ++++++++++++++++ QueryBuilder/Compilers/Compiler.cs | 32 ++++++++++++++------- QueryBuilder/Compilers/FirebirdCompiler.cs | 20 ++++++------- QueryBuilder/Compilers/SqlServerCompiler.cs | 20 ++++--------- 4 files changed, 60 insertions(+), 36 deletions(-) diff --git a/QueryBuilder.Tests/GeneralTests.cs b/QueryBuilder.Tests/GeneralTests.cs index 5496f063..98b850e5 100644 --- a/QueryBuilder.Tests/GeneralTests.cs +++ b/QueryBuilder.Tests/GeneralTests.cs @@ -754,5 +754,29 @@ public void Compile_WhereExists_DoesNotMutateSubquery() Assert.Equal(2, inner.SelectClauses.Count); } + + [Fact] + public void SqlServer_SelectPrefix_TopWithColumnBindings_PreservesParameterOrder() + { + var query = new Query("users").SelectRaw("? AS [bonus]", 100).Limit(5); + var compiler = new SqlServerCompiler { UseLegacyPagination = true }; + + var result = compiler.Compile(query); + + Assert.Equal("SELECT TOP (@p0) @p1 AS [bonus] FROM [users]", result.Sql); + Assert.Equal(5, result.NamedBindings["@p0"]); + Assert.Equal(100, result.NamedBindings["@p1"]); + } + + [Fact] + public void Firebird_SelectPrefix_FirstWithDistinct() + { + var query = new Query("users").Select("name").Distinct().Limit(10); + var compiler = new FirebirdCompiler(); + + var result = compiler.Compile(query); + + Assert.Equal("SELECT FIRST 10 DISTINCT \"NAME\" FROM \"USERS\"", result.ToString()); + } } } diff --git a/QueryBuilder/Compilers/Compiler.cs b/QueryBuilder/Compilers/Compiler.cs index a09d8124..b9b783ae 100644 --- a/QueryBuilder/Compilers/Compiler.cs +++ b/QueryBuilder/Compilers/Compiler.cs @@ -675,7 +675,19 @@ protected virtual SqlResult OnBeforeSelect(SqlResult ctx) return ctx; } - protected virtual string CompileColumns(SqlResult ctx) + protected virtual string CompileSelectPrefix(SqlResult ctx) + { + var aggregate = ctx.Query.AggregateClauses.ForEngineOrDefault(EngineCode); + if (aggregate != null) + { + return "SELECT "; + } + + var distinct = ctx.Query.IsDistinct ? "DISTINCT " : ""; + return $"SELECT {distinct}"; + } + + protected virtual string CompileColumnExpressions(SqlResult ctx) { var aggregate = ctx.Query.AggregateClauses.ForEngineOrDefault(EngineCode); if (aggregate != null) @@ -684,21 +696,19 @@ protected virtual string CompileColumns(SqlResult ctx) .Select(x => CompileColumn(ctx, new Column { Name = x })) .ToList(); - string sql = string.Empty; - if (aggregateColumns.Count == 1) { - sql = string.Join(", ", aggregateColumns); + var sql = string.Join(", ", aggregateColumns); if (ctx.Query.IsDistinct) { sql = "DISTINCT " + sql; } - return "SELECT " + aggregate.Type.ToUpperInvariant() + "(" + sql + $") {ColumnAsKeyword}" + WrapValue(aggregate.Type); + return aggregate.Type.ToUpperInvariant() + "(" + sql + $") {ColumnAsKeyword}" + WrapValue(aggregate.Type); } - return "SELECT 1"; + return "1"; } var columns = ctx.Query.SelectClauses @@ -706,12 +716,12 @@ protected virtual string CompileColumns(SqlResult ctx) .Select(x => CompileColumn(ctx, x)) .ToList(); - var distinct = ctx.Query.IsDistinct ? "DISTINCT " : ""; - - var select = columns.Any() ? string.Join(", ", columns) : "*"; - - return $"SELECT {distinct}{select}"; + return columns.Any() ? string.Join(", ", columns) : "*"; + } + protected virtual string CompileColumns(SqlResult ctx) + { + return $"{CompileSelectPrefix(ctx)}{CompileColumnExpressions(ctx)}"; } public virtual string CompileUnion(SqlResult ctx) diff --git a/QueryBuilder/Compilers/FirebirdCompiler.cs b/QueryBuilder/Compilers/FirebirdCompiler.cs index 74637c63..7af02d99 100644 --- a/QueryBuilder/Compilers/FirebirdCompiler.cs +++ b/QueryBuilder/Compilers/FirebirdCompiler.cs @@ -47,27 +47,25 @@ public override string CompileLimit(SqlResult ctx) } - protected override string CompileColumns(SqlResult ctx) + protected override string CompileSelectPrefix(SqlResult ctx) { - var compiled = base.CompileColumns(ctx); - var limit = ctx.Query.GetLimit(EngineCode); var offset = ctx.Query.GetOffset(EngineCode); + var distinct = ctx.Query.IsDistinct ? "DISTINCT " : ""; if (limit > 0 && offset == 0) { - ctx.Bindings.Insert(0, limit); - - return string.Concat($"SELECT FIRST {parameterPlaceholder}", compiled.AsSpan(6)); + ctx.Bindings.Add(limit); + return $"SELECT FIRST {parameterPlaceholder} {distinct}"; } - else if (limit == 0 && offset > 0) - { - ctx.Bindings.Insert(0, offset); - return string.Concat($"SELECT SKIP {parameterPlaceholder}", compiled.AsSpan(6)); + if (limit == 0 && offset > 0) + { + ctx.Bindings.Add(offset); + return $"SELECT SKIP {parameterPlaceholder} {distinct}"; } - return compiled; + return base.CompileSelectPrefix(ctx); } protected override string CompileBasicDateCondition(SqlResult ctx, BasicDateCondition condition) diff --git a/QueryBuilder/Compilers/SqlServerCompiler.cs b/QueryBuilder/Compilers/SqlServerCompiler.cs index d48141e3..0bd4daea 100644 --- a/QueryBuilder/Compilers/SqlServerCompiler.cs +++ b/QueryBuilder/Compilers/SqlServerCompiler.cs @@ -61,13 +61,11 @@ protected override SqlResult CompileSelectQuery(Query query) return result; } - protected override string CompileColumns(SqlResult ctx) + protected override string CompileSelectPrefix(SqlResult ctx) { - var compiled = base.CompileColumns(ctx); - if (!UseLegacyPagination) { - return compiled; + return base.CompileSelectPrefix(ctx); } // If there is a limit on the query, but not an offset, we will add the top @@ -78,19 +76,13 @@ protected override string CompileColumns(SqlResult ctx) if (limit > 0 && offset == 0) { - // top bindings should be inserted first - ctx.Bindings.Insert(0, limit); - - // handle distinct - if (compiled.StartsWith("SELECT DISTINCT")) - { - return $"SELECT DISTINCT TOP ({parameterPlaceholder}){compiled.Substring(15)}"; - } + ctx.Bindings.Add(limit); - return $"SELECT TOP ({parameterPlaceholder}){compiled.Substring(6)}"; + var distinct = ctx.Query.IsDistinct ? "DISTINCT " : ""; + return $"SELECT {distinct}TOP ({parameterPlaceholder}) "; } - return compiled; + return base.CompileSelectPrefix(ctx); } public override string CompileLimit(SqlResult ctx) From 232d6fdc1646613dae3dcd8fb8eef747830dd397 Mon Sep 17 00:00:00 2001 From: Ahmad Date: Wed, 14 Oct 2026 16:08:06 +0300 Subject: [PATCH 2/2] refactor(compiler): visitor pattern --- .../Firebird/FirebirdLimitTests.cs | 8 +- QueryBuilder.Tests/MySql/MySqlLimitTests.cs | 8 +- .../Oracle/OracleLegacyLimitTests.cs | 8 +- QueryBuilder.Tests/Oracle/OracleLimitTests.cs | 8 +- .../PostgreSql/PostgreSqlLimitTests.cs | 8 +- .../SqlServer/SqlServerLegacyLimitTests.cs | 8 +- .../SqlServer/SqlServerLimitTests.cs | 8 +- QueryBuilder.Tests/Sqlite/SqliteLimitTests.cs | 8 +- QueryBuilder/Compilers/CompileContext.cs | 105 ++ QueryBuilder/Compilers/Compiler.Conditions.cs | 329 ++++-- QueryBuilder/Compilers/Compiler.cs | 992 ++++++++---------- QueryBuilder/Compilers/FirebirdCompiler.cs | 58 +- QueryBuilder/Compilers/MySqlCompiler.cs | 27 +- QueryBuilder/Compilers/OracleCompiler.cs | 80 +- QueryBuilder/Compilers/PostgresCompiler.cs | 35 +- QueryBuilder/Compilers/SqlServerCompiler.cs | 118 ++- QueryBuilder/Compilers/SqliteCompiler.cs | 36 +- QueryBuilder/SqlResult.cs | 7 +- 18 files changed, 995 insertions(+), 856 deletions(-) create mode 100644 QueryBuilder/Compilers/CompileContext.cs diff --git a/QueryBuilder.Tests/Firebird/FirebirdLimitTests.cs b/QueryBuilder.Tests/Firebird/FirebirdLimitTests.cs index b449bae8..ef058628 100644 --- a/QueryBuilder.Tests/Firebird/FirebirdLimitTests.cs +++ b/QueryBuilder.Tests/Firebird/FirebirdLimitTests.cs @@ -17,7 +17,7 @@ public FirebirdLimitTests() public void NoLimitNorOffset() { var query = new Query("Table"); - var ctx = new SqlResult("?", "\\") {Query = query}; + var ctx = new CompileContext(query); Assert.Null(compiler.CompileLimit(ctx)); } @@ -26,7 +26,7 @@ public void NoLimitNorOffset() public void LimitOnly() { var query = new Query("Table").Limit(10); - var ctx = new SqlResult("?", "\\") {Query = query}; + var ctx = new CompileContext(query); Assert.Null(compiler.CompileLimit(ctx)); } @@ -35,7 +35,7 @@ public void LimitOnly() public void OffsetOnly() { var query = new Query("Table").Offset(20); - var ctx = new SqlResult("?", "\\") {Query = query}; + var ctx = new CompileContext(query); Assert.Null(compiler.CompileLimit(ctx)); } @@ -44,7 +44,7 @@ public void OffsetOnly() public void LimitAndOffset() { var query = new Query("Table").Limit(5).Offset(20); - var ctx = new SqlResult("?", "\\") {Query = query}; + var ctx = new CompileContext(query); Assert.Equal("ROWS ? TO ?", compiler.CompileLimit(ctx)); Assert.Equal(21L, ctx.Bindings[0]); diff --git a/QueryBuilder.Tests/MySql/MySqlLimitTests.cs b/QueryBuilder.Tests/MySql/MySqlLimitTests.cs index 3e22c19a..6360ca40 100644 --- a/QueryBuilder.Tests/MySql/MySqlLimitTests.cs +++ b/QueryBuilder.Tests/MySql/MySqlLimitTests.cs @@ -17,7 +17,7 @@ public MySqlLimitTests() public void WithNoLimitNorOffset() { var query = new Query("Table"); - var ctx = new SqlResult("?", "\\") {Query = query}; + var ctx = new CompileContext(query); Assert.Null(compiler.CompileLimit(ctx)); } @@ -26,7 +26,7 @@ public void WithNoLimitNorOffset() public void WithNoOffset() { var query = new Query("Table").Limit(10); - var ctx = new SqlResult("?", "\\") {Query = query}; + var ctx = new CompileContext(query); Assert.Equal("LIMIT ?", compiler.CompileLimit(ctx)); Assert.Equal(10, ctx.Bindings[0]); @@ -36,7 +36,7 @@ public void WithNoOffset() public void WithNoLimit() { var query = new Query("Table").Offset(20); - var ctx = new SqlResult("?", "\\") {Query = query}; + var ctx = new CompileContext(query); Assert.Equal("LIMIT 18446744073709551615 OFFSET ?", compiler.CompileLimit(ctx)); Assert.Equal(20L, ctx.Bindings[0]); @@ -47,7 +47,7 @@ public void WithNoLimit() public void WithLimitAndOffset() { var query = new Query("Table").Limit(5).Offset(20); - var ctx = new SqlResult("?", "\\") {Query = query}; + var ctx = new CompileContext(query); Assert.Equal("LIMIT ? OFFSET ?", compiler.CompileLimit(ctx)); Assert.Equal(5, ctx.Bindings[0]); diff --git a/QueryBuilder.Tests/Oracle/OracleLegacyLimitTests.cs b/QueryBuilder.Tests/Oracle/OracleLegacyLimitTests.cs index 47a233dd..3540fba1 100644 --- a/QueryBuilder.Tests/Oracle/OracleLegacyLimitTests.cs +++ b/QueryBuilder.Tests/Oracle/OracleLegacyLimitTests.cs @@ -21,7 +21,7 @@ public void WithNoLimitNorOffset() { // Arrange: var query = new Query(TableName); - var ctx = new SqlResult("?", "\\") { Query = query, RawSql = SqlPlaceholder }; + var ctx = new CompileContext(query, "?", "\\") { Query = query, RawSql = SqlPlaceholder }; // Act: compiler.ApplyLegacyLimit(ctx); @@ -35,7 +35,7 @@ public void WithNoOffset() { // Arrange: var query = new Query(TableName).Limit(10); - var ctx = new SqlResult("?", "\\") { Query = query, RawSql = SqlPlaceholder }; + var ctx = new CompileContext(query, "?", "\\") { Query = query, RawSql = SqlPlaceholder }; // Act: compiler.ApplyLegacyLimit(ctx); @@ -51,7 +51,7 @@ public void WithNoLimit() { // Arrange: var query = new Query(TableName).Offset(20); - var ctx = new SqlResult("?", "\\") { Query = query, RawSql = SqlPlaceholder }; + var ctx = new CompileContext(query, "?", "\\") { Query = query, RawSql = SqlPlaceholder }; // Act: compiler.ApplyLegacyLimit(ctx); @@ -67,7 +67,7 @@ public void WithLimitAndOffset() { // Arrange: var query = new Query(TableName).Limit(5).Offset(20); - var ctx = new SqlResult("?", "\\") { Query = query, RawSql = SqlPlaceholder }; + var ctx = new CompileContext(query, "?", "\\") { Query = query, RawSql = SqlPlaceholder }; // Act: compiler.ApplyLegacyLimit(ctx); diff --git a/QueryBuilder.Tests/Oracle/OracleLimitTests.cs b/QueryBuilder.Tests/Oracle/OracleLimitTests.cs index 9701705c..700f6e23 100644 --- a/QueryBuilder.Tests/Oracle/OracleLimitTests.cs +++ b/QueryBuilder.Tests/Oracle/OracleLimitTests.cs @@ -21,7 +21,7 @@ public void NoLimitNorOffset() { // Arrange: var query = new Query(TableName); - var ctx = new SqlResult("?", "\\") { Query = query, RawSql = SqlPlaceholder }; + var ctx = new CompileContext(query, "?", "\\") { RawSql = SqlPlaceholder }; // Act & Assert: Assert.Null(compiler.CompileLimit(ctx)); @@ -32,7 +32,7 @@ public void LimitOnly() { // Arrange: var query = new Query(TableName).Limit(10); - var ctx = new SqlResult("?", "\\") { Query = query, RawSql = SqlPlaceholder }; + var ctx = new CompileContext(query, "?", "\\") { RawSql = SqlPlaceholder }; // Act & Assert: Assert.EndsWith("OFFSET ? ROWS FETCH NEXT ? ROWS ONLY", compiler.CompileLimit(ctx)); @@ -46,7 +46,7 @@ public void OffsetOnly() { // Arrange: var query = new Query(TableName).Offset(20); - var ctx = new SqlResult("?", "\\") { Query = query, RawSql = SqlPlaceholder }; + var ctx = new CompileContext(query, "?", "\\") { RawSql = SqlPlaceholder }; // Act & Assert: Assert.EndsWith("OFFSET ? ROWS", compiler.CompileLimit(ctx)); @@ -60,7 +60,7 @@ public void LimitAndOffset() { // Arrange: var query = new Query(TableName).Limit(5).Offset(20); - var ctx = new SqlResult("?", "\\") { Query = query, RawSql = SqlPlaceholder }; + var ctx = new CompileContext(query, "?", "\\") { RawSql = SqlPlaceholder }; // Act & Assert: Assert.EndsWith("OFFSET ? ROWS FETCH NEXT ? ROWS ONLY", compiler.CompileLimit(ctx)); diff --git a/QueryBuilder.Tests/PostgreSql/PostgreSqlLimitTests.cs b/QueryBuilder.Tests/PostgreSql/PostgreSqlLimitTests.cs index 169fb99d..f7fcbfef 100644 --- a/QueryBuilder.Tests/PostgreSql/PostgreSqlLimitTests.cs +++ b/QueryBuilder.Tests/PostgreSql/PostgreSqlLimitTests.cs @@ -17,7 +17,7 @@ public PostgreSqlLimitTests() public void WithNoLimitNorOffset() { var query = new Query("Table"); - var ctx = new SqlResult("?", "\\") {Query = query}; + var ctx = new CompileContext(query); Assert.Null(compiler.CompileLimit(ctx)); } @@ -26,7 +26,7 @@ public void WithNoLimitNorOffset() public void WithNoOffset() { var query = new Query("Table").Limit(10); - var ctx = new SqlResult("?", "\\") {Query = query}; + var ctx = new CompileContext(query); Assert.Equal("LIMIT ?", compiler.CompileLimit(ctx)); Assert.Equal(10, ctx.Bindings[0]); @@ -36,7 +36,7 @@ public void WithNoOffset() public void WithNoLimit() { var query = new Query("Table").Offset(20); - var ctx = new SqlResult("?", "\\") {Query = query}; + var ctx = new CompileContext(query); Assert.Equal("OFFSET ?", compiler.CompileLimit(ctx)); Assert.Equal(20L, ctx.Bindings[0]); @@ -47,7 +47,7 @@ public void WithNoLimit() public void WithLimitAndOffset() { var query = new Query("Table").Limit(5).Offset(20); - var ctx = new SqlResult("?", "\\") {Query = query}; + var ctx = new CompileContext(query); Assert.Equal("LIMIT ? OFFSET ?", compiler.CompileLimit(ctx)); Assert.Equal(5, ctx.Bindings[0]); diff --git a/QueryBuilder.Tests/SqlServer/SqlServerLegacyLimitTests.cs b/QueryBuilder.Tests/SqlServer/SqlServerLegacyLimitTests.cs index 1cc58060..1119ceb1 100644 --- a/QueryBuilder.Tests/SqlServer/SqlServerLegacyLimitTests.cs +++ b/QueryBuilder.Tests/SqlServer/SqlServerLegacyLimitTests.cs @@ -18,7 +18,7 @@ public SqlServerLegacyLimitTests() public void NoLimitNorOffset() { var query = new Query("Table"); - var ctx = new SqlResult("?", "\\") {Query = query}; + var ctx = new CompileContext(query); Assert.Null(compiler.CompileLimit(ctx)); } @@ -27,7 +27,7 @@ public void NoLimitNorOffset() public void LimitOnly() { var query = new Query("Table").Limit(10); - var ctx = new SqlResult("?", "\\") {Query = query}; + var ctx = new CompileContext(query); Assert.Null(compiler.CompileLimit(ctx)); } @@ -36,7 +36,7 @@ public void LimitOnly() public void OffsetOnly() { var query = new Query("Table").Offset(20); - var ctx = new SqlResult("?", "\\") {Query = query}; + var ctx = new CompileContext(query); Assert.Null(compiler.CompileLimit(ctx)); } @@ -45,7 +45,7 @@ public void OffsetOnly() public void LimitAndOffset() { var query = new Query("Table").Limit(5).Offset(20); - var ctx = new SqlResult("?", "\\") {Query = query}; + var ctx = new CompileContext(query); Assert.Null(compiler.CompileLimit(ctx)); } diff --git a/QueryBuilder.Tests/SqlServer/SqlServerLimitTests.cs b/QueryBuilder.Tests/SqlServer/SqlServerLimitTests.cs index e6fa416b..e4b828ad 100644 --- a/QueryBuilder.Tests/SqlServer/SqlServerLimitTests.cs +++ b/QueryBuilder.Tests/SqlServer/SqlServerLimitTests.cs @@ -18,7 +18,7 @@ public SqlServerLimitTests() public void NoLimitNorOffset() { var query = new Query("Table"); - var ctx = new SqlResult("?", "\\") {Query = query}; + var ctx = new CompileContext(query); Assert.Null(compiler.CompileLimit(ctx)); } @@ -27,7 +27,7 @@ public void NoLimitNorOffset() public void LimitOnly() { var query = new Query("Table").Limit(10); - var ctx = new SqlResult("?", "\\") {Query = query}; + var ctx = new CompileContext(query); Assert.EndsWith("OFFSET ? ROWS FETCH NEXT ? ROWS ONLY", compiler.CompileLimit(ctx)); Assert.Equal(2, ctx.Bindings.Count); @@ -39,7 +39,7 @@ public void LimitOnly() public void OffsetOnly() { var query = new Query("Table").Offset(20); - var ctx = new SqlResult("?", "\\") {Query = query}; + var ctx = new CompileContext(query); Assert.EndsWith("OFFSET ? ROWS", compiler.CompileLimit(ctx)); @@ -51,7 +51,7 @@ public void OffsetOnly() public void LimitAndOffset() { var query = new Query("Table").Limit(5).Offset(20); - var ctx = new SqlResult("?", "\\") {Query = query}; + var ctx = new CompileContext(query); Assert.EndsWith("OFFSET ? ROWS FETCH NEXT ? ROWS ONLY", compiler.CompileLimit(ctx)); diff --git a/QueryBuilder.Tests/Sqlite/SqliteLimitTests.cs b/QueryBuilder.Tests/Sqlite/SqliteLimitTests.cs index 837a6654..8fb73f6c 100644 --- a/QueryBuilder.Tests/Sqlite/SqliteLimitTests.cs +++ b/QueryBuilder.Tests/Sqlite/SqliteLimitTests.cs @@ -17,7 +17,7 @@ public SqliteLimitTests() public void WithNoLimitNorOffset() { var query = new Query("Table"); - var ctx = new SqlResult("?", "\\") { Query = query }; + var ctx = new CompileContext(query); Assert.Null(compiler.CompileLimit(ctx)); } @@ -26,7 +26,7 @@ public void WithNoLimitNorOffset() public void WithNoOffset() { var query = new Query("Table").Limit(10); - var ctx = new SqlResult("?", "\\") { Query = query }; + var ctx = new CompileContext(query); Assert.Equal("LIMIT ?", compiler.CompileLimit(ctx)); Assert.Equal(10, ctx.Bindings[0]); @@ -36,7 +36,7 @@ public void WithNoOffset() public void WithNoLimit() { var query = new Query("Table").Offset(20); - var ctx = new SqlResult("?", "\\") { Query = query }; + var ctx = new CompileContext(query); Assert.Equal("LIMIT -1 OFFSET ?", compiler.CompileLimit(ctx)); Assert.Equal(20L, ctx.Bindings[0]); @@ -47,7 +47,7 @@ public void WithNoLimit() public void WithLimitAndOffset() { var query = new Query("Table").Limit(5).Offset(20); - var ctx = new SqlResult("?", "\\") { Query = query }; + var ctx = new CompileContext(query); Assert.Equal("LIMIT ? OFFSET ?", compiler.CompileLimit(ctx)); Assert.Equal(5, ctx.Bindings[0]); diff --git a/QueryBuilder/Compilers/CompileContext.cs b/QueryBuilder/Compilers/CompileContext.cs new file mode 100644 index 00000000..70d02f90 --- /dev/null +++ b/QueryBuilder/Compilers/CompileContext.cs @@ -0,0 +1,105 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace SqlKata.Compilers +{ + public class CompileContext + { + public StringBuilder Buffer { get; } = new(); + public List Bindings { get; set; } = []; + public string RawSql { get; set; } = ""; + public string ParameterPlaceholder { get; set; } = "?"; + public string EscapeCharacter { get; set; } = "\\"; + + private readonly Stack _queryStack = new(); + + public Query CurrentQuery => _queryStack.Count > 0 ? _queryStack.Peek() : null; + + public Query Query + { + get => CurrentQuery; + set + { + if (_queryStack.Count > 0) + { + _queryStack.Pop(); + } + + if (value != null) + { + _queryStack.Push(value); + } + } + } + + public IReadOnlyCollection QueryStack => _queryStack; + + public CompileContext() : this(null, "?", "\\") + { + } + + public CompileContext(Query query, string parameterPlaceholder = "?", string escapeCharacter = "\\") + { + ParameterPlaceholder = parameterPlaceholder; + EscapeCharacter = escapeCharacter; + if (query != null) + { + _queryStack.Push(query); + } + } + + public void PushQuery(Query query) + { + if (query != null) + { + _queryStack.Push(query); + } + } + + public Query PopQuery() + { + return _queryStack.Count > 0 ? _queryStack.Pop() : null; + } + + public object FindVariable(string name) + { + foreach (var q in _queryStack) + { + if (q?.Variables != null && q.Variables.TryGetValue(name, out var value)) + { + return value; + } + } + + throw new Exception($"Variable '{name}' not found"); + } + + public CompileContext Write(string value) + { + Buffer.Append(value); + return this; + } + + public CompileContext Write(char value) + { + Buffer.Append(value); + return this; + } + + public CompileContext EnsureSpace() + { + if (Buffer.Length > 0) + { + var lastChar = Buffer[Buffer.Length - 1]; + if (lastChar != ' ' && lastChar != '\n') + { + Buffer.Append(' '); + } + } + return this; + } + + public override string ToString() => Buffer.ToString(); + } +} diff --git a/QueryBuilder/Compilers/Compiler.Conditions.cs b/QueryBuilder/Compilers/Compiler.Conditions.cs index e252b1b1..d4f41b9f 100644 --- a/QueryBuilder/Compilers/Compiler.Conditions.cs +++ b/QueryBuilder/Compilers/Compiler.Conditions.cs @@ -1,94 +1,137 @@ using System; using System.Collections.Generic; using System.Linq; -using System.Reflection; namespace SqlKata.Compilers { public partial class Compiler { - protected virtual string CompileCondition(SqlResult ctx, AbstractCondition clause) + public virtual void VisitConditions(CompileContext ctx, List conditions) { - return clause switch - { - BasicDateCondition date => CompileBasicDateCondition(ctx, date), - BasicStringCondition str => CompileBasicStringCondition(ctx, str), - BasicCondition basic => CompileBasicCondition(ctx, basic), - TwoColumnsCondition two => CompileTwoColumnsCondition(ctx, two), - BetweenCondition between => CompileBetweenCondition(ctx, between), - InCondition inCondition => CompileInCondition(ctx, inCondition), - InQueryCondition inQuery => CompileInQueryCondition(ctx, inQuery), - NullCondition nullCondition => CompileNullCondition(ctx, nullCondition), - BooleanCondition booleanCondition => CompileBooleanCondition(ctx, booleanCondition), - NestedCondition nested => CompileNestedCondition(ctx, nested), - ExistsCondition exists => CompileExistsCondition(ctx, exists), - QueryCondition queryCondition => CompileQueryCondition(ctx, queryCondition), - SubQueryCondition subQuery => CompileSubQueryCondition(ctx, subQuery), - RawCondition raw => CompileRawCondition(ctx, raw), - _ => throw new ArgumentOutOfRangeException(nameof(clause), $"Unsupported condition type: {clause.GetType().Name}") - }; - } - - protected virtual string CompileConditions(SqlResult ctx, List conditions) - { - var result = new List(); + var isFirst = true; for (var i = 0; i < conditions.Count; i++) { - var compiled = CompileCondition(ctx, conditions[i]); + var cond = conditions[i]; + var beforeLen = ctx.Buffer.Length; - if (string.IsNullOrEmpty(compiled)) + if (!isFirst) { - continue; + ctx.Write(cond.IsOr ? "OR " : "AND "); } - var boolOperator = i == 0 ? "" : (conditions[i].IsOr ? "OR " : "AND "); + var condStart = ctx.Buffer.Length; + VisitCondition(ctx, cond); - result.Add(boolOperator + compiled); + if (ctx.Buffer.Length == condStart) + { + // Nothing was written for this condition, rollback operator if added + ctx.Buffer.Length = beforeLen; + } + else + { + isFirst = false; + if (i < conditions.Count - 1) + { + ctx.Write(" "); + } + } } - return string.Join(" ", result); + if (ctx.Buffer.Length > 0 && ctx.Buffer[ctx.Buffer.Length - 1] == ' ') + { + ctx.Buffer.Length--; + } } - protected virtual string CompileRawCondition(SqlResult ctx, RawCondition x) + public virtual void VisitCondition(CompileContext ctx, AbstractCondition clause) { - ctx.Bindings.AddRange(x.Bindings); - return WrapIdentifiers(x.Expression); + switch (clause) + { + case BasicDateCondition date: + VisitBasicDateCondition(ctx, date); + break; + case BasicStringCondition str: + VisitBasicStringCondition(ctx, str); + break; + case BasicCondition basic: + VisitBasicCondition(ctx, basic); + break; + case TwoColumnsCondition two: + VisitTwoColumnsCondition(ctx, two); + break; + case BetweenCondition between: + VisitBetweenCondition(ctx, between); + break; + case InCondition inCondition: + VisitInCondition(ctx, inCondition); + break; + case InQueryCondition inQuery: + VisitInQueryCondition(ctx, inQuery); + break; + case NullCondition nullCondition: + VisitNullCondition(ctx, nullCondition); + break; + case BooleanCondition booleanCondition: + VisitBooleanCondition(ctx, booleanCondition); + break; + case NestedCondition nested: + VisitNestedCondition(ctx, nested); + break; + case ExistsCondition exists: + VisitExistsCondition(ctx, exists); + break; + case QueryCondition queryCondition: + VisitQueryCondition(ctx, queryCondition); + break; + case SubQueryCondition subQuery: + VisitSubQueryCondition(ctx, subQuery); + break; + case RawCondition raw: + VisitRawCondition(ctx, raw); + break; + default: + throw new ArgumentOutOfRangeException(nameof(clause), $"Unsupported condition type: {clause.GetType().Name}"); + } } - protected virtual string CompileQueryCondition(SqlResult ctx, QueryCondition x) + protected virtual void VisitRawCondition(CompileContext ctx, RawCondition x) { - var subCtx = CompileSubQuery(ctx, x.Query); - - ctx.Bindings.AddRange(subCtx.Bindings); - - return Wrap(x.Column) + " " + checkOperator(x.Operator) + " (" + subCtx.RawSql + ")"; + ctx.Bindings.AddRange(x.Bindings); + ctx.Write(WrapIdentifiers(x.Expression)); } - protected virtual string CompileSubQueryCondition(SqlResult ctx, SubQueryCondition x) + protected virtual void VisitQueryCondition(CompileContext ctx, QueryCondition x) { - var subCtx = CompileSubQuery(ctx, x.Query); - - ctx.Bindings.AddRange(subCtx.Bindings); - - return "(" + subCtx.RawSql + ") " + checkOperator(x.Operator) + " " + Parameter(ctx, x.Value); + ctx.Write(Wrap(x.Column)).Write(" ").Write(checkOperator(x.Operator)).Write(" ("); + CompileSubQuery(ctx, x.Query); + ctx.Write(")"); } - protected virtual string CompileBasicCondition(SqlResult ctx, BasicCondition x) + protected virtual void VisitSubQueryCondition(CompileContext ctx, SubQueryCondition x) { - var sql = $"{Wrap(x.Column)} {checkOperator(x.Operator)} {Parameter(ctx, x.Value)}"; + ctx.Write("("); + CompileSubQuery(ctx, x.Query); + ctx.Write(") ").Write(checkOperator(x.Operator)).Write(" ").Write(Parameter(ctx, x.Value)); + } + protected virtual void VisitBasicCondition(CompileContext ctx, BasicCondition x) + { if (x.IsNot) { - return $"NOT ({sql})"; + ctx.Write("NOT ("); } - return sql; + ctx.Write(Wrap(x.Column)).Write(" ").Write(checkOperator(x.Operator)).Write(" ").Write(Parameter(ctx, x.Value)); + + if (x.IsNot) + { + ctx.Write(")"); + } } - protected virtual string CompileBasicStringCondition(SqlResult ctx, BasicStringCondition x) + protected virtual void VisitBasicStringCondition(CompileContext ctx, BasicStringCondition x) { - var column = Wrap(x.Column); var value = Resolve(ctx, x.Value) as string; @@ -102,7 +145,6 @@ protected virtual string CompileBasicStringCondition(SqlResult ctx, BasicStringC if (new[] { "starts", "ends", "contains", "like" }.Contains(x.Operator)) { - method = "LIKE"; switch (x.Operator) @@ -119,148 +161,207 @@ protected virtual string CompileBasicStringCondition(SqlResult ctx, BasicStringC } } - string sql; - - if (!x.CaseSensitive) { column = CompileLower(column); value = value.ToLowerInvariant(); } + if (x.IsNot) + { + ctx.Write("NOT ("); + } + if (x.Value is UnsafeLiteral) { - sql = $"{column} {checkOperator(method)} {value}"; + ctx.Write(column).Write(" ").Write(checkOperator(method)).Write(" ").Write(value); } else { - sql = $"{column} {checkOperator(method)} {Parameter(ctx, value)}"; + ctx.Write(column).Write(" ").Write(checkOperator(method)).Write(" ").Write(Parameter(ctx, value)); } if (!string.IsNullOrEmpty(x.EscapeCharacter)) { - sql = $"{sql} ESCAPE '{x.EscapeCharacter}'"; + ctx.Write(" ESCAPE '").Write(x.EscapeCharacter).Write("'"); } - return x.IsNot ? $"NOT ({sql})" : sql; - + if (x.IsNot) + { + ctx.Write(")"); + } } - protected virtual string CompileBasicDateCondition(SqlResult ctx, BasicDateCondition x) + protected virtual void VisitBasicDateCondition(CompileContext ctx, BasicDateCondition x) { var column = Wrap(x.Column); var op = checkOperator(x.Operator); - var sql = $"{x.Part.ToUpperInvariant()}({column}) {op} {Parameter(ctx, x.Value)}"; + if (x.IsNot) + { + ctx.Write("NOT ("); + } + + ctx.Write(x.Part.ToUpperInvariant()).Write("(").Write(column).Write(") ").Write(op).Write(" ").Write(Parameter(ctx, x.Value)); - return x.IsNot ? $"NOT ({sql})" : sql; + if (x.IsNot) + { + ctx.Write(")"); + } } - protected virtual string CompileNestedCondition(SqlResult ctx, NestedCondition x) + protected virtual void VisitNestedCondition(CompileContext ctx, NestedCondition x) { + List clauses; + if (x.Conditions is ConditionGroup group) { if (!group.HasConditions(EngineCode)) { - return null; + return; } - var sql = CompileConditions(ctx, group.GetConditions(EngineCode)); - return x.IsNot ? $"NOT ({sql})" : $"({sql})"; + clauses = group.GetConditions(EngineCode); } + else + { + if (!(x.Conditions.HasComponent("where", EngineCode) || x.Conditions.HasComponent("having", EngineCode))) + { + return; + } - if (!(x.Conditions.HasComponent("where", EngineCode) || x.Conditions.HasComponent("having", EngineCode))) + var clause = x.Conditions.HasComponent("where", EngineCode) ? "where" : "having"; + clauses = x.Conditions.GetComponents(clause, EngineCode); + } + + var startLen = ctx.Buffer.Length; + + if (x.IsNot) { - return null; + ctx.Write("NOT "); } - var clause = x.Conditions.HasComponent("where", EngineCode) ? "where" : "having"; + ctx.Write("("); + var innerStart = ctx.Buffer.Length; - var clauses = x.Conditions.GetComponents(clause, EngineCode); + VisitConditions(ctx, clauses); - var sql2 = CompileConditions(ctx, clauses); + if (ctx.Buffer.Length == innerStart) + { + // Nothing was written inside the nested group, roll back + ctx.Buffer.Length = startLen; + return; + } - return x.IsNot ? $"NOT ({sql2})" : $"({sql2})"; + ctx.Write(")"); } - protected string CompileTwoColumnsCondition(SqlResult ctx, TwoColumnsCondition clause) + protected virtual void VisitTwoColumnsCondition(CompileContext ctx, TwoColumnsCondition clause) { - var op = clause.IsNot ? "NOT " : ""; - return $"{op}{Wrap(clause.First)} {checkOperator(clause.Operator)} {Wrap(clause.Second)}"; + if (clause.IsNot) + { + ctx.Write("NOT "); + } + + ctx.Write(Wrap(clause.First)).Write(" ").Write(checkOperator(clause.Operator)).Write(" ").Write(Wrap(clause.Second)); } - protected virtual string CompileBetweenCondition(SqlResult ctx, BetweenCondition item) + protected virtual void VisitBetweenCondition(CompileContext ctx, BetweenCondition item) { var between = item.IsNot ? "NOT BETWEEN" : "BETWEEN"; - var lower = Parameter(ctx, item.Lower); - var higher = Parameter(ctx, item.Higher); - - return Wrap(item.Column) + $" {between} {lower} AND {higher}"; + ctx.Write(Wrap(item.Column)).Write(" ").Write(between).Write(" ") + .Write(Parameter(ctx, item.Lower)).Write(" AND ").Write(Parameter(ctx, item.Higher)); } - protected virtual string CompileInCondition(SqlResult ctx, InCondition item) + protected virtual void VisitInCondition(CompileContext ctx, InCondition item) { - var column = Wrap(item.Column); + ctx.Write(Wrap(item.Column)); if (!item.Values.Any()) { - return item.IsNot ? $"1 = 1 /* NOT IN [empty list] */" : "1 = 0 /* IN [empty list] */"; + ctx.Write(item.IsNot ? " 1 = 1 /* NOT IN [empty list] */" : " 1 = 0 /* IN [empty list] */"); + return; } - var inOperator = item.IsNot ? "NOT IN" : "IN"; - - var parameterizedValues = Parameterize(ctx, item.Values); - - return column + $" {inOperator} ({parameterizedValues})"; + var inOperator = item.IsNot ? " NOT IN (" : " IN ("; + ctx.Write(inOperator).Write(Parameterize(ctx, item.Values)).Write(")"); } - protected virtual string CompileInQueryCondition(SqlResult ctx, InQueryCondition item) + protected virtual void VisitInQueryCondition(CompileContext ctx, InQueryCondition item) { - - var subCtx = CompileSubQuery(ctx, item.Query); - - ctx.Bindings.AddRange(subCtx.Bindings); - - var inOperator = item.IsNot ? "NOT IN" : "IN"; - - return Wrap(item.Column) + $" {inOperator} ({subCtx.RawSql})"; + var inOperator = item.IsNot ? " NOT IN (" : " IN ("; + ctx.Write(Wrap(item.Column)).Write(inOperator); + CompileSubQuery(ctx, item.Query); + ctx.Write(")"); } - protected virtual string CompileNullCondition(SqlResult ctx, NullCondition item) + protected virtual void VisitNullCondition(CompileContext ctx, NullCondition item) { var op = item.IsNot ? "IS NOT NULL" : "IS NULL"; - return Wrap(item.Column) + " " + op; + ctx.Write(Wrap(item.Column)).Write(" ").Write(op); } - protected virtual string CompileBooleanCondition(SqlResult ctx, BooleanCondition item) + protected virtual void VisitBooleanCondition(CompileContext ctx, BooleanCondition item) { - var column = Wrap(item.Column); var value = item.Value ? CompileTrue() : CompileFalse(); - var op = item.IsNot ? "!=" : "="; - - return $"{column} {op} {value}"; + ctx.Write(Wrap(item.Column)).Write(" ").Write(op).Write(" ").Write(value); } - protected virtual string CompileExistsCondition(SqlResult ctx, ExistsCondition item) + protected virtual void VisitExistsCondition(CompileContext ctx, ExistsCondition item) { - var op = item.IsNot ? "NOT EXISTS" : "EXISTS"; + var op = item.IsNot ? "NOT EXISTS (" : "EXISTS ("; - - // remove unneeded components var query = item.Query.Clone(); - if (OmitSelectInsideExists) { query.SelectClauses.Clear(); query.SelectRaw("1"); } - var subCtx = CompileSubQuery(ctx, query); + ctx.Write(op); + CompileSubQuery(ctx, query); + ctx.Write(")"); + } - ctx.Bindings.AddRange(subCtx.Bindings); + // Backward compatibility wrappers delegating to visitor methods + protected virtual string CompileCondition(CompileContext ctx, AbstractCondition clause) + { + var temp = new CompileContext(ctx.CurrentQuery, parameterPlaceholder, EscapeCharacter); + VisitCondition(temp, clause); + ctx.Bindings.AddRange(temp.Bindings); + return temp.Buffer.ToString(); + } - return $"{op} ({subCtx.RawSql})"; + protected virtual string CompileConditions(CompileContext ctx, List conditions) + { + var temp = new CompileContext(ctx.CurrentQuery, parameterPlaceholder, EscapeCharacter); + VisitConditions(temp, conditions); + ctx.Bindings.AddRange(temp.Bindings); + return temp.Buffer.ToString(); + } + + protected virtual string CompileRawCondition(CompileContext ctx, RawCondition x) => RunToString(ctx, c => VisitRawCondition(c, x)); + protected virtual string CompileQueryCondition(CompileContext ctx, QueryCondition x) => RunToString(ctx, c => VisitQueryCondition(c, x)); + protected virtual string CompileSubQueryCondition(CompileContext ctx, SubQueryCondition x) => RunToString(ctx, c => VisitSubQueryCondition(c, x)); + protected virtual string CompileBasicCondition(CompileContext ctx, BasicCondition x) => RunToString(ctx, c => VisitBasicCondition(c, x)); + protected virtual string CompileBasicStringCondition(CompileContext ctx, BasicStringCondition x) => RunToString(ctx, c => VisitBasicStringCondition(c, x)); + protected virtual string CompileBasicDateCondition(CompileContext ctx, BasicDateCondition x) => RunToString(ctx, c => VisitBasicDateCondition(c, x)); + protected virtual string CompileNestedCondition(CompileContext ctx, NestedCondition x) => RunToString(ctx, c => VisitNestedCondition(c, x)); + protected virtual string CompileTwoColumnsCondition(CompileContext ctx, TwoColumnsCondition clause) => RunToString(ctx, c => VisitTwoColumnsCondition(c, clause)); + protected virtual string CompileBetweenCondition(CompileContext ctx, BetweenCondition item) => RunToString(ctx, c => VisitBetweenCondition(c, item)); + protected virtual string CompileInCondition(CompileContext ctx, InCondition item) => RunToString(ctx, c => VisitInCondition(c, item)); + protected virtual string CompileInQueryCondition(CompileContext ctx, InQueryCondition item) => RunToString(ctx, c => VisitInQueryCondition(c, item)); + protected virtual string CompileNullCondition(CompileContext ctx, NullCondition item) => RunToString(ctx, c => VisitNullCondition(c, item)); + protected virtual string CompileBooleanCondition(CompileContext ctx, BooleanCondition item) => RunToString(ctx, c => VisitBooleanCondition(c, item)); + protected virtual string CompileExistsCondition(CompileContext ctx, ExistsCondition item) => RunToString(ctx, c => VisitExistsCondition(c, item)); + + private string RunToString(CompileContext ctx, Action action) + { + var temp = new CompileContext(ctx.CurrentQuery, parameterPlaceholder, EscapeCharacter); + action(temp); + ctx.Bindings.AddRange(temp.Bindings); + return temp.Buffer.ToString(); } } } diff --git a/QueryBuilder/Compilers/Compiler.cs b/QueryBuilder/Compilers/Compiler.cs index b9b783ae..9a5394d3 100644 --- a/QueryBuilder/Compilers/Compiler.cs +++ b/QueryBuilder/Compilers/Compiler.cs @@ -16,11 +16,9 @@ public partial class Compiler protected virtual string LastId { get; set; } = ""; protected virtual string EscapeCharacter { get; set; } = "\\"; - protected virtual string SingleInsertStartClause { get; set; } = "INSERT INTO"; protected virtual string MultiInsertStartClause { get; set; } = "INSERT INTO"; - protected Compiler() { } @@ -30,23 +28,20 @@ protected Compiler() /// /// Whether the compiler supports the `SELECT ... FILTER` syntax /// - /// public virtual bool SupportsFilterClause { get; set; } = false; /// /// If true the compiler will remove the SELECT clause for the query used inside WHERE EXISTS /// - /// public virtual bool OmitSelectInsideExists { get; set; } = true; - protected virtual string SingleRowDummyTableName { get => null; } + protected virtual string SingleRowDummyTableName => null; /// /// A list of white-listed operators /// - /// - protected readonly HashSet operators = new HashSet - { + protected readonly HashSet operators = + [ "=", "<", ">", "<=", ">=", "<>", "!=", "<=>", "like", "not like", "ilike", "not ilike", @@ -54,12 +49,9 @@ protected Compiler() "rlike", "not rlike", "regexp", "not regexp", "similar to", "not similar to" - }; + ]; - protected HashSet userOperators = new HashSet - { - - }; + protected HashSet userOperators = []; protected Dictionary generateNamedBindings(object[] bindings) { @@ -67,23 +59,23 @@ protected Dictionary generateNamedBindings(object[] bindings) .ToDictionary(x => parameterPrefix + x.i, x => x.v); } - protected SqlResult PrepareResult(SqlResult ctx) + protected SqlResult PrepareResult(CompileContext ctx) { - ctx.NamedBindings = generateNamedBindings(ctx.Bindings.ToArray()); - ctx.Sql = Helper.ReplaceAll(ctx.RawSql, parameterPlaceholder, EscapeCharacter, i => parameterPrefix + i); - return ctx; + return new SqlResult(parameterPlaceholder, EscapeCharacter) + { + Query = ctx.Query, + RawSql = ctx.RawSql, + Bindings = ctx.Bindings, + NamedBindings = generateNamedBindings(ctx.Bindings.ToArray()), + Sql = Helper.ReplaceAll(ctx.RawSql, parameterPlaceholder, EscapeCharacter, i => parameterPrefix + i) + }; } - private Query TransformAggregateQuery(Query query) { var clause = query.AggregateClauses.ForEngineOrDefault(EngineCode); // Case 1: Simple single-column aggregate without DISTINCT. - // Example: query.From("users").AsCount("id") - // Output: SELECT COUNT("id") FROM "users" - // No subquery is required; standard SQL natively supports single-column aggregates. - // Limits, orders, and group-by clauses are stripped because they do not apply to a scalar aggregate. if (clause.Columns.Count == 1 && !query.IsDistinct) { var single = query.Clone(); @@ -94,7 +86,6 @@ private Query TransformAggregateQuery(Query query) } // Case 2 & 3: Multi-column aggregates or DISTINCT aggregates require wrapping the query in a subquery: - // Output template: SELECT (*) FROM () AS Query var transformed = query.Clone(); transformed.LimitClauses.Clear(); transformed.OrderByClauses.Clear(); @@ -102,24 +93,12 @@ private Query TransformAggregateQuery(Query query) if (transformed.IsDistinct) { - // Case 2: DISTINCT aggregate. - // Example: query.From("users").Distinct().AsCount("first_name", "last_name") - // SQL dialects generally do not allow multi-column COUNT(DISTINCT a, b). - // Instead, the inner query projects the distinct columns: - // Inner: SELECT DISTINCT "first_name", "last_name" FROM "users" - // Outer: SELECT COUNT(*) FROM (SELECT DISTINCT "first_name", "last_name" FROM "users") AS countQuery transformed.AggregateClauses.ClearForEngine(EngineCode); transformed.SelectClauses.ClearForEngine(EngineCode); transformed.Select(clause.Columns.ToArray()); } else { - // Case 3: Multi-column aggregate without DISTINCT. - // Example: query.From("users").AsCount("first_name", "last_name") - // In standard SQL, COUNT(a, b) is invalid syntax because aggregates take a single expression or '*'. - // In SQL aggregate semantics, counting multiple columns means counting rows where none of those columns are NULL: - // Inner: SELECT * FROM "users" WHERE "first_name" IS NOT NULL AND "last_name" IS NOT NULL - // Outer: SELECT COUNT(*) FROM (SELECT * FROM "users" WHERE "first_name" IS NOT NULL AND "last_name" IS NOT NULL) AS countQuery foreach (var column in clause.Columns) { transformed.WhereNotNull(column); @@ -139,49 +118,17 @@ private Query TransformAggregateQuery(Query query) return outerQuery; } - protected virtual SqlResult CompileRaw(Query query) + protected virtual CompileContext CompileRaw(Query query) { - QueryStack.Push(query); - try - { - SqlResult ctx; - - if (query.Method == "insert") - { - ctx = CompileInsertQuery(query); - } - else if (query.Method == "update") - { - ctx = CompileUpdateQuery(query); - } - else if (query.Method == "delete") - { - ctx = CompileDeleteQuery(query); - } - else - { - if (query.Method == "aggregate") - { - query = TransformAggregateQuery(query); - } - - ctx = CompileSelectQuery(query); - } - - // handle CTEs - if (query.CteClauses.ForEngine(EngineCode).Any()) - { - ctx = CompileCteQuery(ctx, query); - } - - ctx.RawSql = Helper.ExpandParameters(ctx.RawSql, parameterPlaceholder, EscapeCharacter, ctx.Bindings.ToArray()); + var ctx = new CompileContext(query, parameterPlaceholder, EscapeCharacter); + return CompileRaw(ctx); + } - return ctx; - } - finally - { - QueryStack.Pop(); - } + protected virtual CompileContext CompileRaw(CompileContext ctx) + { + VisitQuery(ctx, ctx.Query); + ctx.RawSql = Helper.ExpandParameters(ctx.Buffer.ToString(), parameterPlaceholder, EscapeCharacter, ctx.Bindings.ToArray()); + return ctx; } /// @@ -205,9 +152,7 @@ public virtual SqlResult Compile(Query query) { var ctx = CompileRaw(query); - ctx = PrepareResult(ctx); - - return ctx; + return PrepareResult(ctx); } public virtual SqlResult Compile(IEnumerable queries) @@ -222,109 +167,79 @@ public virtual SqlResult Compile(IEnumerable queries) combinedBindings.AddRange(cb); } - var ctx = new SqlResult(parameterPlaceholder, EscapeCharacter) + var ctx = new CompileContext(null, parameterPlaceholder, EscapeCharacter) { RawSql = compiled.Select(r => r.RawSql).Aggregate((a, b) => a + ";\n" + b), Bindings = combinedBindings, }; - ctx = PrepareResult(ctx); - - return ctx; - } - - [ThreadStatic] - private static Stack _queryStack; - - protected static Stack QueryStack => _queryStack ??= new Stack(); - - public virtual object FindVariable(string name) - { - if (_queryStack != null) - { - foreach (var query in _queryStack) - { - if (query?.Variables != null && query.Variables.TryGetValue(name, out var value)) - { - return value; - } - } - } - - throw new Exception($"Variable '{name}' not found"); + return PrepareResult(ctx); } - protected virtual SqlResult CompileSubQuery(SqlResult parentCtx, Query query) + public virtual void CompileSubQuery(CompileContext ctx, Query query) { - QueryStack.Push(query); + ctx.PushQuery(query); try { - return CompileSelectQuery(query); + VisitSelectQuery(ctx, query); } finally { - QueryStack.Pop(); + ctx.PopQuery(); } } - protected virtual SqlResult CompileSelectQuery(Query query) + // ========================================== + // Visitor Methods: Queries / Statements + // ========================================== + + public virtual void VisitQuery(CompileContext ctx, Query query) { - var ctx = new SqlResult(parameterPlaceholder, EscapeCharacter) + if (query.CteClauses.ForEngine(EngineCode).Any()) { - Query = query, - }; + VisitCteQuery(ctx, query); + } - var results = new[] { - this.CompileColumns(ctx), - this.CompileFrom(ctx), - this.CompileJoins(ctx), - this.CompileWheres(ctx), - this.CompileGroups(ctx), - this.CompileHaving(ctx), - this.CompileOrders(ctx), - this.CompileLimit(ctx), - this.CompileUnion(ctx), + if (query.Method == "insert") + { + VisitInsertQuery(ctx, query); + } + else if (query.Method == "update") + { + VisitUpdateQuery(ctx, query); + } + else if (query.Method == "delete") + { + VisitDeleteQuery(ctx, query); + } + else + { + if (query.Method == "aggregate") + { + query = TransformAggregateQuery(query); + ctx.Query = query; } - .Where(x => x != null) - .Where(x => !string.IsNullOrEmpty(x)) - .ToList(); - - string sql = string.Join(" ", results); - - ctx.RawSql = sql; - return ctx; + VisitSelectQuery(ctx, query); + } } - protected virtual SqlResult CompileAdHocQuery(AdHocTableFromClause adHoc) + public virtual void VisitSelectQuery(CompileContext ctx, Query query) { - var ctx = new SqlResult(parameterPlaceholder, EscapeCharacter); - - var row = "SELECT " + string.Join(", ", adHoc.Columns.Select(col => $"{parameterPlaceholder} AS {Wrap(col)}")); - - var fromTable = SingleRowDummyTableName; - - if (fromTable != null) - { - row += $" FROM {fromTable}"; - } - - var rows = string.Join(" UNION ALL ", Enumerable.Repeat(row, adHoc.Values.Count / adHoc.Columns.Count)); - - ctx.RawSql = rows; - ctx.Bindings = adHoc.Values; - - return ctx; + VisitColumns(ctx, query); + VisitFrom(ctx, query); + VisitJoins(ctx, query); + VisitWheres(ctx, query); + VisitGroups(ctx, query); + VisitHaving(ctx, query); + VisitOrders(ctx, query); + VisitLimit(ctx, query); + VisitUnions(ctx, query); } - protected virtual SqlResult CompileDeleteQuery(Query query) + public virtual void VisitDeleteQuery(CompileContext ctx, Query query) { - var ctx = new SqlResult(parameterPlaceholder, EscapeCharacter) - { - Query = query - }; - - var fromClause = ctx.Query.FromClauses.ForEngineOrDefault(EngineCode); + var fromClause = query.FromClauses.ForEngineOrDefault(EngineCode); if (fromClause is null) { throw new InvalidOperationException("No table set to delete"); @@ -348,44 +263,31 @@ protected virtual SqlResult CompileDeleteQuery(Query query) throw new InvalidOperationException("Invalid table expression"); } - var joins = CompileJoins(ctx); + var hasJoins = query.Joins.ForEngine(EngineCode).Any(); - var where = CompileWheres(ctx); - - if (!string.IsNullOrEmpty(where)) + if (!hasJoins) { - where = " " + where; + ctx.Write($"DELETE FROM {table}"); + VisitWheres(ctx, query); + return; } - if (string.IsNullOrEmpty(joins)) + if (fromClause is FromClause && !string.IsNullOrEmpty(fromClause.Alias)) { - ctx.RawSql = $"DELETE FROM {table}{where}"; + ctx.Write($"DELETE {Wrap(fromClause.Alias)} FROM {table}"); } else { - // check if we have alias - if (fromClause is FromClause && !string.IsNullOrEmpty(fromClause.Alias)) - { - ctx.RawSql = $"DELETE {Wrap(fromClause.Alias)} FROM {table} {joins}{where}"; - } - else - { - ctx.RawSql = $"DELETE {table} FROM {table} {joins}{where}"; - } - + ctx.Write($"DELETE {table} FROM {table}"); } - return ctx; + VisitJoins(ctx, query); + VisitWheres(ctx, query); } - protected virtual SqlResult CompileUpdateQuery(Query query) + public virtual void VisitUpdateQuery(CompileContext ctx, Query query) { - var ctx = new SqlResult(parameterPlaceholder, EscapeCharacter) - { - Query = query - }; - - var fromClause = ctx.Query.FromClauses.ForEngineOrDefault(EngineCode); + var fromClause = query.FromClauses.ForEngineOrDefault(EngineCode); if (fromClause is null) { throw new InvalidOperationException("No table set to update"); @@ -409,10 +311,7 @@ protected virtual SqlResult CompileUpdateQuery(Query query) throw new InvalidOperationException("Invalid table expression"); } - // check for increment statements - var clause = ctx.Query.UpdateClauses.ForEngineOrDefault(EngineCode); - - string wheres; + var clause = query.UpdateClauses.ForEngineOrDefault(EngineCode); if (clause != null && clause is IncrementClause increment) { @@ -420,19 +319,11 @@ protected virtual SqlResult CompileUpdateQuery(Query query) var value = Parameter(ctx, Math.Abs(increment.Value)); var sign = increment.Value >= 0 ? "+" : "-"; - wheres = CompileWheres(ctx); - - if (!string.IsNullOrEmpty(wheres)) - { - wheres = " " + wheres; - } - - ctx.RawSql = $"UPDATE {table} SET {column} = {column} {sign} {value}{wheres}"; - - return ctx; + ctx.Write($"UPDATE {table} SET {column} = {column} {sign} {value}"); + VisitWheres(ctx, query); + return; } - var toUpdate = clause as InsertClause; var parts = new List(); @@ -443,26 +334,13 @@ protected virtual SqlResult CompileUpdateQuery(Query query) var sets = string.Join(", ", parts); - wheres = CompileWheres(ctx); - - if (!string.IsNullOrEmpty(wheres)) - { - wheres = " " + wheres; - } - - ctx.RawSql = $"UPDATE {table} SET {sets}{wheres}"; - - return ctx; + ctx.Write($"UPDATE {table} SET {sets}"); + VisitWheres(ctx, query); } - protected virtual SqlResult CompileInsertQuery(Query query) + public virtual void VisitInsertQuery(CompileContext ctx, Query query) { - var ctx = new SqlResult(parameterPlaceholder, EscapeCharacter) - { - Query = query - }; - - var fromClause = ctx.Query.FromClauses.ForEngineOrDefault(EngineCode); + var fromClause = query.FromClauses.ForEngineOrDefault(EngineCode); if (fromClause is null) throw new InvalidOperationException("No table set to insert"); @@ -478,480 +356,542 @@ protected virtual SqlResult CompileInsertQuery(Query query) if (table is null) throw new InvalidOperationException("Invalid table expression"); - var inserts = ctx.Query.InsertClauses.ForEngine(EngineCode).ToList(); + var inserts = query.InsertClauses.ForEngine(EngineCode).ToList(); if (inserts[0] is InsertQueryClause insertQueryClause) - return CompileInsertQueryClause(ctx, table, insertQueryClause); + VisitInsertQueryClause(ctx, table, insertQueryClause); else - return CompileValueInsertClauses(ctx, table, inserts.Cast()); + VisitValueInsertClauses(ctx, table, inserts.Cast()); } - protected virtual SqlResult CompileInsertQueryClause( - SqlResult ctx, string table, InsertQueryClause clause) + public virtual void VisitInsertQueryClause( + CompileContext ctx, string table, InsertQueryClause clause) { string columns = GetInsertColumnsList(clause.Columns); - var subCtx = CompileSubQuery(ctx, clause.Query); - ctx.Bindings.AddRange(subCtx.Bindings); - - ctx.RawSql = $"{SingleInsertStartClause} {table}{columns} {subCtx.RawSql}"; - - return ctx; + ctx.Write($"{SingleInsertStartClause} {table}{columns} "); + CompileSubQuery(ctx, clause.Query); } - protected virtual SqlResult CompileValueInsertClauses( - SqlResult ctx, string table, IEnumerable insertClauses) + public virtual void VisitValueInsertClauses( + CompileContext ctx, string table, IEnumerable insertClauses) { - bool isMultiValueInsert = insertClauses.Skip(1).Any(); + var list = insertClauses.ToList(); + bool isMultiValueInsert = list.Skip(1).Any(); - var insertInto = (isMultiValueInsert) ? MultiInsertStartClause : SingleInsertStartClause; + var insertInto = isMultiValueInsert ? MultiInsertStartClause : SingleInsertStartClause; - var firstInsert = insertClauses.First(); + var firstInsert = list.First(); string columns = GetInsertColumnsList(firstInsert.Columns); var values = string.Join(", ", Parameterize(ctx, firstInsert.Values)); - ctx.RawSql = $"{insertInto} {table}{columns} VALUES ({values})"; + ctx.Write($"{insertInto} {table}{columns} VALUES ({values})"); if (isMultiValueInsert) - return CompileRemainingInsertClauses(ctx, table, insertClauses); + { + VisitRemainingInsertClauses(ctx, table, list); + return; + } if (firstInsert.ReturnId && !string.IsNullOrEmpty(LastId)) - ctx.RawSql += ";" + LastId; - - return ctx; + { + ctx.Write(";" + LastId); + } } - protected virtual SqlResult CompileRemainingInsertClauses(SqlResult ctx, string table, IEnumerable inserts) + public virtual void VisitRemainingInsertClauses(CompileContext ctx, string table, IEnumerable inserts) { foreach (var insert in inserts.Skip(1)) { string values = string.Join(", ", Parameterize(ctx, insert.Values)); - ctx.RawSql += $", ({values})"; + ctx.Write($", ({values})"); } - return ctx; - } - - protected string GetInsertColumnsList(List columnList) - { - var columns = ""; - if (columnList.Any()) - columns = $" ({string.Join(", ", WrapArray(columnList))})"; - - return columns; } - protected virtual SqlResult CompileCteQuery(SqlResult ctx, Query query) + public virtual void VisitCteQuery(CompileContext ctx, Query query) { var cteFinder = new CteFinder(query, EngineCode); var cteSearchResult = cteFinder.Find(); - var rawSql = new StringBuilder("WITH "); - var cteBindings = new List(); + ctx.Write("WITH "); - foreach (var cte in cteSearchResult) + for (var i = 0; i < cteSearchResult.Count; i++) { - var cteCtx = CompileCte(cte); - - cteBindings.AddRange(cteCtx.Bindings); - rawSql.Append(cteCtx.RawSql.Trim()); - rawSql.Append(",\n"); - } - - rawSql.Length -= 2; // remove last comma - rawSql.Append('\n'); - rawSql.Append(ctx.RawSql); - - ctx.Bindings.InsertRange(0, cteBindings); - ctx.RawSql = rawSql.ToString(); - - return ctx; - } - - /// - /// Compile a single column clause - /// - /// - /// - /// - public virtual string CompileColumn(SqlResult ctx, AbstractColumn column) - { - if (column is RawColumn raw) - { - ctx.Bindings.AddRange(raw.Bindings); - return WrapIdentifiers(raw.Expression); - } - - if (column is QueryColumn queryColumn) - { - var alias = ""; - - if (!string.IsNullOrWhiteSpace(queryColumn.Query.QueryAlias)) + if (i > 0) { - alias = $" {ColumnAsKeyword}{WrapValue(queryColumn.Query.QueryAlias)}"; + ctx.Write(",\n"); } - - var subCtx = CompileSubQuery(ctx, queryColumn.Query); - - ctx.Bindings.AddRange(subCtx.Bindings); - - return "(" + subCtx.RawSql + $"){alias}"; + VisitCte(ctx, cteSearchResult[i]); } - if (column is AggregatedColumn aggregatedColumn) - { - string agg = aggregatedColumn.Aggregate.ToUpperInvariant(); - - var (col, alias) = SplitAlias(CompileColumn(ctx, aggregatedColumn.Column)); - - alias = string.IsNullOrEmpty(alias) ? "" : $" {ColumnAsKeyword}{alias}"; - - string filterCondition = CompileFilterConditions(ctx, aggregatedColumn); - - if (string.IsNullOrEmpty(filterCondition)) - { - return $"{agg}({col}){alias}"; - } - - if (SupportsFilterClause) - { - return $"{agg}({col}) FILTER (WHERE {filterCondition}){alias}"; - } - - return $"{agg}(CASE WHEN {filterCondition} THEN {col} END){alias}"; - } - - return Wrap((column as Column).Name); - - } - - protected virtual string CompileFilterConditions(SqlResult ctx, AggregatedColumn aggregatedColumn) - { - if (aggregatedColumn.Filter == null) - { - return null; - } - - var wheres = aggregatedColumn.Filter.Wheres.Clauses - .ForEngine(EngineCode) - .Cast() - .ToList(); - - return CompileConditions(ctx, wheres); + ctx.Write("\n"); } - public virtual SqlResult CompileCte(AbstractFrom cte) + public virtual void VisitCte(CompileContext ctx, AbstractFrom cte) { - var ctx = new SqlResult(parameterPlaceholder, EscapeCharacter); - - if (null == cte) - { - return ctx; - } + if (cte == null) return; if (cte is RawFromClause raw) { ctx.Bindings.AddRange(raw.Bindings); - ctx.RawSql = $"{WrapValue(raw.Alias)} AS ({WrapIdentifiers(raw.Expression)})"; + ctx.Write($"{WrapValue(raw.Alias)} AS ({WrapIdentifiers(raw.Expression)})"); } else if (cte is QueryFromClause queryFromClause) { - var subCtx = CompileSubQuery(ctx, queryFromClause.Query); - ctx.Bindings.AddRange(subCtx.Bindings); - - ctx.RawSql = $"{WrapValue(queryFromClause.Alias)} AS ({subCtx.RawSql})"; + ctx.Write($"{WrapValue(queryFromClause.Alias)} AS ("); + CompileSubQuery(ctx, queryFromClause.Query); + ctx.Write(")"); } else if (cte is AdHocTableFromClause adHoc) { - var subCtx = CompileAdHocQuery(adHoc); - ctx.Bindings.AddRange(subCtx.Bindings); - - ctx.RawSql = $"{WrapValue(adHoc.Alias)} AS ({subCtx.RawSql})"; + ctx.Write($"{WrapValue(adHoc.Alias)} AS ("); + VisitAdHocTableFromClause(ctx, adHoc); + ctx.Write(")"); } - - return ctx; } - protected virtual SqlResult OnBeforeSelect(SqlResult ctx) + // ========================================== + // Visitor Methods: Select Clauses & Elements + // ========================================== + + public virtual void VisitColumns(CompileContext ctx, Query query) { - return ctx; + VisitSelectPrefix(ctx, query); + VisitColumnExpressions(ctx, query); } - protected virtual string CompileSelectPrefix(SqlResult ctx) + public virtual void VisitSelectPrefix(CompileContext ctx, Query query) { - var aggregate = ctx.Query.AggregateClauses.ForEngineOrDefault(EngineCode); + var aggregate = query.AggregateClauses.ForEngineOrDefault(EngineCode); if (aggregate != null) { - return "SELECT "; + ctx.Write("SELECT "); + return; } - var distinct = ctx.Query.IsDistinct ? "DISTINCT " : ""; - return $"SELECT {distinct}"; + var distinct = query.IsDistinct ? "DISTINCT " : ""; + ctx.Write($"SELECT {distinct}"); } - protected virtual string CompileColumnExpressions(SqlResult ctx) + public virtual void VisitColumnExpressions(CompileContext ctx, Query query) { - var aggregate = ctx.Query.AggregateClauses.ForEngineOrDefault(EngineCode); + var aggregate = query.AggregateClauses.ForEngineOrDefault(EngineCode); if (aggregate != null) { - var aggregateColumns = aggregate.Columns - .Select(x => CompileColumn(ctx, new Column { Name = x })) - .ToList(); - - if (aggregateColumns.Count == 1) + if (aggregate.Columns.Count == 1) { - var sql = string.Join(", ", aggregateColumns); - - if (ctx.Query.IsDistinct) + ctx.Write(aggregate.Type.ToUpperInvariant()).Write("("); + if (query.IsDistinct) { - sql = "DISTINCT " + sql; + ctx.Write("DISTINCT "); } - - return aggregate.Type.ToUpperInvariant() + "(" + sql + $") {ColumnAsKeyword}" + WrapValue(aggregate.Type); + VisitColumn(ctx, new Column { Name = aggregate.Columns[0] }); + ctx.Write($") {ColumnAsKeyword}").Write(WrapValue(aggregate.Type)); + return; } - return "1"; + ctx.Write("1"); + return; } - var columns = ctx.Query.SelectClauses - .ForEngine(EngineCode) - .Select(x => CompileColumn(ctx, x)) - .ToList(); + var selectClauses = query.SelectClauses.ForEngine(EngineCode).ToList(); + if (!selectClauses.Any()) + { + ctx.Write("*"); + return; + } - return columns.Any() ? string.Join(", ", columns) : "*"; + for (var i = 0; i < selectClauses.Count; i++) + { + if (i > 0) ctx.Write(", "); + VisitColumn(ctx, selectClauses[i]); + } } - protected virtual string CompileColumns(SqlResult ctx) + public virtual void VisitColumn(CompileContext ctx, AbstractColumn column) { - return $"{CompileSelectPrefix(ctx)}{CompileColumnExpressions(ctx)}"; + switch (column) + { + case RawColumn raw: + VisitRawColumn(ctx, raw); + break; + case QueryColumn queryColumn: + VisitQueryColumn(ctx, queryColumn); + break; + case AggregatedColumn aggregatedColumn: + VisitAggregatedColumn(ctx, aggregatedColumn); + break; + case Column col: + VisitBasicColumn(ctx, col); + break; + default: + throw InvalidClauseException("Column", column); + } } - public virtual string CompileUnion(SqlResult ctx) + public virtual void VisitBasicColumn(CompileContext ctx, Column column) { + ctx.Write(Wrap(column.Name)); + } - // Handle UNION, EXCEPT and INTERSECT - var clauses = ctx.Query.CombineClauses.ForEngine(EngineCode).ToList(); - if (!clauses.Any()) - { - return null; - } + public virtual void VisitRawColumn(CompileContext ctx, RawColumn raw) + { + ctx.Bindings.AddRange(raw.Bindings); + ctx.Write(WrapIdentifiers(raw.Expression)); + } - var combinedQueries = new List(); + public virtual void VisitQueryColumn(CompileContext ctx, QueryColumn queryColumn) + { + ctx.Write("("); + CompileSubQuery(ctx, queryColumn.Query); + ctx.Write(")"); - foreach (var clause in clauses) + if (!string.IsNullOrWhiteSpace(queryColumn.Query.QueryAlias)) { - if (clause is Combine combineClause) - { - var combineOperator = combineClause.Operation.ToUpperInvariant() + " " + (combineClause.All ? "ALL " : ""); - - var subCtx = CompileSubQuery(ctx, combineClause.Query); + ctx.Write($" {ColumnAsKeyword}").Write(WrapValue(queryColumn.Query.QueryAlias)); + } + } - ctx.Bindings.AddRange(subCtx.Bindings); + public virtual void VisitAggregatedColumn(CompileContext ctx, AggregatedColumn aggregatedColumn) + { + string agg = aggregatedColumn.Aggregate.ToUpperInvariant(); - combinedQueries.Add($"{combineOperator}{subCtx.RawSql}"); - } - else - { - var combineRawClause = clause as RawCombine; + var tempCtx = new CompileContext(ctx.CurrentQuery, parameterPlaceholder, EscapeCharacter); + VisitColumn(tempCtx, aggregatedColumn.Column); + ctx.Bindings.AddRange(tempCtx.Bindings); - ctx.Bindings.AddRange(combineRawClause.Bindings); + var (col, alias) = SplitAlias(tempCtx.Buffer.ToString()); + alias = string.IsNullOrEmpty(alias) ? "" : $" {ColumnAsKeyword}{alias}"; - combinedQueries.Add(WrapIdentifiers(combineRawClause.Expression)); + var filterCtx = new CompileContext(ctx.CurrentQuery, parameterPlaceholder, EscapeCharacter); + VisitFilterConditions(filterCtx, aggregatedColumn); - } + if (filterCtx.Buffer.Length == 0) + { + ctx.Write($"{agg}({col}){alias}"); + return; } - return string.Join(" ", combinedQueries); + ctx.Bindings.AddRange(filterCtx.Bindings); + if (SupportsFilterClause) + { + ctx.Write($"{agg}({col}) FILTER (WHERE {filterCtx.Buffer}){alias}"); + } + else + { + ctx.Write($"{agg}(CASE WHEN {filterCtx.Buffer} THEN {col} END){alias}"); + } } - public virtual string CompileTableExpression(SqlResult ctx, AbstractFrom from) + public virtual void VisitFilterConditions(CompileContext ctx, AggregatedColumn aggregatedColumn) { - if (from is RawFromClause raw) + if (aggregatedColumn.Filter == null) { - ctx.Bindings.AddRange(raw.Bindings); - return WrapIdentifiers(raw.Expression); + return; } - if (from is QueryFromClause queryFromClause) - { - var fromQuery = queryFromClause.Query; - - var alias = string.IsNullOrEmpty(fromQuery.QueryAlias) ? "" : $" {TableAsKeyword}" + WrapValue(fromQuery.QueryAlias); - - var subCtx = CompileSubQuery(ctx, fromQuery); + var wheres = aggregatedColumn.Filter.Wheres.Clauses + .ForEngine(EngineCode) + .Cast() + .ToList(); - ctx.Bindings.AddRange(subCtx.Bindings); + VisitConditions(ctx, wheres); + } - return "(" + subCtx.RawSql + ")" + alias; + public virtual void VisitFrom(CompileContext ctx, Query query) + { + var from = query.FromClauses.ForEngineOrDefault(EngineCode); + if (from != null) + { + ctx.EnsureSpace().Write("FROM "); + VisitTableExpression(ctx, from); } + } - if (from is FromClause fromClause) - { - return Wrap(fromClause.Table); + public virtual void VisitTableExpression(CompileContext ctx, AbstractFrom from) + { + switch (from) + { + case RawFromClause raw: + VisitRawFromClause(ctx, raw); + break; + case QueryFromClause queryFromClause: + VisitQueryFromClause(ctx, queryFromClause); + break; + case FromClause fromClause: + VisitFromClause(ctx, fromClause); + break; + case AdHocTableFromClause adHoc: + VisitAdHocTableFromClause(ctx, adHoc); + break; + default: + throw InvalidClauseException("TableExpression", from); } + } - throw InvalidClauseException("TableExpression", from); + public virtual void VisitFromClause(CompileContext ctx, FromClause fromClause) + { + ctx.Write(Wrap(fromClause.Table)); } - public virtual string CompileFrom(SqlResult ctx) + public virtual void VisitRawFromClause(CompileContext ctx, RawFromClause raw) { - var from = ctx.Query.FromClauses.ForEngineOrDefault(EngineCode); - if (from != null) - { - return "FROM " + CompileTableExpression(ctx, from); - } + ctx.Bindings.AddRange(raw.Bindings); + ctx.Write(WrapIdentifiers(raw.Expression)); + } - return string.Empty; + public virtual void VisitQueryFromClause(CompileContext ctx, QueryFromClause queryFromClause) + { + var fromQuery = queryFromClause.Query; + var alias = string.IsNullOrEmpty(fromQuery.QueryAlias) ? "" : $" {TableAsKeyword}" + WrapValue(fromQuery.QueryAlias); + ctx.Write("("); + CompileSubQuery(ctx, fromQuery); + ctx.Write(")").Write(alias); } - public virtual string CompileJoins(SqlResult ctx) + public virtual void VisitAdHocTableFromClause(CompileContext ctx, AdHocTableFromClause adHoc) { - var joins = ctx.Query.Joins - .ForEngine(EngineCode) - .Select(x => CompileJoin(ctx, x.Join)) - .ToList(); + var row = "SELECT " + string.Join(", ", adHoc.Columns.Select(col => $"{parameterPlaceholder} AS {Wrap(col)}")); - if (!joins.Any()) + var fromTable = SingleRowDummyTableName; + if (fromTable != null) { - return null; + row += $" FROM {fromTable}"; } - return "\n" + string.Join("\n", joins); + var rows = string.Join(" UNION ALL ", Enumerable.Repeat(row, adHoc.Values.Count / adHoc.Columns.Count)); + ctx.Bindings.AddRange(adHoc.Values); + ctx.Write(rows); } - public virtual string CompileJoin(SqlResult ctx, Join join, bool isNested = false) + public virtual void VisitJoins(CompileContext ctx, Query query) { + var joins = query.Joins.ForEngine(EngineCode).ToList(); + if (!joins.Any()) return; + + ctx.EnsureSpace(); + for (var i = 0; i < joins.Count; i++) + { + if (i > 0) + { + ctx.Write("\n"); + } + else if (ctx.Buffer.Length > 0 && ctx.Buffer[ctx.Buffer.Length - 1] == ' ') + { + ctx.Write("\n"); + } + VisitJoin(ctx, joins[i].Join); + } + } + public virtual void VisitJoin(CompileContext ctx, Join join, bool isNested = false) + { var from = join.FromClause; var conditions = join.Conditions.GetConditions(EngineCode); - var joinTable = CompileTableExpression(ctx, from); - var constraints = CompileConditions(ctx, conditions); - - var onClause = conditions.Any() ? $" ON {constraints}" : ""; + ctx.Write(join.Type).Write(" "); + VisitTableExpression(ctx, from); - return $"{join.Type} {joinTable}{onClause}"; + if (conditions.Any()) + { + ctx.Write(" ON "); + VisitConditions(ctx, conditions); + } } - public virtual string CompileWheres(SqlResult ctx) + public virtual void VisitWheres(CompileContext ctx, Query query) { - if (!ctx.Query.Wheres.HasConditions(EngineCode)) + if (!query.Wheres.HasConditions(EngineCode)) { - return null; + return; } - var conditions = ctx.Query.Wheres.Clauses + ctx.EnsureSpace(); + var beforeWhere = ctx.Buffer.Length; + ctx.Write("WHERE "); + var beforeConds = ctx.Buffer.Length; + + var conditions = query.Wheres.Clauses .ForEngine(EngineCode) .Cast() .ToList(); - var sql = CompileConditions(ctx, conditions).Trim(); - return string.IsNullOrEmpty(sql) ? null : $"WHERE {sql}"; + VisitConditions(ctx, conditions); + + if (ctx.Buffer.Length == beforeConds) + { + ctx.Buffer.Length = beforeWhere; + } } - public virtual string CompileGroups(SqlResult ctx) + public virtual void VisitGroups(CompileContext ctx, Query query) { - var columns = ctx.Query.GroupByClauses - .ForEngine(EngineCode) - .Select(x => CompileColumn(ctx, x)) - .ToList(); - + var columns = query.GroupByClauses.ForEngine(EngineCode).ToList(); if (!columns.Any()) { - return null; + return; } - return "GROUP BY " + string.Join(", ", columns); + ctx.EnsureSpace().Write("GROUP BY "); + for (var i = 0; i < columns.Count; i++) + { + if (i > 0) ctx.Write(", "); + VisitColumn(ctx, columns[i]); + } } - public virtual string CompileOrders(SqlResult ctx) + public virtual void VisitOrders(CompileContext ctx, Query query) { - var columns = ctx.Query.OrderByClauses - .ForEngine(EngineCode) - .Select(x => + var columns = query.OrderByClauses.ForEngine(EngineCode).ToList(); + if (!columns.Any()) { + return; + } + ctx.EnsureSpace().Write("ORDER BY "); + for (var i = 0; i < columns.Count; i++) + { + if (i > 0) ctx.Write(", "); + var x = columns[i]; if (x is RawOrderBy raw) { ctx.Bindings.AddRange(raw.Bindings); - return WrapIdentifiers(raw.Expression); + ctx.Write(WrapIdentifiers(raw.Expression)); + } + else if (x is OrderBy orderBy) + { + var direction = orderBy.Ascending ? "" : " DESC"; + ctx.Write(Wrap(orderBy.Column)).Write(direction); } - - var direction = (x as OrderBy).Ascending ? "" : " DESC"; - - return Wrap((x as OrderBy).Column) + direction; - }).ToList(); - - if (!columns.Any()) - { - return null; } - - return "ORDER BY " + string.Join(", ", columns); } - public virtual string CompileHaving(SqlResult ctx) + public virtual void VisitHaving(CompileContext ctx, Query query) { - if (!ctx.Query.Havings.HasConditions(EngineCode)) + if (!query.Havings.HasConditions(EngineCode)) { - return null; + return; } - var sql = new List(); - string boolOperator; + ctx.EnsureSpace(); + var beforeHaving = ctx.Buffer.Length; + ctx.Write("HAVING "); + var beforeConds = ctx.Buffer.Length; - var having = ctx.Query.Havings.Clauses + var having = query.Havings.Clauses .ForEngine(EngineCode) .Cast() .ToList(); - for (var i = 0; i < having.Count; i++) - { - var compiled = CompileCondition(ctx, having[i]); + VisitConditions(ctx, having); - if (!string.IsNullOrEmpty(compiled)) - { - boolOperator = i > 0 ? having[i].IsOr ? "OR " : "AND " : ""; - - sql.Add(boolOperator + compiled); - } + if (ctx.Buffer.Length == beforeConds) + { + ctx.Buffer.Length = beforeHaving; } - - return $"HAVING {string.Join(" ", sql)}"; } - public virtual string CompileLimit(SqlResult ctx) + public virtual void VisitLimit(CompileContext ctx, Query query) { - var limit = ctx.Query.GetLimit(EngineCode); - var offset = ctx.Query.GetOffset(EngineCode); + var limit = query.GetLimit(EngineCode); + var offset = query.GetOffset(EngineCode); if (limit == 0 && offset == 0) { - return null; + return; } + ctx.EnsureSpace(); + if (offset == 0) { ctx.Bindings.Add(limit); - return $"LIMIT {parameterPlaceholder}"; + ctx.Write($"LIMIT {parameterPlaceholder}"); + return; } if (limit == 0) { ctx.Bindings.Add(offset); - return $"OFFSET {parameterPlaceholder}"; + ctx.Write($"OFFSET {parameterPlaceholder}"); + return; } ctx.Bindings.Add(limit); ctx.Bindings.Add(offset); + ctx.Write($"LIMIT {parameterPlaceholder} OFFSET {parameterPlaceholder}"); + } - return $"LIMIT {parameterPlaceholder} OFFSET {parameterPlaceholder}"; + public virtual void VisitUnions(CompileContext ctx, Query query) + { + var clauses = query.CombineClauses.ForEngine(EngineCode).ToList(); + if (!clauses.Any()) + { + return; + } + + foreach (var clause in clauses) + { + ctx.EnsureSpace(); + if (clause is Combine combineClause) + { + var combineOperator = combineClause.Operation.ToUpperInvariant() + " " + (combineClause.All ? "ALL " : ""); + ctx.Write(combineOperator); + CompileSubQuery(ctx, combineClause.Query); + } + else if (clause is RawCombine rawCombine) + { + ctx.Bindings.AddRange(rawCombine.Bindings); + ctx.Write(WrapIdentifiers(rawCombine.Expression)); + } + } + } + + // ========================================== + // Backward Compatibility Wrappers + // ========================================== + + protected virtual CompileContext OnBeforeSelect(CompileContext ctx) => ctx; + + public virtual string CompileLimit(CompileContext ctx) + { + var prevLen = ctx.Buffer.Length; + VisitLimit(ctx, ctx.Query); + if (ctx.Buffer.Length > prevLen) + { + var result = ctx.Buffer.ToString(prevLen, ctx.Buffer.Length - prevLen).Trim(); + ctx.Buffer.Length = prevLen; + return result; + } + return null; + } + + public virtual string CompileOrders(CompileContext ctx) + { + var prevLen = ctx.Buffer.Length; + VisitOrders(ctx, ctx.Query); + if (ctx.Buffer.Length > prevLen) + { + var result = ctx.Buffer.ToString(prevLen, ctx.Buffer.Length - prevLen).Trim(); + ctx.Buffer.Length = prevLen; + return result; + } + return null; + } + + // ========================================== + // Helper / Formatting Methods + // ========================================== + + protected string GetInsertColumnsList(List columnList) + { + var columns = ""; + if (columnList.Any()) + columns = $" ({string.Join(", ", WrapArray(columnList))})"; + + return columns; } - /// - /// Compile the random statement into SQL. - /// - /// - /// public virtual string CompileRandom(string seed) { return "RANDOM()"; @@ -996,14 +936,8 @@ protected string checkOperator(string op) return op; } - /// - /// Wrap a single string in a column identifier. - /// - /// - /// public virtual string Wrap(string value) { - if (value.ToLowerInvariant().Contains(" as ")) { var (before, after) = SplitAlias(value); @@ -1019,8 +953,6 @@ public virtual string Wrap(string value) })); } - // If we reach here then the value does not contain an "AS" alias - // nor dot "." expression, so wrap it as regular value. return WrapValue(value); } @@ -1038,11 +970,6 @@ public virtual (string, string) SplitAlias(string value) return (value, null); } - /// - /// Wrap a single string in keyword identifiers. - /// - /// - /// public virtual string WrapValue(string value) { if (value == "*") return value; @@ -1055,49 +982,32 @@ public virtual string WrapValue(string value) return opening + value.Replace(closing, closing + closing) + closing; } - /// - /// Resolve a parameter - /// - /// - /// - /// - public virtual object Resolve(SqlResult ctx, object parameter) + public virtual object Resolve(CompileContext ctx, object parameter) { - // if we face a literal value we have to return it directly if (parameter is UnsafeLiteral literal) { return literal.Value; } - // if we face a variable we have to lookup the variable from the predefined variables if (parameter is Variable variable) { - var value = FindVariable(variable.Name); + var value = ctx.FindVariable(variable.Name); return value; } return parameter; - } - /// - /// Resolve a parameter and add it to the binding list - /// - /// - /// - /// - public virtual string Parameter(SqlResult ctx, object parameter) + public virtual string Parameter(CompileContext ctx, object parameter) { - // if we face a literal value we have to return it directly if (parameter is UnsafeLiteral literal) { return literal.Value; } - // if we face a variable we have to lookup the variable from the predefined variables if (parameter is Variable variable) { - var value = FindVariable(variable.Name); + var value = ctx.FindVariable(variable.Name); ctx.Bindings.Add(value); return parameterPlaceholder; } @@ -1106,22 +1016,11 @@ public virtual string Parameter(SqlResult ctx, object parameter) return parameterPlaceholder; } - /// - /// Create query parameter place-holders for an array. - /// - /// - /// - /// - public virtual string Parameterize(SqlResult ctx, IEnumerable values) + public virtual string Parameterize(CompileContext ctx, IEnumerable values) { return string.Join(", ", values.Select(x => Parameter(ctx, x))); } - /// - /// Wrap an array of values. - /// - /// - /// public virtual List WrapArray(List values) { return values.Select(x => Wrap(x)).ToList(); @@ -1130,11 +1029,8 @@ public virtual List WrapArray(List values) public virtual string WrapIdentifiers(string input) { return input - - // deprecated .ReplaceIdentifierUnlessEscaped(this.EscapeCharacter, "{", this.OpeningIdentifier) .ReplaceIdentifierUnlessEscaped(this.EscapeCharacter, "}", this.ClosingIdentifier) - .ReplaceIdentifierUnlessEscaped(this.EscapeCharacter, "[", this.OpeningIdentifier) .ReplaceIdentifierUnlessEscaped(this.EscapeCharacter, "]", this.ClosingIdentifier); } diff --git a/QueryBuilder/Compilers/FirebirdCompiler.cs b/QueryBuilder/Compilers/FirebirdCompiler.cs index 7af02d99..988b4371 100644 --- a/QueryBuilder/Compilers/FirebirdCompiler.cs +++ b/QueryBuilder/Compilers/FirebirdCompiler.cs @@ -14,61 +14,64 @@ public FirebirdCompiler() public override string EngineCode { get; } = EngineCodes.Firebird; protected override string SingleRowDummyTableName => "RDB$DATABASE"; - protected override SqlResult CompileInsertQuery(Query query) + public override void VisitInsertQuery(CompileContext ctx, Query query) { - var ctx = base.CompileInsertQuery(query); + var before = ctx.Buffer.Length; + base.VisitInsertQuery(ctx, query); + var sql = ctx.Buffer.ToString(before, ctx.Buffer.Length - before); + ctx.Buffer.Length = before; - var inserts = ctx.Query.InsertClauses.ForEngine(EngineCode).ToList(); + var inserts = query.InsertClauses.ForEngine(EngineCode).ToList(); if (inserts.Count > 1) { - ctx.RawSql = Regex.Replace(ctx.RawSql, @"\)\s+VALUES\s+\(", ") SELECT "); - ctx.RawSql = Regex.Replace(ctx.RawSql, @"\),\s*\(", " FROM RDB$DATABASE UNION ALL SELECT "); - ctx.RawSql = Regex.Replace(ctx.RawSql, @"\)$", " FROM RDB$DATABASE"); + sql = Regex.Replace(sql, @"\)\s+VALUES\s+\(", ") SELECT "); + sql = Regex.Replace(sql, @"\),\s*\(", " FROM RDB$DATABASE UNION ALL SELECT "); + sql = Regex.Replace(sql, @"\)$", " FROM RDB$DATABASE"); } - return ctx; + ctx.Write(sql); } - public override string CompileLimit(SqlResult ctx) + public override void VisitLimit(CompileContext ctx, Query query) { - var limit = ctx.Query.GetLimit(EngineCode); - var offset = ctx.Query.GetOffset(EngineCode); + var limit = query.GetLimit(EngineCode); + var offset = query.GetOffset(EngineCode); if (limit > 0 && offset > 0) { + ctx.EnsureSpace(); ctx.Bindings.Add(offset + 1); ctx.Bindings.Add(limit + offset); - return $"ROWS {parameterPlaceholder} TO {parameterPlaceholder}"; + ctx.Write($"ROWS {parameterPlaceholder} TO {parameterPlaceholder}"); } - - return null; } - - protected override string CompileSelectPrefix(SqlResult ctx) + public override void VisitSelectPrefix(CompileContext ctx, Query query) { - var limit = ctx.Query.GetLimit(EngineCode); - var offset = ctx.Query.GetOffset(EngineCode); - var distinct = ctx.Query.IsDistinct ? "DISTINCT " : ""; + var limit = query.GetLimit(EngineCode); + var offset = query.GetOffset(EngineCode); + var distinct = query.IsDistinct ? "DISTINCT " : ""; if (limit > 0 && offset == 0) { ctx.Bindings.Add(limit); - return $"SELECT FIRST {parameterPlaceholder} {distinct}"; + ctx.Write($"SELECT FIRST {parameterPlaceholder} {distinct}"); + return; } if (limit == 0 && offset > 0) { ctx.Bindings.Add(offset); - return $"SELECT SKIP {parameterPlaceholder} {distinct}"; + ctx.Write($"SELECT SKIP {parameterPlaceholder} {distinct}"); + return; } - return base.CompileSelectPrefix(ctx); + base.VisitSelectPrefix(ctx, query); } - protected override string CompileBasicDateCondition(SqlResult ctx, BasicDateCondition condition) + protected override void VisitBasicDateCondition(CompileContext ctx, BasicDateCondition condition) { var column = Wrap(condition.Column); @@ -87,14 +90,17 @@ protected override string CompileBasicDateCondition(SqlResult ctx, BasicDateCond left = $"EXTRACT({condition.Part.ToUpperInvariant()} FROM {column})"; } - var sql = $"{left} {condition.Operator} {Parameter(ctx, condition.Value)}"; - if (condition.IsNot) { - return $"NOT ({sql})"; + ctx.Write("NOT ("); } - return sql; + ctx.Write(left).Write(" ").Write(condition.Operator).Write(" ").Write(Parameter(ctx, condition.Value)); + + if (condition.IsNot) + { + ctx.Write(")"); + } } public override string WrapValue(string value) diff --git a/QueryBuilder/Compilers/MySqlCompiler.cs b/QueryBuilder/Compilers/MySqlCompiler.cs index 4729125b..b1c69faf 100644 --- a/QueryBuilder/Compilers/MySqlCompiler.cs +++ b/QueryBuilder/Compilers/MySqlCompiler.cs @@ -10,40 +10,35 @@ public MySqlCompiler() public override string EngineCode { get; } = EngineCodes.MySql; - public override string CompileLimit(SqlResult ctx) + public override void VisitLimit(CompileContext ctx, Query query) { - var limit = ctx.Query.GetLimit(EngineCode); - var offset = ctx.Query.GetOffset(EngineCode); - + var limit = query.GetLimit(EngineCode); + var offset = query.GetOffset(EngineCode); if (offset == 0 && limit == 0) { - return null; + return; } + ctx.EnsureSpace(); + if (offset == 0) { ctx.Bindings.Add(limit); - return $"LIMIT {parameterPlaceholder}"; + ctx.Write($"LIMIT {parameterPlaceholder}"); + return; } if (limit == 0) { - - // MySql will not accept offset without limit, so we will put a large number - // to avoid this error. - ctx.Bindings.Add(offset); - return $"LIMIT 18446744073709551615 OFFSET {parameterPlaceholder}"; + ctx.Write($"LIMIT 18446744073709551615 OFFSET {parameterPlaceholder}"); + return; } - // We have both values - ctx.Bindings.Add(limit); ctx.Bindings.Add(offset); - - return $"LIMIT {parameterPlaceholder} OFFSET {parameterPlaceholder}"; - + ctx.Write($"LIMIT {parameterPlaceholder} OFFSET {parameterPlaceholder}"); } } } diff --git a/QueryBuilder/Compilers/OracleCompiler.cs b/QueryBuilder/Compilers/OracleCompiler.cs index 8bca3edf..70c2d7b3 100644 --- a/QueryBuilder/Compilers/OracleCompiler.cs +++ b/QueryBuilder/Compilers/OracleCompiler.cs @@ -1,8 +1,6 @@ using System; using System.Collections.Generic; -using System.Diagnostics; using System.Linq; -using System.Text.RegularExpressions; namespace SqlKata.Compilers { @@ -20,89 +18,95 @@ public OracleCompiler() public bool UseLegacyPagination { get; set; } = false; protected override string SingleRowDummyTableName => "DUAL"; - protected override SqlResult CompileSelectQuery(Query query) + public override void VisitSelectQuery(CompileContext ctx, Query query) { if (!UseLegacyPagination) { - return base.CompileSelectQuery(query); + base.VisitSelectQuery(ctx, query); + return; } - var result = base.CompileSelectQuery(query); - - ApplyLegacyLimit(result); + var beforeInner = ctx.Buffer.Length; + base.VisitSelectQuery(ctx, query); + var innerSql = ctx.Buffer.ToString(beforeInner, ctx.Buffer.Length - beforeInner); + ctx.Buffer.Length = beforeInner; - return result; + ctx.Write(ApplyLegacyLimit(ctx, innerSql)); } - public override string CompileLimit(SqlResult ctx) + public override void VisitLimit(CompileContext ctx, Query query) { if (UseLegacyPagination) { - // in pre-12c versions of Oracle, limit is handled by ROWNUM techniques - return null; + return; } - var limit = ctx.Query.GetLimit(EngineCode); - var offset = ctx.Query.GetOffset(EngineCode); + var limit = query.GetLimit(EngineCode); + var offset = query.GetOffset(EngineCode); if (limit == 0 && offset == 0) { - return null; + return; } - var safeOrder = ""; + ctx.EnsureSpace(); - if (!ctx.Query.OrderByClauses.Any()) + if (!query.OrderByClauses.Any()) { - safeOrder = "ORDER BY (SELECT 0 FROM DUAL) "; + ctx.Write("ORDER BY (SELECT 0 FROM DUAL) "); } if (limit == 0) { ctx.Bindings.Add(offset); - return $"{safeOrder}OFFSET {parameterPlaceholder} ROWS"; + ctx.Write($"OFFSET {parameterPlaceholder} ROWS"); + return; } ctx.Bindings.Add(offset); ctx.Bindings.Add(limit); - return $"{safeOrder}OFFSET {parameterPlaceholder} ROWS FETCH NEXT {parameterPlaceholder} ROWS ONLY"; + ctx.Write($"OFFSET {parameterPlaceholder} ROWS FETCH NEXT {parameterPlaceholder} ROWS ONLY"); + } + + internal void ApplyLegacyLimit(CompileContext ctx) + { + ctx.RawSql = ApplyLegacyLimit(ctx, ctx.RawSql); } - internal void ApplyLegacyLimit(SqlResult ctx) + internal string ApplyLegacyLimit(CompileContext ctx, string sql) { var limit = ctx.Query.GetLimit(EngineCode); var offset = ctx.Query.GetOffset(EngineCode); if (limit == 0 && offset == 0) { - return; + return sql; } string newSql; if (limit == 0) { - newSql = $"SELECT * FROM (SELECT \"results_wrapper\".*, ROWNUM \"row_num\" FROM ({ctx.RawSql}) \"results_wrapper\") WHERE \"row_num\" > {parameterPlaceholder}"; + newSql = $"SELECT * FROM (SELECT \"results_wrapper\".*, ROWNUM \"row_num\" FROM ({sql}) \"results_wrapper\") WHERE \"row_num\" > {parameterPlaceholder}"; ctx.Bindings.Add(offset); } else if (offset == 0) { - newSql = $"SELECT * FROM ({ctx.RawSql}) WHERE ROWNUM <= {parameterPlaceholder}"; + newSql = $"SELECT * FROM ({sql}) WHERE ROWNUM <= {parameterPlaceholder}"; ctx.Bindings.Add(limit); } else { - newSql = $"SELECT * FROM (SELECT \"results_wrapper\".*, ROWNUM \"row_num\" FROM ({ctx.RawSql}) \"results_wrapper\" WHERE ROWNUM <= {parameterPlaceholder}) WHERE \"row_num\" > {parameterPlaceholder}"; + newSql = $"SELECT * FROM (SELECT \"results_wrapper\".*, ROWNUM \"row_num\" FROM ({sql}) \"results_wrapper\" WHERE ROWNUM <= {parameterPlaceholder}) WHERE \"row_num\" > {parameterPlaceholder}"; ctx.Bindings.Add(limit + offset); ctx.Bindings.Add(offset); } - ctx.RawSql = newSql; + return newSql; } - protected override string CompileBasicDateCondition(SqlResult ctx, BasicDateCondition condition) + protected override void VisitBasicDateCondition(CompileContext ctx, BasicDateCondition condition) { - var column = Wrap(condition.Column); var value = Parameter(ctx, condition.Value); @@ -125,10 +129,9 @@ protected override string CompileBasicDateCondition(SqlResult ctx, BasicDateCond valueFormat = $"{value}"; else { - // assume HH:MM format if (condition.Value.ToString().Split(':').Count() == 2) valueFormat = $"TO_DATE({value}, 'HH24:MI')"; - else // assume HH:MM:SS format + else valueFormat = $"TO_DATE({value}, 'HH24:MI:SS')"; } sql = $"TO_CHAR({column}, 'HH24:MI:SS') {condition.Operator} TO_CHAR({valueFormat}, 'HH24:MI:SS')"; @@ -148,15 +151,19 @@ protected override string CompileBasicDateCondition(SqlResult ctx, BasicDateCond if (condition.IsNot) { - return $"NOT ({sql})"; + ctx.Write("NOT ("); } - return sql; + ctx.Write(sql); + if (condition.IsNot) + { + ctx.Write(")"); + } } - protected override SqlResult CompileRemainingInsertClauses( - SqlResult ctx, string table, IEnumerable inserts) + public override void VisitRemainingInsertClauses( + CompileContext ctx, string table, IEnumerable inserts) { foreach (var insert in inserts.Skip(1)) { @@ -164,13 +171,10 @@ protected override SqlResult CompileRemainingInsertClauses( string values = string.Join(", ", Parameterize(ctx, insert.Values)); string intoFormat = " INTO {0}{1} VALUES ({2})"; - var nextInsert = string.Format(intoFormat, table, columns, values); - - ctx.RawSql += nextInsert; + ctx.Write(string.Format(intoFormat, table, columns, values)); } - ctx.RawSql += " SELECT 1 FROM DUAL"; - return ctx; + ctx.Write(" SELECT 1 FROM DUAL"); } } } diff --git a/QueryBuilder/Compilers/PostgresCompiler.cs b/QueryBuilder/Compilers/PostgresCompiler.cs index 3b45d0e6..dadc8704 100644 --- a/QueryBuilder/Compilers/PostgresCompiler.cs +++ b/QueryBuilder/Compilers/PostgresCompiler.cs @@ -13,10 +13,8 @@ public PostgresCompiler() public override string EngineCode { get; } = EngineCodes.PostgreSql; public override bool SupportsFilterClause { get; set; } = true; - - protected override string CompileBasicStringCondition(SqlResult ctx, BasicStringCondition x) + protected override void VisitBasicStringCondition(CompileContext ctx, BasicStringCondition x) { - var column = Wrap(x.Column); var value = Resolve(ctx, x.Value) as string; @@ -46,28 +44,32 @@ protected override string CompileBasicStringCondition(SqlResult ctx, BasicString } } - string sql; + if (x.IsNot) + { + ctx.Write("NOT ("); + } if (x.Value is UnsafeLiteral) { - sql = $"{column} {checkOperator(method)} {value}"; + ctx.Write(column).Write(" ").Write(checkOperator(method)).Write(" ").Write(value); } else { - sql = $"{column} {checkOperator(method)} {Parameter(ctx, value)}"; + ctx.Write(column).Write(" ").Write(checkOperator(method)).Write(" ").Write(Parameter(ctx, value)); } if (!string.IsNullOrEmpty(x.EscapeCharacter)) { - sql = $"{sql} ESCAPE '{x.EscapeCharacter}'"; + ctx.Write(" ESCAPE '").Write(x.EscapeCharacter).Write("'"); } - return x.IsNot ? $"NOT ({sql})" : sql; - + if (x.IsNot) + { + ctx.Write(")"); + } } - - protected override string CompileBasicDateCondition(SqlResult ctx, BasicDateCondition condition) + protected override void VisitBasicDateCondition(CompileContext ctx, BasicDateCondition condition) { var column = Wrap(condition.Column); @@ -86,14 +88,17 @@ protected override string CompileBasicDateCondition(SqlResult ctx, BasicDateCond left = $"DATE_PART('{condition.Part.ToUpperInvariant()}', {column})"; } - var sql = $"{left} {condition.Operator} {Parameter(ctx, condition.Value)}"; - if (condition.IsNot) { - return $"NOT ({sql})"; + ctx.Write("NOT ("); } - return sql; + ctx.Write(left).Write(" ").Write(condition.Operator).Write(" ").Write(Parameter(ctx, condition.Value)); + + if (condition.IsNot) + { + ctx.Write(")"); + } } } } diff --git a/QueryBuilder/Compilers/SqlServerCompiler.cs b/QueryBuilder/Compilers/SqlServerCompiler.cs index 0bd4daea..1a4ed2c1 100644 --- a/QueryBuilder/Compilers/SqlServerCompiler.cs +++ b/QueryBuilder/Compilers/SqlServerCompiler.cs @@ -14,110 +14,120 @@ public SqlServerCompiler() public override string EngineCode { get; } = EngineCodes.SqlServer; public bool UseLegacyPagination { get; set; } = false; - protected override SqlResult CompileSelectQuery(Query query) + public override void VisitSelectQuery(CompileContext ctx, Query query) { if (!UseLegacyPagination || !query.HasOffset(EngineCode)) { - return base.CompileSelectQuery(query); + base.VisitSelectQuery(ctx, query); + return; } query = query.Clone(); - var ctx = new SqlResult(parameterPlaceholder, EscapeCharacter) - { - Query = query, - }; - var limit = query.GetLimit(EngineCode); var offset = query.GetOffset(EngineCode); - if (!query.SelectClauses.Any()) { query.Select("*"); } - var order = CompileOrders(ctx) ?? "ORDER BY (SELECT 0)"; + ctx.PushQuery(query); + var beforeOrder = ctx.Buffer.Length; + var beforeOrderBindings = ctx.Bindings.Count; + VisitOrders(ctx, query); + var order = ctx.Buffer.Length > beforeOrder ? ctx.Buffer.ToString(beforeOrder, ctx.Buffer.Length - beforeOrder).TrimStart() : "ORDER BY (SELECT 0)"; + var orderBindings = ctx.Bindings.Skip(beforeOrderBindings).ToArray(); + ctx.Buffer.Length = beforeOrder; + ctx.Bindings.RemoveRange(beforeOrderBindings, orderBindings.Length); + ctx.PopQuery(); - query.SelectRaw($"ROW_NUMBER() OVER ({order}) AS [row_num]", ctx.Bindings.ToArray()); + query.SelectRaw($"ROW_NUMBER() OVER ({order}) AS [row_num]", orderBindings); query.OrderByClauses.Clear(); + ctx.PushQuery(query); + var beforeInner = ctx.Buffer.Length; + try + { + base.VisitSelectQuery(ctx, query); + } + finally + { + ctx.PopQuery(); + } - var result = base.CompileSelectQuery(query); + var innerSql = ctx.Buffer.ToString(beforeInner, ctx.Buffer.Length - beforeInner); + ctx.Buffer.Length = beforeInner; if (limit == 0) { - result.RawSql = $"SELECT * FROM ({result.RawSql}) AS [results_wrapper] WHERE [row_num] >= {parameterPlaceholder}"; - result.Bindings.Add(offset + 1); - } - else - { - result.RawSql = $"SELECT * FROM ({result.RawSql}) AS [results_wrapper] WHERE [row_num] BETWEEN {parameterPlaceholder} AND {parameterPlaceholder}"; - result.Bindings.Add(offset + 1); - result.Bindings.Add(limit + offset); + ctx.Bindings.Add(offset + 1); + ctx.Write($"SELECT * FROM ({innerSql}) AS [results_wrapper] WHERE [row_num] >= {parameterPlaceholder}"); + return; } - return result; + ctx.Bindings.Add(offset + 1); + ctx.Bindings.Add(limit + offset); + ctx.Write($"SELECT * FROM ({innerSql}) AS [results_wrapper] WHERE [row_num] BETWEEN {parameterPlaceholder} AND {parameterPlaceholder}"); } - protected override string CompileSelectPrefix(SqlResult ctx) + public override void VisitSelectPrefix(CompileContext ctx, Query query) { if (!UseLegacyPagination) { - return base.CompileSelectPrefix(ctx); + base.VisitSelectPrefix(ctx, query); + return; } - // If there is a limit on the query, but not an offset, we will add the top - // clause to the query, which serves as a "limit" type clause within the - // SQL Server system similar to the limit keywords available in MySQL. - var limit = ctx.Query.GetLimit(EngineCode); - var offset = ctx.Query.GetOffset(EngineCode); + var limit = query.GetLimit(EngineCode); + var offset = query.GetOffset(EngineCode); if (limit > 0 && offset == 0) { ctx.Bindings.Add(limit); - var distinct = ctx.Query.IsDistinct ? "DISTINCT " : ""; - return $"SELECT {distinct}TOP ({parameterPlaceholder}) "; + var distinct = query.IsDistinct ? "DISTINCT " : ""; + ctx.Write($"SELECT {distinct}TOP ({parameterPlaceholder}) "); + return; } - return base.CompileSelectPrefix(ctx); + base.VisitSelectPrefix(ctx, query); } - public override string CompileLimit(SqlResult ctx) + public override void VisitLimit(CompileContext ctx, Query query) { if (UseLegacyPagination) { - // in legacy versions of Sql Server, limit is handled by TOP - // and ROW_NUMBER techniques - return null; + return; } - var limit = ctx.Query.GetLimit(EngineCode); - var offset = ctx.Query.GetOffset(EngineCode); + var limit = query.GetLimit(EngineCode); + var offset = query.GetOffset(EngineCode); if (limit == 0 && offset == 0) { - return null; + return; } - var safeOrder = ""; - if (!ctx.Query.OrderByClauses.Any()) + ctx.EnsureSpace(); + + if (!query.OrderByClauses.Any()) { - safeOrder = "ORDER BY (SELECT 0) "; + ctx.Write("ORDER BY (SELECT 0) "); } if (limit == 0) { ctx.Bindings.Add(offset); - return $"{safeOrder}OFFSET {parameterPlaceholder} ROWS"; + ctx.Write($"OFFSET {parameterPlaceholder} ROWS"); + return; } ctx.Bindings.Add(offset); ctx.Bindings.Add(limit); - return $"{safeOrder}OFFSET {parameterPlaceholder} ROWS FETCH NEXT {parameterPlaceholder} ROWS ONLY"; + ctx.Write($"OFFSET {parameterPlaceholder} ROWS FETCH NEXT {parameterPlaceholder} ROWS ONLY"); } public override string CompileRandom(string seed) @@ -135,7 +145,7 @@ public override string CompileFalse() return "cast(0 as bit)"; } - protected override string CompileBasicDateCondition(SqlResult ctx, BasicDateCondition condition) + protected override void VisitBasicDateCondition(CompileContext ctx, BasicDateCondition condition) { var column = Wrap(condition.Column); var part = condition.Part.ToUpperInvariant(); @@ -151,30 +161,28 @@ protected override string CompileBasicDateCondition(SqlResult ctx, BasicDateCond left = $"DATEPART({part.ToUpperInvariant()}, {column})"; } - var sql = $"{left} {condition.Operator} {Parameter(ctx, condition.Value)}"; - if (condition.IsNot) { - return $"NOT ({sql})"; + ctx.Write("NOT ("); } - return sql; + ctx.Write(left).Write(" ").Write(condition.Operator).Write(" ").Write(Parameter(ctx, condition.Value)); + + if (condition.IsNot) + { + ctx.Write(")"); + } } - protected override SqlResult CompileAdHocQuery(AdHocTableFromClause adHoc) + public override void VisitAdHocTableFromClause(CompileContext ctx, AdHocTableFromClause adHoc) { - var ctx = new SqlResult(parameterPlaceholder, EscapeCharacter); - var colNames = string.Join(", ", adHoc.Columns.Select(Wrap)); var valueRow = string.Join(", ", Enumerable.Repeat(parameterPlaceholder, adHoc.Columns.Count)); var valueRows = string.Join(", ", Enumerable.Repeat($"({valueRow})", adHoc.Values.Count / adHoc.Columns.Count)); - var sql = $"SELECT {colNames} FROM (VALUES {valueRows}) AS tbl ({colNames})"; - - ctx.RawSql = sql; - ctx.Bindings = adHoc.Values; - return ctx; + ctx.Write($"SELECT {colNames} FROM (VALUES {valueRows}) AS tbl ({colNames})"); + ctx.Bindings.AddRange(adHoc.Values); } } } diff --git a/QueryBuilder/Compilers/SqliteCompiler.cs b/QueryBuilder/Compilers/SqliteCompiler.cs index 1401dd35..ff248720 100644 --- a/QueryBuilder/Compilers/SqliteCompiler.cs +++ b/QueryBuilder/Compilers/SqliteCompiler.cs @@ -20,21 +20,23 @@ public override string CompileFalse() return "0"; } - public override string CompileLimit(SqlResult ctx) + public override void VisitLimit(CompileContext ctx, Query query) { - var limit = ctx.Query.GetLimit(EngineCode); - var offset = ctx.Query.GetOffset(EngineCode); + var limit = query.GetLimit(EngineCode); + var offset = query.GetOffset(EngineCode); if (limit == 0 && offset > 0) { + ctx.EnsureSpace(); ctx.Bindings.Add(offset); - return $"LIMIT -1 OFFSET {parameterPlaceholder}"; + ctx.Write($"LIMIT -1 OFFSET {parameterPlaceholder}"); + return; } - return base.CompileLimit(ctx); + base.VisitLimit(ctx, query); } - protected override string CompileBasicDateCondition(SqlResult ctx, BasicDateCondition condition) + protected override void VisitBasicDateCondition(CompileContext ctx, BasicDateCondition condition) { var column = Wrap(condition.Column); var value = Parameter(ctx, condition.Value); @@ -51,17 +53,31 @@ protected override string CompileBasicDateCondition(SqlResult ctx, BasicDateCond if (!formatMap.ContainsKey(condition.Part)) { - return $"{column} {condition.Operator} {value}"; + if (condition.IsNot) + { + ctx.Write("NOT ("); + } + + ctx.Write(column).Write(" ").Write(condition.Operator).Write(" ").Write(value); + + if (condition.IsNot) + { + ctx.Write(")"); + } + + return; } var sql = $"strftime('{formatMap[condition.Part]}', {column}) {condition.Operator} cast({value} as text)"; if (condition.IsNot) { - return $"NOT ({sql})"; + ctx.Write($"NOT ({sql})"); + } + else + { + ctx.Write(sql); } - - return sql; } } } diff --git a/QueryBuilder/SqlResult.cs b/QueryBuilder/SqlResult.cs index b84baf8a..099424db 100644 --- a/QueryBuilder/SqlResult.cs +++ b/QueryBuilder/SqlResult.cs @@ -8,8 +8,11 @@ namespace SqlKata { public class SqlResult { - private string ParameterPlaceholder { get; set; } - private string EscapeCharacter { get; set; } + public string ParameterPlaceholder { get; set; } = "?"; + public string EscapeCharacter { get; set; } = "\\"; + + public SqlResult() { } + public SqlResult(string parameterPlaceholder, string escapeCharacter) { ParameterPlaceholder = parameterPlaceholder;