From 861210993795bbd38213c0af0489069fdd80817a Mon Sep 17 00:00:00 2001 From: Ahmad Date: Tue, 13 Oct 2026 21:46:41 +0300 Subject: [PATCH 1/8] refactor: refactor core to simplify the code structure and API --- QueryBuilder.Tests/GeneralTests.cs | 30 - .../Infrastructure/TestCompiler.cs | 24 - QueryBuilder.Tests/OperatorWhitelistTests.cs | 4 +- QueryBuilder.Tests/WhereTests.cs | 105 +++ QueryBuilder/Base.Where.cs | 699 ---------------- QueryBuilder/BaseQuery.cs | 310 -------- QueryBuilder/Clauses/ConditionClause.cs | 41 +- QueryBuilder/Compilers/Compiler.Conditions.cs | 81 +- QueryBuilder/Compilers/Compiler.cs | 8 +- .../Compilers/ConditionsCompilerProvider.cs | 54 -- QueryBuilder/ConditionGroup.cs | 148 ++++ .../Extensions/ConditionExtensions.cs | 746 ++++++++++++++++++ QueryBuilder/IClauseContainer.cs | 11 + QueryBuilder/IConditional.cs | 16 + QueryBuilder/Join.cs | 257 +++++- QueryBuilder/Query.Aggregate.cs | 16 +- QueryBuilder/Query.From.cs | 54 ++ QueryBuilder/Query.Having.cs | 610 ++++++-------- QueryBuilder/Query.Join.cs | 11 +- QueryBuilder/Query.Where.cs | 566 +++++++++++++ QueryBuilder/Query.cs | 372 ++++++++- SqlKata.Execution/QueryFactory.cs | 2 +- SqlKata.Execution/XQuery.cs | 2 +- 23 files changed, 2578 insertions(+), 1589 deletions(-) delete mode 100644 QueryBuilder/Base.Where.cs delete mode 100644 QueryBuilder/BaseQuery.cs delete mode 100644 QueryBuilder/Compilers/ConditionsCompilerProvider.cs create mode 100644 QueryBuilder/ConditionGroup.cs create mode 100644 QueryBuilder/Extensions/ConditionExtensions.cs create mode 100644 QueryBuilder/IClauseContainer.cs create mode 100644 QueryBuilder/IConditional.cs create mode 100644 QueryBuilder/Query.From.cs create mode 100644 QueryBuilder/Query.Where.cs diff --git a/QueryBuilder.Tests/GeneralTests.cs b/QueryBuilder.Tests/GeneralTests.cs index ae20a469..b6e355d9 100644 --- a/QueryBuilder.Tests/GeneralTests.cs +++ b/QueryBuilder.Tests/GeneralTests.cs @@ -57,37 +57,7 @@ public void InnerScopeEngineWithinSubQuery() Assert.Equal("SELECT * FROM (SELECT * FROM \"TABLE\" WHERE firebird = 1) AS \"SERIES\"", c[EngineCodes.Firebird]); } - [Fact] - public void ItShouldCacheMethodInfoByType() - { - var compiler = new TestSqlServerCompiler(); - - var call1 = compiler.Call_FindCompilerMethodInfo( - typeof(BasicCondition), "CompileBasicCondition" - ); - - var call2 = compiler.Call_FindCompilerMethodInfo( - typeof(BasicCondition), "CompileBasicCondition" - ); - - Assert.Same(call1, call2); - } - [Fact] - public void Return_Different_MethodInfo_WhenSame_Method_With_Different_GenericTypes() - { - var compiler = new TestSqlServerCompiler(); - - var call1 = compiler.Call_FindCompilerMethodInfo( - typeof(NestedCondition), "CompileNestedCondition" - ); - - var call2 = compiler.Call_FindCompilerMethodInfo( - typeof(NestedCondition), "CompileNestedCondition" - ); - - Assert.NotSame(call1, call2); - } [Fact] public void Custom_compiler_with_empty_identifier_overrides_should_remove_identifiers() diff --git a/QueryBuilder.Tests/Infrastructure/TestCompiler.cs b/QueryBuilder.Tests/Infrastructure/TestCompiler.cs index aded9fd1..efc11d96 100644 --- a/QueryBuilder.Tests/Infrastructure/TestCompiler.cs +++ b/QueryBuilder.Tests/Infrastructure/TestCompiler.cs @@ -1,5 +1,3 @@ -using System; -using System.Reflection; using SqlKata.Compilers; namespace SqlKata.Tests.Infrastructure @@ -10,43 +8,22 @@ namespace SqlKata.Tests.Infrastructure class TestCompiler : Compiler { public override string EngineCode { get; } = "test"; - - public virtual MethodInfo Call_FindCompilerMethodInfo(Type clauseType, string methodName) - { - return FindCompilerMethodInfo(clauseType, methodName); - } } class TestSqlServerCompiler : SqlServerCompiler { - public virtual MethodInfo Call_FindCompilerMethodInfo(Type clauseType, string methodName) - { - return FindCompilerMethodInfo(clauseType, methodName); - } } class TestMySqlCompiler : MySqlCompiler { - public virtual MethodInfo Call_FindCompilerMethodInfo(Type clauseType, string methodName) - { - return FindCompilerMethodInfo(clauseType, methodName); - } } class TestPostgresCompiler : PostgresCompiler { - public virtual MethodInfo Call_FindCompilerMethodInfo(Type clauseType, string methodName) - { - return FindCompilerMethodInfo(clauseType, methodName); - } } class TestFirebirdCompiler : FirebirdCompiler { - public virtual MethodInfo Call_FindCompilerMethodInfo(Type clauseType, string methodName) - { - return FindCompilerMethodInfo(clauseType, methodName); - } } class TestEmptyIdentifiersCompiler : TestCompiler @@ -55,4 +32,3 @@ class TestEmptyIdentifiersCompiler : TestCompiler protected override string ClosingIdentifier { get; set; } = ""; } } - diff --git a/QueryBuilder.Tests/OperatorWhitelistTests.cs b/QueryBuilder.Tests/OperatorWhitelistTests.cs index 55c3aafc..6e6303be 100644 --- a/QueryBuilder.Tests/OperatorWhitelistTests.cs +++ b/QueryBuilder.Tests/OperatorWhitelistTests.cs @@ -20,7 +20,7 @@ public void DenyInvalidOperatorsInWhere(string op) { var compiler = new SqlServerCompiler(); - Assert.Throws(() => + Assert.Throws(() => { compiler.Compile(new Query("Table").Where("Id", op, 1)); compiler.Compile(new Query("Table").OrWhere("Id", op, 1)); @@ -40,7 +40,7 @@ public void DenyInvalidOperatorsInHaving(string op) { var compiler = new SqlServerCompiler(); - Assert.Throws(() => + Assert.Throws(() => { compiler.Compile(new Query("Table").Having("Id", op, 1)); compiler.Compile(new Query("Table").OrHaving("Id", op, 1)); diff --git a/QueryBuilder.Tests/WhereTests.cs b/QueryBuilder.Tests/WhereTests.cs index 0c1254b2..2821134c 100644 --- a/QueryBuilder.Tests/WhereTests.cs +++ b/QueryBuilder.Tests/WhereTests.cs @@ -29,5 +29,110 @@ public void GroupedHavingFilters() Assert.Equal(@"SELECT * FROM ""Table1"" HAVING (SUM(""Column1"") = 10 OR SUM(""Column2"") = 20) AND SUM(""Column3"") = 30", c[EngineCodes.PostgreSql]); } + + [Fact] + public void GroupedHavingWithConditionGroup() + { + var group = new ConditionGroup(); + group.Where("Total", ">", 100).OrWhere("Count", "<", 5); + + var q = new Query("Orders") + .Having(group) + .HavingBetween("Average", 10, 50) + .HavingIn("Category", new[] { 1, 2, 3 }); + + var c = Compile(q); + + Assert.Equal(@"SELECT * FROM [Orders] HAVING ([Total] > 100 OR [Count] < 5) AND [Average] BETWEEN 10 AND 50 AND [Category] IN (1, 2, 3)", c[EngineCodes.SqlServer]); + } + + [Fact] + public void ConditionGroupStandalone() + { + var group = new ConditionGroup(); + group.Where("A", 1).OrWhere("B", 2); + + Assert.Equal(2, group.Clauses.Count); + Assert.False(((AbstractCondition)group.Clauses[0]).IsOr); + Assert.True(((AbstractCondition)group.Clauses[1]).IsOr); + } + + [Fact] + public void Or_FollowedByWhere_DoesNotAffectSubsequentHaving() + { + var q = new Query("Table1") + .Where("A", 10) + .Or() + .Where("B", 20) + .HavingRaw("SUM([C]) = ?", 1) + .HavingRaw("SUM([D]) = ?", 2); + + var c = Compile(q); + + Assert.True(((AbstractCondition)q.Wheres.Clauses[1]).IsOr); + Assert.False(((AbstractCondition)q.Havings.Clauses[1]).IsOr); + Assert.Equal( + @"SELECT * FROM [Table1] WHERE [A] = 10 OR [B] = 20 HAVING SUM([C]) = 1 AND SUM([D]) = 2", + c[EngineCodes.SqlServer] + ); + } + + [Fact] + public void Or_FollowedByHaving_DoesNotAffectSubsequentWhere() + { + var q = new Query("Table1") + .HavingRaw("SUM([C]) = ?", 1) + .Or() + .HavingRaw("SUM([D]) = ?", 2) + .Where("A", 10) + .Where("B", 20); + + var c = Compile(q); + + Assert.True(((AbstractCondition)q.Havings.Clauses[1]).IsOr); + Assert.False(((AbstractCondition)q.Wheres.Clauses[1]).IsOr); + Assert.Equal( + @"SELECT * FROM [Table1] WHERE [A] = 10 AND [B] = 20 HAVING SUM([C]) = 1 OR SUM([D]) = 2", + c[EngineCodes.SqlServer] + ); + } + + [Fact] + public void Or_Interleaved_WhereThenHavingThenWhere() + { + var q = new Query("Table1") + .Where("A", 1) + .Or() + .HavingRaw("SUM([C]) = ?", 2) + .Where("B", 3); + + var c = Compile(q); + + // The Or() was consumed by Having, so B remains AND + Assert.False(((AbstractCondition)q.Wheres.Clauses[1]).IsOr); + Assert.Equal( + @"SELECT * FROM [Table1] WHERE [A] = 1 AND [B] = 3 HAVING SUM([C]) = 2", + c[EngineCodes.SqlServer] + ); + } + + [Fact] + public void Or_Interleaved_HavingThenWhereThenHaving() + { + var q = new Query("Table1") + .HavingRaw("SUM([C]) = ?", 1) + .Or() + .Where("A", 2) + .HavingRaw("SUM([D]) = ?", 3); + + var c = Compile(q); + + // The Or() was consumed by Where, so SUM(D) remains AND + Assert.False(((AbstractCondition)q.Havings.Clauses[1]).IsOr); + Assert.Equal( + @"SELECT * FROM [Table1] WHERE [A] = 2 HAVING SUM([C]) = 1 AND SUM([D]) = 3", + c[EngineCodes.SqlServer] + ); + } } } diff --git a/QueryBuilder/Base.Where.cs b/QueryBuilder/Base.Where.cs deleted file mode 100644 index c40cc40a..00000000 --- a/QueryBuilder/Base.Where.cs +++ /dev/null @@ -1,699 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Reflection; - -namespace SqlKata -{ - public abstract partial class BaseQuery - { - public Q Where(string column, string op, object value) - { - - // If the value is "null", we will just assume the developer wants to add a - // where null clause to the query. So, we will allow a short-cut here to - // that method for convenience so the developer doesn't have to check. - if (value == null) - { - return Not(op != "=").WhereNull(column); - } - - if (value is bool boolValue) - { - if (op != "=") - { - Not(); - } - - return boolValue ? WhereTrue(column) : WhereFalse(column); - } - - return AddComponent("where", new BasicCondition - { - Column = column, - Operator = op, - Value = value, - IsOr = GetOr(), - IsNot = GetNot(), - }); - } - - public Q WhereNot(string column, string op, object value) - { - return Not().Where(column, op, value); - } - - public Q OrWhere(string column, string op, object value) - { - return Or().Where(column, op, value); - } - - public Q OrWhereNot(string column, string op, object value) - { - return this.Or().Not().Where(column, op, value); - } - - public Q Where(string column, object value) - { - return Where(column, "=", value); - } - public Q WhereNot(string column, object value) - { - return WhereNot(column, "=", value); - } - public Q OrWhere(string column, object value) - { - return OrWhere(column, "=", value); - } - public Q OrWhereNot(string column, object value) - { - return OrWhereNot(column, "=", value); - } - - /// - /// Perform a where constraint - /// - /// - /// - public Q Where(object constraints) - { - var dictionary = new Dictionary(); - - foreach (var item in constraints.GetType().GetRuntimeProperties()) - { - dictionary.Add(item.Name, item.GetValue(constraints)); - } - - return Where(dictionary); - } - - public Q Where(IEnumerable> values) - { - var query = (Q)this; - var orFlag = GetOr(); - var notFlag = GetNot(); - - foreach (var tuple in values) - { - if (orFlag) - { - query = query.Or(); - } - else - { - query.And(); - } - - query = this.Not(notFlag).Where(tuple.Key, tuple.Value); - } - - return query; - } - - public Q WhereRaw(string sql, params object[] bindings) - { - return AddComponent("where", new RawCondition - { - Expression = sql, - Bindings = bindings, - IsOr = GetOr(), - IsNot = GetNot(), - }); - } - - public Q OrWhereRaw(string sql, params object[] bindings) - { - return Or().WhereRaw(sql, bindings); - } - - /// - /// Apply a nested where clause - /// - /// - /// - public Q Where(Func callback) - { - var query = callback.Invoke(NewChild()); - - // omit empty queries - if (!query.Clauses.Where(x => x.Component == "where").Any()) - { - return (Q)this; - } - - return AddComponent("where", new NestedCondition - { - Query = query, - IsNot = GetNot(), - IsOr = GetOr(), - }); - } - - public Q WhereNot(Func callback) - { - return Not().Where(callback); - } - - public Q OrWhere(Func callback) - { - return Or().Where(callback); - } - - public Q OrWhereNot(Func callback) - { - return Not().Or().Where(callback); - } - - public Q WhereColumns(string first, string op, string second) - { - return AddComponent("where", new TwoColumnsCondition - { - First = first, - Second = second, - Operator = op, - IsOr = GetOr(), - IsNot = GetNot(), - }); - } - - public Q OrWhereColumns(string first, string op, string second) - { - return Or().WhereColumns(first, op, second); - } - - public Q WhereNull(string column) - { - return AddComponent("where", new NullCondition - { - Column = column, - IsOr = GetOr(), - IsNot = GetNot(), - }); - } - - public Q WhereNotNull(string column) - { - return Not().WhereNull(column); - } - - public Q OrWhereNull(string column) - { - return this.Or().WhereNull(column); - } - - public Q OrWhereNotNull(string column) - { - return Or().Not().WhereNull(column); - } - - public Q WhereTrue(string column) - { - return AddComponent("where", new BooleanCondition - { - Column = column, - Value = true, - IsOr = GetOr(), - IsNot = GetNot(), - }); - } - - public Q OrWhereTrue(string column) - { - return Or().WhereTrue(column); - } - - public Q WhereFalse(string column) - { - return AddComponent("where", new BooleanCondition - { - Column = column, - Value = false, - IsOr = GetOr(), - IsNot = GetNot(), - }); - } - - public Q OrWhereFalse(string column) - { - return Or().WhereFalse(column); - } - - public Q WhereLike(string column, object value, bool caseSensitive = false, string escapeCharacter = null) - { - return AddComponent("where", new BasicStringCondition - { - Operator = "like", - Column = column, - Value = value, - CaseSensitive = caseSensitive, - EscapeCharacter = escapeCharacter, - IsOr = GetOr(), - IsNot = GetNot(), - }); - } - - public Q WhereNotLike(string column, object value, bool caseSensitive = false, string escapeCharacter = null) - { - return Not().WhereLike(column, value, caseSensitive, escapeCharacter); - } - - public Q OrWhereLike(string column, object value, bool caseSensitive = false, string escapeCharacter = null) - { - return Or().WhereLike(column, value, caseSensitive, escapeCharacter); - } - - public Q OrWhereNotLike(string column, object value, bool caseSensitive = false, string escapeCharacter = null) - { - return Or().Not().WhereLike(column, value, caseSensitive, escapeCharacter); - } - public Q WhereStarts(string column, object value, bool caseSensitive = false, string escapeCharacter = null) - { - return AddComponent("where", new BasicStringCondition - { - Operator = "starts", - Column = column, - Value = value, - CaseSensitive = caseSensitive, - EscapeCharacter = escapeCharacter, - IsOr = GetOr(), - IsNot = GetNot(), - }); - } - - public Q WhereNotStarts(string column, object value, bool caseSensitive = false, string escapeCharacter = null) - { - return Not().WhereStarts(column, value, caseSensitive, escapeCharacter); - } - - public Q OrWhereStarts(string column, object value, bool caseSensitive = false, string escapeCharacter = null) - { - return Or().WhereStarts(column, value, caseSensitive, escapeCharacter); - } - - public Q OrWhereNotStarts(string column, object value, bool caseSensitive = false, string escapeCharacter = null) - { - return Or().Not().WhereStarts(column, value, caseSensitive, escapeCharacter); - } - - public Q WhereEnds(string column, object value, bool caseSensitive = false, string escapeCharacter = null) - { - return AddComponent("where", new BasicStringCondition - { - Operator = "ends", - Column = column, - Value = value, - CaseSensitive = caseSensitive, - EscapeCharacter = escapeCharacter, - IsOr = GetOr(), - IsNot = GetNot(), - }); - } - - public Q WhereNotEnds(string column, object value, bool caseSensitive = false, string escapeCharacter = null) - { - return Not().WhereEnds(column, value, caseSensitive, escapeCharacter); - } - - public Q OrWhereEnds(string column, object value, bool caseSensitive = false, string escapeCharacter = null) - { - return Or().WhereEnds(column, value, caseSensitive, escapeCharacter); - } - - public Q OrWhereNotEnds(string column, object value, bool caseSensitive = false, string escapeCharacter = null) - { - return Or().Not().WhereEnds(column, value, caseSensitive, escapeCharacter); - } - - public Q WhereContains(string column, object value, bool caseSensitive = false, string escapeCharacter = null) - { - return AddComponent("where", new BasicStringCondition - { - Operator = "contains", - Column = column, - Value = value, - CaseSensitive = caseSensitive, - EscapeCharacter = escapeCharacter, - IsOr = GetOr(), - IsNot = GetNot(), - }); - } - - public Q WhereNotContains(string column, object value, bool caseSensitive = false, string escapeCharacter = null) - { - return Not().WhereContains(column, value, caseSensitive, escapeCharacter); - } - - public Q OrWhereContains(string column, object value, bool caseSensitive = false, string escapeCharacter = null) - { - return Or().WhereContains(column, value, caseSensitive, escapeCharacter); - } - - public Q OrWhereNotContains(string column, object value, bool caseSensitive = false, string escapeCharacter = null) - { - return Or().Not().WhereContains(column, value, caseSensitive, escapeCharacter); - } - - public Q WhereBetween(string column, T lower, T higher) - { - return AddComponent("where", new BetweenCondition - { - Column = column, - IsOr = GetOr(), - IsNot = GetNot(), - Lower = lower, - Higher = higher - }); - } - - public Q OrWhereBetween(string column, T lower, T higher) - { - return Or().WhereBetween(column, lower, higher); - } - public Q WhereNotBetween(string column, T lower, T higher) - { - return Not().WhereBetween(column, lower, higher); - } - public Q OrWhereNotBetween(string column, T lower, T higher) - { - return Or().Not().WhereBetween(column, lower, higher); - } - - public Q WhereIn(string column, IEnumerable values) - { - - // If the developer has passed a string they most likely want a List - // since string is considered as List - if (values is string) - { - string val = values as string; - - return AddComponent("where", new InCondition - { - Column = column, - IsOr = GetOr(), - IsNot = GetNot(), - Values = new List { val } - }); - } - - return AddComponent("where", new InCondition - { - Column = column, - IsOr = GetOr(), - IsNot = GetNot(), - Values = values.Distinct().ToList() - }); - - - } - - public Q OrWhereIn(string column, IEnumerable values) - { - return Or().WhereIn(column, values); - } - - public Q WhereNotIn(string column, IEnumerable values) - { - return Not().WhereIn(column, values); - } - - public Q OrWhereNotIn(string column, IEnumerable values) - { - return Or().Not().WhereIn(column, values); - } - - - public Q WhereIn(string column, Query query) - { - return AddComponent("where", new InQueryCondition - { - Column = column, - IsOr = GetOr(), - IsNot = GetNot(), - Query = query, - }); - } - public Q WhereIn(string column, Func callback) - { - var query = callback.Invoke(new Query().SetParent(this)); - - return WhereIn(column, query); - } - - public Q OrWhereIn(string column, Query query) - { - return Or().WhereIn(column, query); - } - - public Q OrWhereIn(string column, Func callback) - { - return Or().WhereIn(column, callback); - } - public Q WhereNotIn(string column, Query query) - { - return Not().WhereIn(column, query); - } - - public Q WhereNotIn(string column, Func callback) - { - return Not().WhereIn(column, callback); - } - - public Q OrWhereNotIn(string column, Query query) - { - return Or().Not().WhereIn(column, query); - } - - public Q OrWhereNotIn(string column, Func callback) - { - return Or().Not().WhereIn(column, callback); - } - - - /// - /// Perform a sub query where clause - /// - /// - /// - /// - /// - public Q Where(string column, string op, Func callback) - { - var query = callback.Invoke(NewChild()); - - return Where(column, op, query); - } - - public Q Where(string column, string op, Query query) - { - return AddComponent("where", new QueryCondition - { - Column = column, - Operator = op, - Query = query, - IsNot = GetNot(), - IsOr = GetOr(), - }); - } - - public Q WhereSub(Query query, object value) - { - return WhereSub(query, "=", value); - } - - public Q WhereSub(Query query, string op, object value) - { - return AddComponent("where", new SubQueryCondition - { - Value = value, - Operator = op, - Query = query, - IsNot = GetNot(), - IsOr = GetOr(), - }); - } - - public Q OrWhereSub(Query query, object value) - { - return Or().WhereSub(query, value); - } - - public Q OrWhereSub(Query query, string op, object value) - { - return Or().WhereSub(query, op, value); - } - - public Q OrWhere(string column, string op, Query query) - { - return Or().Where(column, op, query); - } - public Q OrWhere(string column, string op, Func callback) - { - return Or().Where(column, op, callback); - } - - public Q WhereExists(Query query) - { - if (!query.HasComponent("from")) - { - throw new ArgumentException($"'{nameof(FromClause)}' cannot be empty if used inside a '{nameof(WhereExists)}' condition"); - } - - return AddComponent("where", new ExistsCondition - { - Query = query, - IsNot = GetNot(), - IsOr = GetOr(), - }); - } - public Q WhereExists(Func callback) - { - var childQuery = new Query().SetParent(this); - return WhereExists(callback.Invoke(childQuery)); - } - - public Q WhereNotExists(Query query) - { - return Not().WhereExists(query); - } - - public Q WhereNotExists(Func callback) - { - return Not().WhereExists(callback); - } - - public Q OrWhereExists(Query query) - { - return Or().WhereExists(query); - } - public Q OrWhereExists(Func callback) - { - return Or().WhereExists(callback); - } - public Q OrWhereNotExists(Query query) - { - return Or().Not().WhereExists(query); - } - public Q OrWhereNotExists(Func callback) - { - return Or().Not().WhereExists(callback); - } - - #region date - public Q WhereDatePart(string part, string column, string op, object value) - { - return AddComponent("where", new BasicDateCondition - { - Operator = op, - Column = column, - Value = value, - Part = part?.ToLowerInvariant(), - IsOr = GetOr(), - IsNot = GetNot(), - }); - } - public Q WhereNotDatePart(string part, string column, string op, object value) - { - return Not().WhereDatePart(part, column, op, value); - } - - public Q OrWhereDatePart(string part, string column, string op, object value) - { - return Or().WhereDatePart(part, column, op, value); - } - - public Q OrWhereNotDatePart(string part, string column, string op, object value) - { - return Or().Not().WhereDatePart(part, column, op, value); - } - - public Q WhereDate(string column, string op, object value) - { - return WhereDatePart("date", column, op, value); - } - public Q WhereNotDate(string column, string op, object value) - { - return Not().WhereDate(column, op, value); - } - public Q OrWhereDate(string column, string op, object value) - { - return Or().WhereDate(column, op, value); - } - public Q OrWhereNotDate(string column, string op, object value) - { - return Or().Not().WhereDate(column, op, value); - } - - public Q WhereTime(string column, string op, object value) - { - return WhereDatePart("time", column, op, value); - } - public Q WhereNotTime(string column, string op, object value) - { - return Not().WhereTime(column, op, value); - } - public Q OrWhereTime(string column, string op, object value) - { - return Or().WhereTime(column, op, value); - } - public Q OrWhereNotTime(string column, string op, object value) - { - return Or().Not().WhereTime(column, op, value); - } - - public Q WhereDatePart(string part, string column, object value) - { - return WhereDatePart(part, column, "=", value); - } - public Q WhereNotDatePart(string part, string column, object value) - { - return WhereNotDatePart(part, column, "=", value); - } - - public Q OrWhereDatePart(string part, string column, object value) - { - return OrWhereDatePart(part, column, "=", value); - } - - public Q OrWhereNotDatePart(string part, string column, object value) - { - return OrWhereNotDatePart(part, column, "=", value); - } - - public Q WhereDate(string column, object value) - { - return WhereDate(column, "=", value); - } - public Q WhereNotDate(string column, object value) - { - return WhereNotDate(column, "=", value); - } - public Q OrWhereDate(string column, object value) - { - return OrWhereDate(column, "=", value); - } - public Q OrWhereNotDate(string column, object value) - { - return OrWhereNotDate(column, "=", value); - } - - public Q WhereTime(string column, object value) - { - return WhereTime(column, "=", value); - } - public Q WhereNotTime(string column, object value) - { - return WhereNotTime(column, "=", value); - } - public Q OrWhereTime(string column, object value) - { - return OrWhereTime(column, "=", value); - } - public Q OrWhereNotTime(string column, object value) - { - return OrWhereNotTime(column, "=", value); - } - - #endregion - } -} diff --git a/QueryBuilder/BaseQuery.cs b/QueryBuilder/BaseQuery.cs deleted file mode 100644 index 86b44a23..00000000 --- a/QueryBuilder/BaseQuery.cs +++ /dev/null @@ -1,310 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; - -namespace SqlKata -{ - public abstract class AbstractQuery - { - public AbstractQuery Parent; - } - - public abstract partial class BaseQuery : AbstractQuery where Q : BaseQuery - { - public List Clauses { get; set; } = new List(); - - private bool orFlag = false; - private bool notFlag = false; - public string EngineScope = null; - - public Q SetEngineScope(string engine) - { - this.EngineScope = engine; - - return (Q)this; - } - - public BaseQuery() - { - } - - /// - /// Return a cloned copy of the current query. - /// - /// - public virtual Q Clone() - { - var q = NewQuery(); - - q.Clauses = this.Clauses.Select(x => x.Clone()).ToList(); - - return q; - } - - public Q SetParent(AbstractQuery parent) - { - if (this == parent) - { - throw new ArgumentException($"Cannot set the same {nameof(AbstractQuery)} as a parent of itself"); - } - - this.Parent = parent; - return (Q)this; - } - - public abstract Q NewQuery(); - - public Q NewChild() - { - var newQuery = NewQuery().SetParent((Q)this); - newQuery.EngineScope = this.EngineScope; - return newQuery; - } - - /// - /// Add a component clause to the query. - /// - /// - /// - /// - /// - public Q AddComponent(string component, AbstractClause clause, string engineCode = null) - { - if (engineCode == null) - { - engineCode = EngineScope; - } - - clause.Engine = engineCode; - clause.Component = component; - Clauses.Add(clause); - - return (Q)this; - } - - /// - /// If the query already contains a clause for the given component - /// and engine, replace it with the specified clause. Otherwise, just - /// add the clause. - /// - /// - /// - /// - /// - public Q AddOrReplaceComponent(string component, AbstractClause clause, string engineCode = null) - { - engineCode = engineCode ?? EngineScope; - - var current = GetComponents(component).SingleOrDefault(c => c.Engine == engineCode); - if (current != null) - Clauses.Remove(current); - - return AddComponent(component, clause, engineCode); - } - - - - /// - /// Get the list of clauses for a component. - /// - /// - public List GetComponents(string component, string engineCode = null) where C : AbstractClause - { - if (engineCode == null) - { - engineCode = EngineScope; - } - - var clauses = Clauses - .Where(x => x.Component == component) - .Where(x => engineCode == null || x.Engine == null || engineCode == x.Engine) - .Cast(); - - return clauses.ToList(); - } - - /// - /// Get the list of clauses for a component. - /// - /// - /// - /// - public List GetComponents(string component, string engineCode = null) - { - if (engineCode == null) - { - engineCode = EngineScope; - } - - return GetComponents(component, engineCode); - } - - /// - /// Get a single component clause from the query. - /// - /// - public C GetOneComponent(string component, string engineCode = null) where C : AbstractClause - { - engineCode = engineCode ?? EngineScope; - - var all = GetComponents(component, engineCode); - return all.FirstOrDefault(c => c.Engine == engineCode) ?? all.FirstOrDefault(c => c.Engine == null); - } - - /// - /// Get a single component clause from the query. - /// - /// - /// - /// - public AbstractClause GetOneComponent(string component, string engineCode = null) - { - if (engineCode == null) - { - engineCode = EngineScope; - } - - return GetOneComponent(component, engineCode); - } - - /// - /// Return whether the query has clauses for a component. - /// - /// - /// - /// - public bool HasComponent(string component, string engineCode = null) - { - if (engineCode == null) - { - engineCode = EngineScope; - } - - return GetComponents(component, engineCode).Any(); - } - - /// - /// Remove all clauses for a component. - /// - /// - /// - /// - public Q ClearComponent(string component, string engineCode = null) - { - if (engineCode == null) - { - engineCode = EngineScope; - } - - Clauses = Clauses - .Where(x => !(x.Component == component && (engineCode == null || x.Engine == null || engineCode == x.Engine))) - .ToList(); - - return (Q)this; - } - - /// - /// Set the next boolean operator to "and" for the "where" clause. - /// - /// - protected Q And() - { - orFlag = false; - return (Q)this; - } - - /// - /// Set the next boolean operator to "or" for the "where" clause. - /// - /// - public Q Or() - { - orFlag = true; - return (Q)this; - } - - /// - /// Set the next "not" operator for the "where" clause. - /// - /// - public Q Not(bool flag = true) - { - notFlag = flag; - return (Q)this; - } - - /// - /// Get the boolean operator and reset it to "and" - /// - /// - protected bool GetOr() - { - var ret = orFlag; - - // reset the flag - orFlag = false; - return ret; - } - - /// - /// Get the "not" operator and clear it - /// - /// - protected bool GetNot() - { - var ret = notFlag; - - // reset the flag - notFlag = false; - return ret; - } - - /// - /// Add a from Clause - /// - /// - /// - public Q From(string table) - { - return AddOrReplaceComponent("from", new FromClause - { - Table = table, - }); - } - - public Q From(Query query, string alias = null) - { - query = query.Clone(); - query.SetParent((Q)this); - - if (alias != null) - { - query.As(alias); - }; - - return AddOrReplaceComponent("from", new QueryFromClause - { - Query = query - }); - } - - public Q FromRaw(string sql, params object[] bindings) - { - return AddOrReplaceComponent("from", new RawFromClause - { - Expression = sql, - Bindings = bindings, - }); - } - - public Q From(Func callback, string alias = null) - { - var query = new Query(); - - query.SetParent((Q)this); - - return From(callback.Invoke(query), alias); - } - - } -} diff --git a/QueryBuilder/Clauses/ConditionClause.cs b/QueryBuilder/Clauses/ConditionClause.cs index 287149e5..4e48e82c 100644 --- a/QueryBuilder/Clauses/ConditionClause.cs +++ b/QueryBuilder/Clauses/ConditionClause.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Linq; namespace SqlKata { @@ -119,7 +120,7 @@ public override AbstractClause Clone() /// /// Represents a comparison between a column and a full "subquery". /// - public class QueryCondition : AbstractCondition where T : BaseQuery + public class QueryCondition : AbstractCondition { public string Column { get; set; } public string Operator { get; set; } @@ -128,7 +129,7 @@ public class QueryCondition : AbstractCondition where T : BaseQuery /// public override AbstractClause Clone() { - return new QueryCondition + return new QueryCondition { Engine = Engine, Column = Column, @@ -144,7 +145,7 @@ public override AbstractClause Clone() /// /// Represents a comparison between a full "subquery" and a value. /// - public class SubQueryCondition : AbstractCondition where T : BaseQuery + public class SubQueryCondition : AbstractCondition { public object Value { get; set; } public string Operator { get; set; } @@ -153,7 +154,7 @@ public class SubQueryCondition : AbstractCondition where T : BaseQuery /// public override AbstractClause Clone() { - return new SubQueryCondition + return new SubQueryCondition { Engine = Engine, Value = Value, @@ -169,17 +170,17 @@ public override AbstractClause Clone() /// /// Represents a "is in" condition. /// - public class InCondition : AbstractCondition + public class InCondition : AbstractCondition { public string Column { get; set; } - public IEnumerable Values { get; set; } + public IEnumerable Values { get; set; } public override AbstractClause Clone() { - return new InCondition + return new InCondition { Engine = Engine, Column = Column, - Values = new List(Values), + Values = new List(Values), IsOr = IsOr, IsNot = IsNot, Component = Component, @@ -212,14 +213,14 @@ public override AbstractClause Clone() /// /// Represents a "is between" condition. /// - public class BetweenCondition : AbstractCondition + public class BetweenCondition : AbstractCondition { public string Column { get; set; } - public T Higher { get; set; } - public T Lower { get; set; } + public object Higher { get; set; } + public object Lower { get; set; } public override AbstractClause Clone() { - return new BetweenCondition + return new BetweenCondition { Engine = Engine, Column = Column, @@ -280,15 +281,23 @@ public override AbstractClause Clone() /// Represents a "nested" clause condition. /// i.e OR (myColumn = "A") /// - public class NestedCondition : AbstractCondition where T : BaseQuery + public class NestedCondition : AbstractCondition { - public T Query { get; set; } + public IClauseContainer Conditions { get; set; } + + [Obsolete("Use Conditions instead.")] + public IClauseContainer Query + { + get => Conditions; + set => Conditions = value; + } + public override AbstractClause Clone() { - return new NestedCondition + return new NestedCondition { Engine = Engine, - Query = Query.Clone(), + Conditions = Conditions?.Clone(), IsOr = IsOr, IsNot = IsNot, Component = Component, diff --git a/QueryBuilder/Compilers/Compiler.Conditions.cs b/QueryBuilder/Compilers/Compiler.Conditions.cs index 6615c246..eba6c516 100644 --- a/QueryBuilder/Compilers/Compiler.Conditions.cs +++ b/QueryBuilder/Compilers/Compiler.Conditions.cs @@ -7,38 +7,26 @@ namespace SqlKata.Compilers { public partial class Compiler { - protected virtual MethodInfo FindCompilerMethodInfo(Type clauseType, string methodName) - { - return _compileConditionMethodsProvider.GetMethodInfo(clauseType, methodName); - } - protected virtual string CompileCondition(SqlResult ctx, AbstractCondition clause) { - var clauseType = clause.GetType(); - - var name = clauseType.Name; - - name = name.Substring(0, name.IndexOf("Condition")); - - var methodName = "Compile" + name + "Condition"; - - var methodInfo = FindCompilerMethodInfo(clauseType, methodName); - - try - { - - var result = methodInfo.Invoke(this, new object[] { - ctx, - clause - }); - - return result as string; - } - catch (Exception ex) + return clause switch { - throw new Exception($"Failed to invoke '{methodName}'", ex); - } - + 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) @@ -68,7 +56,7 @@ protected virtual string CompileRawCondition(SqlResult ctx, RawCondition x) return WrapIdentifiers(x.Expression); } - protected virtual string CompileQueryCondition(SqlResult ctx, QueryCondition x) where T : BaseQuery + protected virtual string CompileQueryCondition(SqlResult ctx, QueryCondition x) { var subCtx = CompileSelectQuery(x.Query); @@ -77,7 +65,7 @@ protected virtual string CompileQueryCondition(SqlResult ctx, QueryCondition< return Wrap(x.Column) + " " + checkOperator(x.Operator) + " (" + subCtx.RawSql + ")"; } - protected virtual string CompileSubQueryCondition(SqlResult ctx, SubQueryCondition x) where T : BaseQuery + protected virtual string CompileSubQueryCondition(SqlResult ctx, SubQueryCondition x) { var subCtx = CompileSelectQuery(x.Query); @@ -168,20 +156,31 @@ protected virtual string CompileBasicDateCondition(SqlResult ctx, BasicDateCondi return x.IsNot ? $"NOT ({sql})" : sql; } - protected virtual string CompileNestedCondition(SqlResult ctx, NestedCondition x) where Q : BaseQuery + protected virtual string CompileNestedCondition(SqlResult ctx, NestedCondition x) { - if (!(x.Query.HasComponent("where", EngineCode) || x.Query.HasComponent("having", EngineCode))) + if (x.Conditions is ConditionGroup group) + { + if (!group.HasConditions(EngineCode)) + { + return null; + } + + var sql = CompileConditions(ctx, group.GetConditions(EngineCode)); + return x.IsNot ? $"NOT ({sql})" : $"({sql})"; + } + + if (!(x.Conditions.HasComponent("where", EngineCode) || x.Conditions.HasComponent("having", EngineCode))) { return null; } - var clause = x.Query.HasComponent("where", EngineCode) ? "where" : "having"; + var clause = x.Conditions.HasComponent("where", EngineCode) ? "where" : "having"; - var clauses = x.Query.GetComponents(clause, EngineCode); + var clauses = x.Conditions.GetComponents(clause, EngineCode); - var sql = CompileConditions(ctx, clauses); + var sql2 = CompileConditions(ctx, clauses); - return x.IsNot ? $"NOT ({sql})" : $"({sql})"; + return x.IsNot ? $"NOT ({sql2})" : $"({sql2})"; } protected string CompileTwoColumnsCondition(SqlResult ctx, TwoColumnsCondition clause) @@ -190,7 +189,7 @@ protected string CompileTwoColumnsCondition(SqlResult ctx, TwoColumnsCondition c return $"{op}{Wrap(clause.First)} {checkOperator(clause.Operator)} {Wrap(clause.Second)}"; } - protected virtual string CompileBetweenCondition(SqlResult ctx, BetweenCondition item) + protected virtual string CompileBetweenCondition(SqlResult ctx, BetweenCondition item) { var between = item.IsNot ? "NOT BETWEEN" : "BETWEEN"; var lower = Parameter(ctx, item.Lower); @@ -199,7 +198,7 @@ protected virtual string CompileBetweenCondition(SqlResult ctx, BetweenCondit return Wrap(item.Column) + $" {between} {lower} AND {higher}"; } - protected virtual string CompileInCondition(SqlResult ctx, InCondition item) + protected virtual string CompileInCondition(SqlResult ctx, InCondition item) { var column = Wrap(item.Column); @@ -210,9 +209,9 @@ protected virtual string CompileInCondition(SqlResult ctx, InCondition ite var inOperator = item.IsNot ? "NOT IN" : "IN"; - var values = Parameterize(ctx, item.Values); + var parameterizedValues = Parameterize(ctx, item.Values); - return column + $" {inOperator} ({values})"; + return column + $" {inOperator} ({parameterizedValues})"; } protected virtual string CompileInQueryCondition(SqlResult ctx, InQueryCondition item) diff --git a/QueryBuilder/Compilers/Compiler.cs b/QueryBuilder/Compilers/Compiler.cs index 5ac6c080..b685e1b0 100644 --- a/QueryBuilder/Compilers/Compiler.cs +++ b/QueryBuilder/Compilers/Compiler.cs @@ -7,7 +7,6 @@ namespace SqlKata.Compilers { public partial class Compiler { - private readonly ConditionsCompilerProvider _compileConditionMethodsProvider; protected virtual string parameterPlaceholder { get; set; } = "?"; protected virtual string parameterPrefix { get; set; } = "@p"; protected virtual string OpeningIdentifier { get; set; } = "\""; @@ -24,7 +23,6 @@ public partial class Compiler protected Compiler() { - _compileConditionMethodsProvider = new ConditionsCompilerProvider(this); } public virtual string EngineCode { get; } @@ -746,8 +744,10 @@ public virtual string CompileJoins(SqlResult ctx) public virtual string CompileJoin(SqlResult ctx, Join join, bool isNested = false) { - var from = join.GetOneComponent("from", EngineCode); - var conditions = join.GetComponents("where", EngineCode); +#pragma warning disable CS0618 + var from = join.FromClause ?? join.GetOneComponent("from", EngineCode); +#pragma warning restore CS0618 + var conditions = join.Conditions.GetConditions(EngineCode); var joinTable = CompileTableExpression(ctx, from); var constraints = CompileConditions(ctx, conditions); diff --git a/QueryBuilder/Compilers/ConditionsCompilerProvider.cs b/QueryBuilder/Compilers/ConditionsCompilerProvider.cs deleted file mode 100644 index 6439da94..00000000 --- a/QueryBuilder/Compilers/ConditionsCompilerProvider.cs +++ /dev/null @@ -1,54 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Reflection; - -namespace SqlKata.Compilers -{ - internal class ConditionsCompilerProvider - { - private readonly Type compilerType; - private readonly Dictionary methodsCache = new Dictionary(); - private readonly object syncRoot = new object(); - - public ConditionsCompilerProvider(Compiler compiler) - { - this.compilerType = compiler.GetType(); - } - - public MethodInfo GetMethodInfo(Type clauseType, string methodName) - { - // The cache key should take the type and the method name into consideration - var cacheKey = methodName + "::" + clauseType.FullName; - - lock (syncRoot) - { - if (methodsCache.ContainsKey(cacheKey)) - { - return methodsCache[cacheKey]; - } - - return methodsCache[cacheKey] = FindMethodInfo(clauseType, methodName); - } - } - - private MethodInfo FindMethodInfo(Type clauseType, string methodName) - { - MethodInfo methodInfo = compilerType - .GetRuntimeMethods() - .FirstOrDefault(x => x.Name == methodName); - - if (methodInfo == null) - { - throw new Exception($"Failed to locate a compiler for '{methodName}'."); - } - - if (clauseType.IsConstructedGenericType && methodInfo.GetGenericArguments().Any()) - { - methodInfo = methodInfo.MakeGenericMethod(clauseType.GenericTypeArguments); - } - - return methodInfo; - } - } -} diff --git a/QueryBuilder/ConditionGroup.cs b/QueryBuilder/ConditionGroup.cs new file mode 100644 index 00000000..d2b39e3f --- /dev/null +++ b/QueryBuilder/ConditionGroup.cs @@ -0,0 +1,148 @@ +using System; +using System.Collections.Generic; +using System.Linq; + +namespace SqlKata +{ + public class ConditionGroup : IConditional + { + private bool orFlag = false; + private bool notFlag = false; + + public Action OnGetOr { get; set; } + public Action OnGetNot { get; set; } + + public string EngineScope { get; set; } = null; + public List Clauses { get; set; } + + public ConditionGroup() + { + Clauses = []; + } + + public virtual ConditionGroup Clone() + { + return new ConditionGroup + { + EngineScope = EngineScope, + Clauses = Clauses.Select(x => x.Clone()).ToList() + }; + } + + IClauseContainer IClauseContainer.Clone() => Clone(); + + public bool HasConditions(string engineCode = null) + { + engineCode ??= EngineScope; + return Clauses.Any(x => engineCode == null || x.Engine == null || engineCode == x.Engine); + } + + public List GetConditions(string engineCode = null) + { + return GetComponents(null, engineCode); + } + + public List GetComponents(string component = null, string engineCode = null) where C : AbstractClause + { + engineCode ??= EngineScope; + + return Clauses + .Where(x => engineCode == null || x.Engine == null || engineCode == x.Engine) + .OfType() + .ToList(); + } + + public List GetComponents(string component = null, string engineCode = null) + { + return GetComponents(component, engineCode); + } + + public C GetOneComponent(string component, string engineCode = null) where C : AbstractClause + { + engineCode ??= EngineScope; + + var all = GetComponents(component, engineCode); + return all.FirstOrDefault(c => c.Engine == engineCode) ?? all.FirstOrDefault(c => c.Engine == null); + } + + public AbstractClause GetOneComponent(string component, string engineCode = null) + { + return GetOneComponent(component, engineCode); + } + + public bool HasComponent(string component, string engineCode = null) + { + engineCode ??= EngineScope; + return GetComponents(component, engineCode).Count != 0; + } + + public ConditionGroup Clear(string engineCode = null) + { + engineCode ??= EngineScope; + + if (engineCode == null) + { + Clauses.Clear(); + } + else + { + Clauses.RemoveAll(x => x.Engine == null || x.Engine == engineCode); + } + + return this; + } + + public bool GetOr() + { + var ret = orFlag; + orFlag = false; + OnGetOr?.Invoke(); + return ret; + } + + public bool GetNot() + { + var ret = notFlag; + notFlag = false; + OnGetNot?.Invoke(); + return ret; + } + + public void SetOr(bool value) + { + orFlag = value; + } + + public void SetNot(bool value) + { + notFlag = value; + } + + public void AddComponent(string component, AbstractClause clause, string engineCode = null) + { + clause.Engine = engineCode ?? EngineScope; + if (component != null && clause.Component == null) + { + clause.Component = component; + } + Clauses.Add(clause); + } + + public IClauseContainer Add(AbstractClause clause, string engineCode = null) + { + AddComponent(clause.Component, clause, engineCode); + return this; + } + + public IClauseContainer Add(IReadOnlyCollection clauses, string engineCode = null) + { + foreach (var clause in clauses) + { + Add(clause, engineCode); + } + return this; + } + + public IConditional NewChild() => new ConditionGroup { EngineScope = this.EngineScope }; + } +} diff --git a/QueryBuilder/Extensions/ConditionExtensions.cs b/QueryBuilder/Extensions/ConditionExtensions.cs new file mode 100644 index 00000000..6855bad7 --- /dev/null +++ b/QueryBuilder/Extensions/ConditionExtensions.cs @@ -0,0 +1,746 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; + +namespace SqlKata +{ + public static class ConditionExtensions + { + public static T Or(this T target) where T : IConditional + { + target.SetOr(true); + return target; + } + + public static T Not(this T target, bool flag = true) where T : IConditional + { + target.SetNot(flag); + return target; + } + + public static T And(this T target) where T : IConditional + { + target.SetOr(false); + return target; + } + + public static T Where(this T target, string column, string op, object value) where T : IConditional + { + if (value == null) + { + return target.Not(op != "=").WhereNull(column); + } + + if (value is bool boolValue) + { + if (op != "=") + { + target.Not(); + } + + return boolValue ? target.WhereTrue(column) : target.WhereFalse(column); + } + + target.AddComponent("where", new BasicCondition + { + Column = column, + Operator = op, + Value = value, + IsOr = target.GetOr(), + IsNot = target.GetNot(), + }); + + return target; + } + + public static T WhereNot(this T target, string column, string op, object value) where T : IConditional + { + return target.Not().Where(column, op, value); + } + + public static T OrWhere(this T target, string column, string op, object value) where T : IConditional + { + return target.Or().Where(column, op, value); + } + + public static T OrWhereNot(this T target, string column, string op, object value) where T : IConditional + { + return target.Or().Not().Where(column, op, value); + } + + public static T Where(this T target, string column, object value) where T : IConditional + { + return target.Where(column, "=", value); + } + + public static T WhereNot(this T target, string column, object value) where T : IConditional + { + return target.WhereNot(column, "=", value); + } + + public static T OrWhere(this T target, string column, object value) where T : IConditional + { + return target.OrWhere(column, "=", value); + } + + public static T OrWhereNot(this T target, string column, object value) where T : IConditional + { + return target.OrWhereNot(column, "=", value); + } + + public static T Where(this T target, object constraints) where T : IConditional + { + var dictionary = new Dictionary(); + + foreach (var item in constraints.GetType().GetRuntimeProperties()) + { + dictionary.Add(item.Name, item.GetValue(constraints)); + } + + return target.Where(dictionary); + } + + public static T Where(this T target, IEnumerable> values) where T : IConditional + { + var orFlag = target.GetOr(); + var notFlag = target.GetNot(); + + foreach (var tuple in values) + { + if (orFlag) + { + target.Or(); + } + else + { + target.And(); + } + + target.Not(notFlag).Where(tuple.Key, tuple.Value); + } + + return target; + } + + public static T WhereRaw(this T target, string sql, params object[] bindings) where T : IConditional + { + target.AddComponent("where", new RawCondition + { + Expression = sql, + Bindings = bindings, + IsOr = target.GetOr(), + IsNot = target.GetNot(), + }); + + return target; + } + + public static T OrWhereRaw(this T target, string sql, params object[] bindings) where T : IConditional + { + return target.Or().WhereRaw(sql, bindings); + } + + public static T Where(this T target, Func callback) where T : IConditional + { + var child = (T)target.NewChild(); + var query = callback.Invoke(child); + + // omit empty queries + if (!query.HasComponent("where")) + { + return target; + } + + target.AddComponent("where", new NestedCondition + { + Conditions = query, + IsNot = target.GetNot(), + IsOr = target.GetOr(), + }); + + return target; + } + + public static T WhereNot(this T target, Func callback) where T : IConditional + { + return target.Not().Where(callback); + } + + public static T OrWhere(this T target, Func callback) where T : IConditional + { + return target.Or().Where(callback); + } + + public static T OrWhereNot(this T target, Func callback) where T : IConditional + { + return target.Not().Or().Where(callback); + } + + public static T WhereColumns(this T target, string first, string op, string second) where T : IConditional + { + target.AddComponent("where", new TwoColumnsCondition + { + First = first, + Second = second, + Operator = op, + IsOr = target.GetOr(), + IsNot = target.GetNot(), + }); + + return target; + } + + public static T OrWhereColumns(this T target, string first, string op, string second) where T : IConditional + { + return target.Or().WhereColumns(first, op, second); + } + + public static T WhereNull(this T target, string column) where T : IConditional + { + target.AddComponent("where", new NullCondition + { + Column = column, + IsOr = target.GetOr(), + IsNot = target.GetNot(), + }); + + return target; + } + + public static T WhereNotNull(this T target, string column) where T : IConditional + { + return target.Not().WhereNull(column); + } + + public static T OrWhereNull(this T target, string column) where T : IConditional + { + return target.Or().WhereNull(column); + } + + public static T OrWhereNotNull(this T target, string column) where T : IConditional + { + return target.Or().Not().WhereNull(column); + } + + public static T WhereTrue(this T target, string column) where T : IConditional + { + target.AddComponent("where", new BooleanCondition + { + Column = column, + Value = true, + IsOr = target.GetOr(), + IsNot = target.GetNot(), + }); + + return target; + } + + public static T OrWhereTrue(this T target, string column) where T : IConditional + { + return target.Or().WhereTrue(column); + } + + public static T WhereFalse(this T target, string column) where T : IConditional + { + target.AddComponent("where", new BooleanCondition + { + Column = column, + Value = false, + IsOr = target.GetOr(), + IsNot = target.GetNot(), + }); + + return target; + } + + public static T OrWhereFalse(this T target, string column) where T : IConditional + { + return target.Or().WhereFalse(column); + } + + public static T WhereLike(this T target, string column, object value, bool caseSensitive = false, string escapeCharacter = null) where T : IConditional + { + target.AddComponent("where", new BasicStringCondition + { + Operator = "like", + Column = column, + Value = value, + CaseSensitive = caseSensitive, + EscapeCharacter = escapeCharacter, + IsOr = target.GetOr(), + IsNot = target.GetNot(), + }); + + return target; + } + + public static T WhereNotLike(this T target, string column, object value, bool caseSensitive = false, string escapeCharacter = null) where T : IConditional + { + return target.Not().WhereLike(column, value, caseSensitive, escapeCharacter); + } + + public static T OrWhereLike(this T target, string column, object value, bool caseSensitive = false, string escapeCharacter = null) where T : IConditional + { + return target.Or().WhereLike(column, value, caseSensitive, escapeCharacter); + } + + public static T OrWhereNotLike(this T target, string column, object value, bool caseSensitive = false, string escapeCharacter = null) where T : IConditional + { + return target.Or().Not().WhereLike(column, value, caseSensitive, escapeCharacter); + } + + public static T WhereStarts(this T target, string column, object value, bool caseSensitive = false, string escapeCharacter = null) where T : IConditional + { + target.AddComponent("where", new BasicStringCondition + { + Operator = "starts", + Column = column, + Value = value, + CaseSensitive = caseSensitive, + EscapeCharacter = escapeCharacter, + IsOr = target.GetOr(), + IsNot = target.GetNot(), + }); + + return target; + } + + public static T WhereNotStarts(this T target, string column, object value, bool caseSensitive = false, string escapeCharacter = null) where T : IConditional + { + return target.Not().WhereStarts(column, value, caseSensitive, escapeCharacter); + } + + public static T OrWhereStarts(this T target, string column, object value, bool caseSensitive = false, string escapeCharacter = null) where T : IConditional + { + return target.Or().WhereStarts(column, value, caseSensitive, escapeCharacter); + } + + public static T OrWhereNotStarts(this T target, string column, object value, bool caseSensitive = false, string escapeCharacter = null) where T : IConditional + { + return target.Or().Not().WhereStarts(column, value, caseSensitive, escapeCharacter); + } + + public static T WhereEnds(this T target, string column, object value, bool caseSensitive = false, string escapeCharacter = null) where T : IConditional + { + target.AddComponent("where", new BasicStringCondition + { + Operator = "ends", + Column = column, + Value = value, + CaseSensitive = caseSensitive, + EscapeCharacter = escapeCharacter, + IsOr = target.GetOr(), + IsNot = target.GetNot(), + }); + + return target; + } + + public static T WhereNotEnds(this T target, string column, object value, bool caseSensitive = false, string escapeCharacter = null) where T : IConditional + { + return target.Not().WhereEnds(column, value, caseSensitive, escapeCharacter); + } + + public static T OrWhereEnds(this T target, string column, object value, bool caseSensitive = false, string escapeCharacter = null) where T : IConditional + { + return target.Or().WhereEnds(column, value, caseSensitive, escapeCharacter); + } + + public static T OrWhereNotEnds(this T target, string column, object value, bool caseSensitive = false, string escapeCharacter = null) where T : IConditional + { + return target.Or().Not().WhereEnds(column, value, caseSensitive, escapeCharacter); + } + + public static T WhereContains(this T target, string column, object value, bool caseSensitive = false, string escapeCharacter = null) where T : IConditional + { + target.AddComponent("where", new BasicStringCondition + { + Operator = "contains", + Column = column, + Value = value, + CaseSensitive = caseSensitive, + EscapeCharacter = escapeCharacter, + IsOr = target.GetOr(), + IsNot = target.GetNot(), + }); + + return target; + } + + public static T WhereNotContains(this T target, string column, object value, bool caseSensitive = false, string escapeCharacter = null) where T : IConditional + { + return target.Not().WhereContains(column, value, caseSensitive, escapeCharacter); + } + + public static T OrWhereContains(this T target, string column, object value, bool caseSensitive = false, string escapeCharacter = null) where T : IConditional + { + return target.Or().WhereContains(column, value, caseSensitive, escapeCharacter); + } + + public static T OrWhereNotContains(this T target, string column, object value, bool caseSensitive = false, string escapeCharacter = null) where T : IConditional + { + return target.Or().Not().WhereContains(column, value, caseSensitive, escapeCharacter); + } + + public static T WhereBetween(this T target, string column, TVal lower, TVal higher) where T : IConditional + { + target.AddComponent("where", new BetweenCondition + { + Column = column, + IsOr = target.GetOr(), + IsNot = target.GetNot(), + Lower = lower, + Higher = higher + }); + + return target; + } + + public static T OrWhereBetween(this T target, string column, TVal lower, TVal higher) where T : IConditional + { + return target.Or().WhereBetween(column, lower, higher); + } + + public static T WhereNotBetween(this T target, string column, TVal lower, TVal higher) where T : IConditional + { + return target.Not().WhereBetween(column, lower, higher); + } + + public static T OrWhereNotBetween(this T target, string column, TVal lower, TVal higher) where T : IConditional + { + return target.Or().Not().WhereBetween(column, lower, higher); + } + + public static T WhereIn(this T target, string column, IEnumerable values) where T : IConditional + { + if (values is string val) + { + target.AddComponent("where", new InCondition + { + Column = column, + IsOr = target.GetOr(), + IsNot = target.GetNot(), + Values = new List { val } + }); + + return target; + } + + target.AddComponent("where", new InCondition + { + Column = column, + IsOr = target.GetOr(), + IsNot = target.GetNot(), + Values = values.Distinct().Cast().ToList() + }); + + return target; + } + + public static T OrWhereIn(this T target, string column, IEnumerable values) where T : IConditional + { + return target.Or().WhereIn(column, values); + } + + public static T WhereNotIn(this T target, string column, IEnumerable values) where T : IConditional + { + return target.Not().WhereIn(column, values); + } + + public static T OrWhereNotIn(this T target, string column, IEnumerable values) where T : IConditional + { + return target.Or().Not().WhereIn(column, values); + } + + public static T WhereIn(this T target, string column, Query query) where T : IConditional + { + target.AddComponent("where", new InQueryCondition + { + Column = column, + IsOr = target.GetOr(), + IsNot = target.GetNot(), + Query = query, + }); + + return target; + } + + public static T WhereIn(this T target, string column, Func callback) where T : IConditional + { + return target.WhereIn(column, callback.Invoke(new Query())); + } + + public static T OrWhereIn(this T target, string column, Query query) where T : IConditional + { + return target.Or().WhereIn(column, query); + } + + public static T OrWhereIn(this T target, string column, Func callback) where T : IConditional + { + return target.Or().WhereIn(column, callback); + } + + public static T WhereNotIn(this T target, string column, Query query) where T : IConditional + { + return target.Not().WhereIn(column, query); + } + + public static T WhereNotIn(this T target, string column, Func callback) where T : IConditional + { + return target.Not().WhereIn(column, callback); + } + + public static T OrWhereNotIn(this T target, string column, Query query) where T : IConditional + { + return target.Or().Not().WhereIn(column, query); + } + + public static T OrWhereNotIn(this T target, string column, Func callback) where T : IConditional + { + return target.Or().Not().WhereIn(column, callback); + } + + public static T Where(this T target, string column, string op, Query query) where T : IConditional + { + target.AddComponent("where", new QueryCondition + { + Column = column, + Operator = op, + Query = query, + IsNot = target.GetNot(), + IsOr = target.GetOr(), + }); + + return target; + } + + public static T Where(this T target, string column, string op, Func callback) where T : IConditional + { + return target.Where(column, op, callback.Invoke(new Query())); + } + + public static T WhereSub(this T target, Query query, object value) where T : IConditional + { + return target.WhereSub(query, "=", value); + } + + public static T WhereSub(this T target, Query query, string op, object value) where T : IConditional + { + target.AddComponent("where", new SubQueryCondition + { + Value = value, + Operator = op, + Query = query, + IsNot = target.GetNot(), + IsOr = target.GetOr(), + }); + + return target; + } + + public static T OrWhereSub(this T target, Query query, object value) where T : IConditional + { + return target.Or().WhereSub(query, value); + } + + public static T OrWhereSub(this T target, Query query, string op, object value) where T : IConditional + { + return target.Or().WhereSub(query, op, value); + } + + public static T OrWhere(this T target, string column, string op, Query query) where T : IConditional + { + return target.Or().Where(column, op, query); + } + + public static T OrWhere(this T target, string column, string op, Func callback) where T : IConditional + { + return target.Or().Where(column, op, callback); + } + + public static T WhereExists(this T target, Query query) where T : IConditional + { + if (!query.HasComponent("from")) + { + throw new ArgumentException($"'{nameof(FromClause)}' cannot be empty if used inside a '{nameof(WhereExists)}' condition"); + } + + target.AddComponent("where", new ExistsCondition + { + Query = query, + IsNot = target.GetNot(), + IsOr = target.GetOr(), + }); + + return target; + } + + public static T WhereExists(this T target, Func callback) where T : IConditional + { + return target.WhereExists(callback.Invoke(new Query())); + } + + public static T WhereNotExists(this T target, Query query) where T : IConditional + { + return target.Not().WhereExists(query); + } + + public static T WhereNotExists(this T target, Func callback) where T : IConditional + { + return target.Not().WhereExists(callback); + } + + public static T OrWhereExists(this T target, Query query) where T : IConditional + { + return target.Or().WhereExists(query); + } + + public static T OrWhereExists(this T target, Func callback) where T : IConditional + { + return target.Or().WhereExists(callback); + } + + public static T OrWhereNotExists(this T target, Query query) where T : IConditional + { + return target.Or().Not().WhereExists(query); + } + + public static T OrWhereNotExists(this T target, Func callback) where T : IConditional + { + return target.Or().Not().WhereExists(callback); + } + + #region date + public static T WhereDatePart(this T target, string part, string column, string op, object value) where T : IConditional + { + target.AddComponent("where", new BasicDateCondition + { + Operator = op, + Column = column, + Value = value, + Part = part?.ToLowerInvariant(), + IsOr = target.GetOr(), + IsNot = target.GetNot(), + }); + + return target; + } + + public static T WhereNotDatePart(this T target, string part, string column, string op, object value) where T : IConditional + { + return target.Not().WhereDatePart(part, column, op, value); + } + + public static T OrWhereDatePart(this T target, string part, string column, string op, object value) where T : IConditional + { + return target.Or().WhereDatePart(part, column, op, value); + } + + public static T OrWhereNotDatePart(this T target, string part, string column, string op, object value) where T : IConditional + { + return target.Or().Not().WhereDatePart(part, column, op, value); + } + + public static T WhereDate(this T target, string column, string op, object value) where T : IConditional + { + return target.WhereDatePart("date", column, op, value); + } + + public static T WhereNotDate(this T target, string column, string op, object value) where T : IConditional + { + return target.Not().WhereDate(column, op, value); + } + + public static T OrWhereDate(this T target, string column, string op, object value) where T : IConditional + { + return target.Or().WhereDate(column, op, value); + } + + public static T OrWhereNotDate(this T target, string column, string op, object value) where T : IConditional + { + return target.Or().Not().WhereDate(column, op, value); + } + + public static T WhereTime(this T target, string column, string op, object value) where T : IConditional + { + return target.WhereDatePart("time", column, op, value); + } + + public static T WhereNotTime(this T target, string column, string op, object value) where T : IConditional + { + return target.Not().WhereTime(column, op, value); + } + + public static T OrWhereTime(this T target, string column, string op, object value) where T : IConditional + { + return target.Or().WhereTime(column, op, value); + } + + public static T OrWhereNotTime(this T target, string column, string op, object value) where T : IConditional + { + return target.Or().Not().WhereTime(column, op, value); + } + + public static T WhereDatePart(this T target, string part, string column, object value) where T : IConditional + { + return target.WhereDatePart(part, column, "=", value); + } + + public static T WhereNotDatePart(this T target, string part, string column, object value) where T : IConditional + { + return target.WhereNotDatePart(part, column, "=", value); + } + + public static T OrWhereDatePart(this T target, string part, string column, object value) where T : IConditional + { + return target.OrWhereDatePart(part, column, "=", value); + } + + public static T OrWhereNotDatePart(this T target, string part, string column, object value) where T : IConditional + { + return target.OrWhereNotDatePart(part, column, "=", value); + } + + public static T WhereDate(this T target, string column, object value) where T : IConditional + { + return target.WhereDate(column, "=", value); + } + + public static T WhereNotDate(this T target, string column, object value) where T : IConditional + { + return target.WhereNotDate(column, "=", value); + } + + public static T OrWhereDate(this T target, string column, object value) where T : IConditional + { + return target.OrWhereDate(column, "=", value); + } + + public static T OrWhereNotDate(this T target, string column, object value) where T : IConditional + { + return target.OrWhereNotDate(column, "=", value); + } + + public static T WhereTime(this T target, string column, object value) where T : IConditional + { + return target.WhereTime(column, "=", value); + } + + public static T WhereNotTime(this T target, string column, object value) where T : IConditional + { + return target.WhereNotTime(column, "=", value); + } + + public static T OrWhereTime(this T target, string column, object value) where T : IConditional + { + return target.OrWhereTime(column, "=", value); + } + + public static T OrWhereNotTime(this T target, string column, object value) where T : IConditional + { + return target.OrWhereNotTime(column, "=", value); + } + #endregion + } +} diff --git a/QueryBuilder/IClauseContainer.cs b/QueryBuilder/IClauseContainer.cs new file mode 100644 index 00000000..9986668a --- /dev/null +++ b/QueryBuilder/IClauseContainer.cs @@ -0,0 +1,11 @@ +using System.Collections.Generic; + +namespace SqlKata +{ + public interface IClauseContainer + { + bool HasComponent(string component, string engineCode = null); + List GetComponents(string component, string engineCode = null) where C : AbstractClause; + IClauseContainer Clone(); + } +} diff --git a/QueryBuilder/IConditional.cs b/QueryBuilder/IConditional.cs new file mode 100644 index 00000000..2cc7b92e --- /dev/null +++ b/QueryBuilder/IConditional.cs @@ -0,0 +1,16 @@ +using System.Collections.Generic; + +namespace SqlKata +{ + public interface IConditional : IClauseContainer + { + string EngineScope { get; } + List Clauses { get; } + bool GetOr(); + bool GetNot(); + void SetOr(bool value); + void SetNot(bool value); + void AddComponent(string component, AbstractClause clause, string engineCode = null); + IConditional NewChild(); + } +} diff --git a/QueryBuilder/Join.cs b/QueryBuilder/Join.cs index ae0969a6..7008ccb0 100644 --- a/QueryBuilder/Join.cs +++ b/QueryBuilder/Join.cs @@ -1,77 +1,274 @@ using System; +using System.Collections.Generic; +using System.Linq; namespace SqlKata { - public class Join : BaseQuery + public class Join : IConditional { protected string _type = "inner join"; + private bool orFlag = false; + private bool notFlag = false; + protected List _clauses = new List(); - public string Type + public ConditionGroup Conditions { get; set; } = new ConditionGroup(); + public AbstractFrom FromClause { get; set; } + + public string EngineScope { get; set; } = null; + + [Obsolete("Use dedicated properties (Conditions, FromClause) instead.")] + public List Clauses { get { - return _type; + var all = new List(_clauses); + if (FromClause != null) + { + all.Add(FromClause); + } + all.AddRange(Conditions.Clauses); + return all; } set { - _type = value.ToUpperInvariant(); + _clauses.Clear(); + Conditions.Clauses.Clear(); + FromClause = null; + if (value != null) + { + foreach (var clause in value) + { + if (clause is AbstractFrom from) + { + FromClause = from; + } + else if (clause is AbstractCondition) + { + Conditions.Clauses.Add(clause); + } + else + { + _clauses.Add(clause); + } + } + } } } - public Join() : base() + public string Type + { + get => _type; + set => _type = value?.ToUpperInvariant(); + } + + public Join() { } - public override Join Clone() + public virtual Join Clone() { - var clone = base.Clone(); - clone._type = _type; + var clone = new Join + { + _type = _type, + EngineScope = EngineScope, + FromClause = FromClause?.Clone() as AbstractFrom, + Conditions = Conditions.Clone(), + _clauses = _clauses.Select(x => x.Clone()).ToList() + }; return clone; } + IClauseContainer IClauseContainer.Clone() => Clone(); + public Join AsType(string type) { Type = type; return this; } - /// - /// Alias for "from" operator. - /// Since "from" does not sound well with join clauses - /// - /// - /// - public Join JoinWith(string table) => From(table); - public Join JoinWith(Query query) => From(query); - public Join JoinWith(Func callback) => From(callback); - public Join AsInner() => AsType("inner join"); public Join AsOuter() => AsType("outer join"); public Join AsLeft() => AsType("left join"); public Join AsRight() => AsType("right join"); public Join AsCross() => AsType("cross join"); - public Join On(string first, string second, string op = "=") + public Join JoinWith(string table) + { + FromClause = new FromClause { Table = table, Engine = EngineScope, Component = "from" }; + return this; + } + + public Join JoinWith(Query query, string alias = null) + { + query = query.Clone(); + if (alias != null) + { + query.As(alias); + } + FromClause = new QueryFromClause { Query = query, Engine = EngineScope, Component = "from" }; + return this; + } + + public Join JoinWith(Func callback, string alias = null) + { + return JoinWith(callback.Invoke(new Query()), alias); + } + + public Join JoinWithRaw(string sql, params object[] bindings) + { + FromClause = new RawFromClause { Expression = sql, Bindings = bindings, Engine = EngineScope, Component = "from" }; + return this; + } + + /// + /// Alias for "JoinWith" operator. + /// + public Join From(string table) => JoinWith(table); + public Join From(Query query, string alias = null) => JoinWith(query, alias); + public Join From(Func callback, string alias = null) => JoinWith(callback, alias); + public Join FromRaw(string sql, params object[] bindings) => JoinWithRaw(sql, bindings); + + [Obsolete("Use dedicated properties (Conditions, FromClause) instead.")] + public Join AddComponent(string component, AbstractClause clause, string engineCode = null) + { + engineCode ??= EngineScope; + clause.Engine = engineCode; + clause.Component = component; + + if (clause is AbstractFrom from) + { + FromClause = from; + } + else if (clause is AbstractCondition) + { + Conditions.AddComponent(component, clause, engineCode); + } + else + { + _clauses.Add(clause); + } + + return this; + } + + [Obsolete("Use dedicated properties (Conditions, FromClause) instead.")] + public Join AddOrReplaceComponent(string component, AbstractClause clause, string engineCode = null) + { + return AddComponent(component, clause, engineCode); + } + + [Obsolete("Use dedicated properties (Conditions, FromClause) instead.")] + public List GetComponents(string component, string engineCode = null) where C : AbstractClause { - return AddComponent("where", new TwoColumnsCondition + engineCode ??= EngineScope; + + if (component == "from" && FromClause is C cFrom && (engineCode == null || FromClause.Engine == null || FromClause.Engine == engineCode)) + { + return new List { cFrom }; + } + + if (component == "where") { - First = first, - Second = second, - Operator = op, - IsOr = GetOr(), - IsNot = GetNot() - }); + return Conditions.GetComponents(null, engineCode); + } + + return _clauses + .Where(x => x.Component == component) + .Where(x => engineCode == null || x.Engine == null || engineCode == x.Engine) + .Cast() + .ToList(); + } + [Obsolete("Use dedicated properties (Conditions, FromClause) instead.")] + public List GetComponents(string component, string engineCode = null) + { + return GetComponents(component, engineCode); } - public Join OrOn(string first, string second, string op = "=") + [Obsolete("Use dedicated properties (Conditions, FromClause) instead.")] + public C GetOneComponent(string component, string engineCode = null) where C : AbstractClause { - return Or().On(first, second, op); + engineCode ??= EngineScope; + var all = GetComponents(component, engineCode); + return all.FirstOrDefault(c => c.Engine == engineCode) ?? all.FirstOrDefault(c => c.Engine == null); } - public override Join NewQuery() + [Obsolete("Use dedicated properties (Conditions, FromClause) instead.")] + public AbstractClause GetOneComponent(string component, string engineCode = null) { - return new Join(); + return GetOneComponent(component, engineCode); } + + [Obsolete("Use dedicated properties (Conditions, FromClause) instead.")] + public bool HasComponent(string component, string engineCode = null) + { + engineCode ??= EngineScope; + if (component == "from") return FromClause != null && (engineCode == null || FromClause.Engine == null || FromClause.Engine == engineCode); + if (component == "where") return Conditions.HasConditions(engineCode); + return _clauses.Any(x => x.Component == component && (engineCode == null || x.Engine == null || engineCode == x.Engine)); + } + + [Obsolete("Use dedicated properties (Conditions, FromClause) instead.")] + public Join ClearComponent(string component, string engineCode = null) + { + engineCode ??= EngineScope; + + if (component == "from") + { + if (engineCode == null || FromClause?.Engine == engineCode) + { + FromClause = null; + } + } + else if (component == "where") + { + Conditions.Clear(engineCode); + } + else + { + _clauses.RemoveAll(x => x.Component == component && (engineCode == null || x.Engine == null || engineCode == x.Engine)); + } + + return this; + } + + public Join Or() + { + orFlag = true; + return this; + } + + public Join Not(bool flag = true) + { + notFlag = flag; + return this; + } + + public bool GetOr() + { + var ret = orFlag; + orFlag = false; + return ret; + } + + public bool GetNot() + { + var ret = notFlag; + notFlag = false; + return ret; + } + + public void SetOr(bool value) => orFlag = value; + public void SetNot(bool value) => notFlag = value; + + void IConditional.AddComponent(string component, AbstractClause clause, string engineCode) + { + Conditions.AddComponent("where", clause, engineCode); + } + + IConditional IConditional.NewChild() => new Join { EngineScope = this.EngineScope }; + + public Join On(string first, string second, string op = "=") => this.WhereColumns(first, op, second); + public Join OrOn(string first, string second, string op = "=") => this.Or().WhereColumns(first, op, second); } } diff --git a/QueryBuilder/Query.Aggregate.cs b/QueryBuilder/Query.Aggregate.cs index d4fc5057..174b9106 100644 --- a/QueryBuilder/Query.Aggregate.cs +++ b/QueryBuilder/Query.Aggregate.cs @@ -14,7 +14,7 @@ public Query AsAggregate(string type, string[] columns = null) .AddComponent("aggregate", new AggregateClause { Type = type, - Columns = columns?.ToList() ?? new List(), + Columns = columns?.ToList() ?? [], }); return this; @@ -22,19 +22,19 @@ public Query AsAggregate(string type, string[] columns = null) public Query AsCount(string[] columns = null) { - var cols = columns?.ToList() ?? new List { }; + var cols = columns?.ToList() ?? []; - if (!cols.Any()) + if (cols.Count == 0) { cols.Add("*"); } - return AsAggregate("count", cols.ToArray()); + return AsAggregate("count", [.. cols]); } public Query AsAvg(string column) { - return AsAggregate("avg", new string[] { column }); + return AsAggregate("avg", [column]); } public Query AsAverage(string column) { @@ -43,17 +43,17 @@ public Query AsAverage(string column) public Query AsSum(string column) { - return AsAggregate("sum", new[] { column }); + return AsAggregate("sum", [column]); } public Query AsMax(string column) { - return AsAggregate("max", new[] { column }); + return AsAggregate("max", [column]); } public Query AsMin(string column) { - return AsAggregate("min", new[] { column }); + return AsAggregate("min", [column]); } } } diff --git a/QueryBuilder/Query.From.cs b/QueryBuilder/Query.From.cs new file mode 100644 index 00000000..a5a3a82d --- /dev/null +++ b/QueryBuilder/Query.From.cs @@ -0,0 +1,54 @@ +using System; + +namespace SqlKata +{ + public partial class Query + { + /// + /// Add a from Clause + /// + /// + /// + public Query From(string table) + { + return AddOrReplaceComponent("from", new FromClause + { + Table = table, + }); + } + + public Query From(Query query, string alias = null) + { + query = query.Clone(); + query.SetParent(this); + + if (alias != null) + { + query.As(alias); + } + + return AddOrReplaceComponent("from", new QueryFromClause + { + Query = query + }); + } + + public Query FromRaw(string sql, params object[] bindings) + { + return AddOrReplaceComponent("from", new RawFromClause + { + Expression = sql, + Bindings = bindings, + }); + } + + public Query From(Func callback, string alias = null) + { + var query = new Query(); + + query.SetParent(this); + + return From(callback.Invoke(query), alias); + } + } +} diff --git a/QueryBuilder/Query.Having.cs b/QueryBuilder/Query.Having.cs index ee367941..834625c9 100644 --- a/QueryBuilder/Query.Having.cs +++ b/QueryBuilder/Query.Having.cs @@ -1,7 +1,5 @@ using System; using System.Collections.Generic; -using System.Linq; -using System.Reflection; namespace SqlKata { @@ -9,648 +7,560 @@ public partial class Query { public Query Having(string column, string op, object value) { - - // If the value is "null", we will just assume the developer wants to add a - // Having null clause to the query. So, we will allow a short-cut here to - // that method for convenience so the developer doesn't have to check. - if (value == null) - { - return Not(op != "=").HavingNull(column); - } - - return AddComponent("having", new BasicCondition - { - Column = column, - Operator = op, - Value = value, - IsOr = GetOr(), - IsNot = GetNot(), - }); + Havings.Where(column, op, value); + return this; } public Query HavingNot(string column, string op, object value) { - return Not().Having(column, op, value); + Havings.WhereNot(column, op, value); + return this; } public Query OrHaving(string column, string op, object value) { - return Or().Having(column, op, value); + Havings.OrWhere(column, op, value); + return this; } public Query OrHavingNot(string column, string op, object value) { - return this.Or().Not().Having(column, op, value); + Havings.OrWhereNot(column, op, value); + return this; } - public Query Having(string column, object value) + public Query Having(string column, object value) => Having(column, "=", value); + public Query HavingNot(string column, object value) => HavingNot(column, "=", value); + public Query OrHaving(string column, object value) => OrHaving(column, "=", value); + public Query OrHavingNot(string column, object value) => OrHavingNot(column, "=", value); + + public Query Having(object constraints) { - return Having(column, "=", value); + Havings.Where(constraints); + return this; } - public Query HavingNot(string column, object value) + + public Query Having(IEnumerable> values) { - return HavingNot(column, "=", value); + Havings.Where(values); + return this; } - public Query OrHaving(string column, object value) + + public Query HavingRaw(string sql, params object[] bindings) { - return OrHaving(column, "=", value); + Havings.WhereRaw(sql, bindings); + return this; } - public Query OrHavingNot(string column, object value) + + public Query OrHavingRaw(string sql, params object[] bindings) { - return OrHavingNot(column, "=", value); + Havings.OrWhereRaw(sql, bindings); + return this; } - /// - /// Perform a Having constraint - /// - /// - /// - public Query Having(object constraints) + public Query Having(Func callback) { - var dictionary = new Dictionary(); + var query = callback.Invoke(NewChild()); - foreach (var item in constraints.GetType().GetRuntimeProperties()) + if (query.Havings.HasConditions(EngineScope)) { - dictionary.Add(item.Name, item.GetValue(constraints)); + Havings.AddComponent("having", new NestedCondition + { + Conditions = query.Havings, + IsNot = Havings.GetNot(), + IsOr = Havings.GetOr(), + }); } - return Having(dictionary); + return this; } - public Query Having(IEnumerable> values) + public Query Having(ConditionGroup group) { - var query = this; - var orFlag = GetOr(); - var notFlag = GetNot(); - - foreach (var tuple in values) + if (group.HasConditions(EngineScope)) { - if (orFlag) - { - query = query.Or(); - } - else + Havings.AddComponent("having", new NestedCondition { - query.And(); - } - - query = this.Not(notFlag).Having(tuple.Key, tuple.Value); + Conditions = group, + IsNot = Havings.GetNot(), + IsOr = Havings.GetOr(), + }); } - return query; + return this; } - public Query HavingRaw(string sql, params object[] bindings) + public Query HavingNot(Func callback) { - return AddComponent("having", new RawCondition - { - Expression = sql, - Bindings = bindings, - IsOr = GetOr(), - IsNot = GetNot(), - }); + Havings.Not(); + return Having(callback); } - public Query OrHavingRaw(string sql, params object[] bindings) + public Query HavingNot(ConditionGroup group) { - return Or().HavingRaw(sql, bindings); + Havings.Not(); + return Having(group); } - /// - /// Apply a nested Having clause - /// - /// - /// - public Query Having(Func callback) + public Query OrHaving(Func callback) { - var query = callback.Invoke(NewChild()); - - return AddComponent("having", new NestedCondition - { - Query = query, - IsNot = GetNot(), - IsOr = GetOr(), - }); + Havings.Or(); + return Having(callback); } - public Query HavingNot(Func callback) + public Query OrHaving(ConditionGroup group) { - return Not().Having(callback); + Havings.Or(); + return Having(group); } - public Query OrHaving(Func callback) + public Query OrHavingNot(Func callback) { - return Or().Having(callback); + Havings.Or().Not(); + return Having(callback); } - public Query OrHavingNot(Func callback) + public Query OrHavingNot(ConditionGroup group) { - return Not().Or().Having(callback); + Havings.Or().Not(); + return Having(group); } public Query HavingColumns(string first, string op, string second) { - return AddComponent("having", new TwoColumnsCondition - { - First = first, - Second = second, - Operator = op, - IsOr = GetOr(), - IsNot = GetNot(), - }); + Havings.WhereColumns(first, op, second); + return this; } public Query OrHavingColumns(string first, string op, string second) { - return Or().HavingColumns(first, op, second); + Havings.OrWhereColumns(first, op, second); + return this; } public Query HavingNull(string column) { - return AddComponent("having", new NullCondition - { - Column = column, - IsOr = GetOr(), - IsNot = GetNot(), - }); + Havings.WhereNull(column); + return this; } public Query HavingNotNull(string column) { - return Not().HavingNull(column); + Havings.WhereNotNull(column); + return this; } public Query OrHavingNull(string column) { - return this.Or().HavingNull(column); + Havings.OrWhereNull(column); + return this; } public Query OrHavingNotNull(string column) { - return Or().Not().HavingNull(column); + Havings.OrWhereNotNull(column); + return this; } public Query HavingTrue(string column) { - return AddComponent("having", new BooleanCondition - { - Column = column, - Value = true, - }); + Havings.WhereTrue(column); + return this; } - public Query OrHavingTrue(string column) + public Query HavingFalse(string column) { - return Or().HavingTrue(column); + Havings.WhereFalse(column); + return this; } - public Query HavingFalse(string column) + public Query OrHavingTrue(string column) { - return AddComponent("having", new BooleanCondition - { - Column = column, - Value = false, - }); + Havings.OrWhereTrue(column); + return this; } public Query OrHavingFalse(string column) { - return Or().HavingFalse(column); + Havings.OrWhereFalse(column); + return this; } public Query HavingLike(string column, object value, bool caseSensitive = false, string escapeCharacter = null) { - return AddComponent("having", new BasicStringCondition - { - Operator = "like", - Column = column, - Value = value, - CaseSensitive = caseSensitive, - EscapeCharacter = escapeCharacter, - IsOr = GetOr(), - IsNot = GetNot(), - }); + Havings.WhereLike(column, value, caseSensitive, escapeCharacter); + return this; } public Query HavingNotLike(string column, object value, bool caseSensitive = false, string escapeCharacter = null) { - return Not().HavingLike(column, value, caseSensitive, escapeCharacter); + Havings.WhereNotLike(column, value, caseSensitive, escapeCharacter); + return this; } public Query OrHavingLike(string column, object value, bool caseSensitive = false, string escapeCharacter = null) { - return Or().HavingLike(column, value, caseSensitive, escapeCharacter); + Havings.OrWhereLike(column, value, caseSensitive, escapeCharacter); + return this; } public Query OrHavingNotLike(string column, object value, bool caseSensitive = false, string escapeCharacter = null) { - return Or().Not().HavingLike(column, value, caseSensitive, escapeCharacter); + Havings.OrWhereNotLike(column, value, caseSensitive, escapeCharacter); + return this; } + public Query HavingStarts(string column, object value, bool caseSensitive = false, string escapeCharacter = null) { - return AddComponent("having", new BasicStringCondition - { - Operator = "starts", - Column = column, - Value = value, - CaseSensitive = caseSensitive, - EscapeCharacter = escapeCharacter, - IsOr = GetOr(), - IsNot = GetNot(), - }); + Havings.WhereStarts(column, value, caseSensitive, escapeCharacter); + return this; } public Query HavingNotStarts(string column, object value, bool caseSensitive = false, string escapeCharacter = null) { - return Not().HavingStarts(column, value, caseSensitive, escapeCharacter); + Havings.WhereNotStarts(column, value, caseSensitive, escapeCharacter); + return this; } public Query OrHavingStarts(string column, object value, bool caseSensitive = false, string escapeCharacter = null) { - return Or().HavingStarts(column, value, caseSensitive, escapeCharacter); + Havings.OrWhereStarts(column, value, caseSensitive, escapeCharacter); + return this; } public Query OrHavingNotStarts(string column, object value, bool caseSensitive = false, string escapeCharacter = null) { - return Or().Not().HavingStarts(column, value, caseSensitive, escapeCharacter); + Havings.OrWhereNotStarts(column, value, caseSensitive, escapeCharacter); + return this; } public Query HavingEnds(string column, object value, bool caseSensitive = false, string escapeCharacter = null) { - return AddComponent("having", new BasicStringCondition - { - Operator = "ends", - Column = column, - Value = value, - CaseSensitive = caseSensitive, - EscapeCharacter = escapeCharacter, - IsOr = GetOr(), - IsNot = GetNot(), - }); + Havings.WhereEnds(column, value, caseSensitive, escapeCharacter); + return this; } public Query HavingNotEnds(string column, object value, bool caseSensitive = false, string escapeCharacter = null) { - return Not().HavingEnds(column, value, caseSensitive, escapeCharacter); + Havings.WhereNotEnds(column, value, caseSensitive, escapeCharacter); + return this; } public Query OrHavingEnds(string column, object value, bool caseSensitive = false, string escapeCharacter = null) { - return Or().HavingEnds(column, value, caseSensitive, escapeCharacter); + Havings.OrWhereEnds(column, value, caseSensitive, escapeCharacter); + return this; } public Query OrHavingNotEnds(string column, object value, bool caseSensitive = false, string escapeCharacter = null) { - return Or().Not().HavingEnds(column, value, caseSensitive, escapeCharacter); + Havings.OrWhereNotEnds(column, value, caseSensitive, escapeCharacter); + return this; } public Query HavingContains(string column, object value, bool caseSensitive = false, string escapeCharacter = null) { - return AddComponent("having", new BasicStringCondition - { - Operator = "contains", - Column = column, - Value = value, - CaseSensitive = caseSensitive, - EscapeCharacter = escapeCharacter, - IsOr = GetOr(), - IsNot = GetNot(), - }); + Havings.WhereContains(column, value, caseSensitive, escapeCharacter); + return this; } public Query HavingNotContains(string column, object value, bool caseSensitive = false, string escapeCharacter = null) { - return Not().HavingContains(column, value, caseSensitive, escapeCharacter); + Havings.WhereNotContains(column, value, caseSensitive, escapeCharacter); + return this; } public Query OrHavingContains(string column, object value, bool caseSensitive = false, string escapeCharacter = null) { - return Or().HavingContains(column, value, caseSensitive, escapeCharacter); + Havings.OrWhereContains(column, value, caseSensitive, escapeCharacter); + return this; } public Query OrHavingNotContains(string column, object value, bool caseSensitive = false, string escapeCharacter = null) { - return Or().Not().HavingContains(column, value, caseSensitive, escapeCharacter); + Havings.OrWhereNotContains(column, value, caseSensitive, escapeCharacter); + return this; } public Query HavingBetween(string column, T lower, T higher) { - return AddComponent("having", new BetweenCondition - { - Column = column, - IsOr = GetOr(), - IsNot = GetNot(), - Lower = lower, - Higher = higher - }); + Havings.WhereBetween(column, lower, higher); + return this; } - public Query OrHavingBetween(string column, T lower, T higher) + public Query HavingNotBetween(string column, T lower, T higher) { - return Or().HavingBetween(column, lower, higher); + Havings.WhereNotBetween(column, lower, higher); + return this; } - public Query HavingNotBetween(string column, T lower, T higher) + + public Query OrHavingBetween(string column, T lower, T higher) { - return Not().HavingBetween(column, lower, higher); + Havings.OrWhereBetween(column, lower, higher); + return this; } + public Query OrHavingNotBetween(string column, T lower, T higher) { - return Or().Not().HavingBetween(column, lower, higher); + Havings.OrWhereNotBetween(column, lower, higher); + return this; } public Query HavingIn(string column, IEnumerable values) { - // If the developer has passed a string they most likely want a List - // since string is considered as List - if (values is string) - { - string val = values as string; - - return AddComponent("having", new InCondition - { - Column = column, - IsOr = GetOr(), - IsNot = GetNot(), - Values = new List { val } - }); - } - - return AddComponent("having", new InCondition - { - Column = column, - IsOr = GetOr(), - IsNot = GetNot(), - Values = values.Distinct().ToList() - }); - - + Havings.WhereIn(column, values); + return this; } - public Query OrHavingIn(string column, IEnumerable values) + public Query HavingNotIn(string column, IEnumerable values) { - return Or().HavingIn(column, values); + Havings.WhereNotIn(column, values); + return this; } - public Query HavingNotIn(string column, IEnumerable values) + public Query OrHavingIn(string column, IEnumerable values) { - return Not().HavingIn(column, values); + Havings.OrWhereIn(column, values); + return this; } public Query OrHavingNotIn(string column, IEnumerable values) { - return Or().Not().HavingIn(column, values); + Havings.OrWhereNotIn(column, values); + return this; } - public Query HavingIn(string column, Query query) { - return AddComponent("having", new InQueryCondition - { - Column = column, - IsOr = GetOr(), - IsNot = GetNot(), - Query = query, - }); + Havings.WhereIn(column, query); + return this; } + public Query HavingIn(string column, Func callback) { - var query = callback.Invoke(new Query()); - - return HavingIn(column, query); + Havings.WhereIn(column, callback); + return this; } - public Query OrHavingIn(string column, Query query) + public Query HavingNotIn(string column, Query query) { - return Or().HavingIn(column, query); + Havings.WhereNotIn(column, query); + return this; } - public Query OrHavingIn(string column, Func callback) + public Query HavingNotIn(string column, Func callback) { - return Or().HavingIn(column, callback); + Havings.WhereNotIn(column, callback); + return this; } - public Query HavingNotIn(string column, Query query) + + public Query OrHavingIn(string column, Query query) { - return Not().HavingIn(column, query); + Havings.OrWhereIn(column, query); + return this; } - public Query HavingNotIn(string column, Func callback) + public Query OrHavingIn(string column, Func callback) { - return Not().HavingIn(column, callback); + Havings.OrWhereIn(column, callback); + return this; } public Query OrHavingNotIn(string column, Query query) { - return Or().Not().HavingIn(column, query); + Havings.OrWhereNotIn(column, query); + return this; } public Query OrHavingNotIn(string column, Func callback) { - return Or().Not().HavingIn(column, callback); + Havings.OrWhereNotIn(column, callback); + return this; } + public Query Having(string column, string op, Query query) + { + Havings.Where(column, op, query); + return this; + } - /// - /// Perform a sub query Having clause - /// - /// - /// - /// - /// public Query Having(string column, string op, Func callback) { - var query = callback.Invoke(NewChild()); + Havings.Where(column, op, callback); + return this; + } - return Having(column, op, query); + public Query OrHaving(string column, string op, Query query) + { + Havings.OrWhere(column, op, query); + return this; } - public Query Having(string column, string op, Query query) + public Query OrHaving(string column, string op, Func callback) { - return AddComponent("having", new QueryCondition - { - Column = column, - Operator = op, - Query = query, - IsNot = GetNot(), - IsOr = GetOr(), - }); + Havings.OrWhere(column, op, callback); + return this; } - public Query OrHaving(string column, string op, Query query) + public Query HavingSub(Query query, object value) { - return Or().Having(column, op, query); + Havings.WhereSub(query, value); + return this; } - public Query OrHaving(string column, string op, Func callback) + + public Query HavingSub(Query query, string op, object value) { - return Or().Having(column, op, callback); + Havings.WhereSub(query, op, value); + return this; } - public Query HavingExists(Query query) + public Query OrHavingSub(Query query, object value) { - if (!query.HasComponent("from")) - { - throw new ArgumentException($"{nameof(FromClause)} cannot be empty if used inside a {nameof(HavingExists)} condition"); - } + Havings.OrWhereSub(query, value); + return this; + } - // simplify the query as much as possible - query = query.Clone().ClearComponent("select") - .SelectRaw("1") - .Limit(1); + public Query OrHavingSub(Query query, string op, object value) + { + Havings.OrWhereSub(query, op, value); + return this; + } - return AddComponent("having", new ExistsCondition - { - Query = query, - IsNot = GetNot(), - IsOr = GetOr(), - }); + public Query HavingExists(Query query) + { + Havings.WhereExists(query); + return this; } + public Query HavingExists(Func callback) { - var childQuery = new Query().SetParent(this); - return HavingExists(callback.Invoke(childQuery)); + Havings.WhereExists(callback); + return this; } public Query HavingNotExists(Query query) { - return Not().HavingExists(query); + Havings.WhereNotExists(query); + return this; } public Query HavingNotExists(Func callback) { - return Not().HavingExists(callback); + Havings.WhereNotExists(callback); + return this; } public Query OrHavingExists(Query query) { - return Or().HavingExists(query); + Havings.OrWhereExists(query); + return this; } + public Query OrHavingExists(Func callback) { - return Or().HavingExists(callback); + Havings.OrWhereExists(callback); + return this; } + public Query OrHavingNotExists(Query query) { - return Or().Not().HavingExists(query); + Havings.OrWhereNotExists(query); + return this; } + public Query OrHavingNotExists(Func callback) { - return Or().Not().HavingExists(callback); + Havings.OrWhereNotExists(callback); + return this; } - #region date + #region Date public Query HavingDatePart(string part, string column, string op, object value) { - return AddComponent("having", new BasicDateCondition - { - Operator = op, - Column = column, - Value = value, - Part = part, - IsOr = GetOr(), - IsNot = GetNot(), - }); + Havings.WhereDatePart(part, column, op, value); + return this; } + public Query HavingNotDatePart(string part, string column, string op, object value) { - return Not().HavingDatePart(part, column, op, value); + Havings.WhereNotDatePart(part, column, op, value); + return this; } public Query OrHavingDatePart(string part, string column, string op, object value) { - return Or().HavingDatePart(part, column, op, value); + Havings.OrWhereDatePart(part, column, op, value); + return this; } public Query OrHavingNotDatePart(string part, string column, string op, object value) { - return Or().Not().HavingDatePart(part, column, op, value); + Havings.OrWhereNotDatePart(part, column, op, value); + return this; } public Query HavingDate(string column, string op, object value) { - return HavingDatePart("date", column, op, value); + Havings.WhereDate(column, op, value); + return this; } + public Query HavingNotDate(string column, string op, object value) { - return Not().HavingDate(column, op, value); + Havings.WhereNotDate(column, op, value); + return this; } + public Query OrHavingDate(string column, string op, object value) { - return Or().HavingDate(column, op, value); + Havings.OrWhereDate(column, op, value); + return this; } + public Query OrHavingNotDate(string column, string op, object value) { - return Or().Not().HavingDate(column, op, value); + Havings.OrWhereNotDate(column, op, value); + return this; } public Query HavingTime(string column, string op, object value) { - return HavingDatePart("time", column, op, value); - } - public Query HavingNotTime(string column, string op, object value) - { - return Not().HavingTime(column, op, value); - } - public Query OrHavingTime(string column, string op, object value) - { - return Or().HavingTime(column, op, value); - } - public Query OrHavingNotTime(string column, string op, object value) - { - return Or().Not().HavingTime(column, op, value); + Havings.WhereTime(column, op, value); + return this; } - public Query HavingDatePart(string part, string column, object value) - { - return HavingDatePart(part, column, "=", value); - } - public Query HavingNotDatePart(string part, string column, object value) + public Query HavingNotTime(string column, string op, object value) { - return HavingNotDatePart(part, column, "=", value); + Havings.WhereNotTime(column, op, value); + return this; } - public Query OrHavingDatePart(string part, string column, object value) + public Query OrHavingTime(string column, string op, object value) { - return OrHavingDatePart(part, column, "=", value); + Havings.OrWhereTime(column, op, value); + return this; } - public Query OrHavingNotDatePart(string part, string column, object value) + public Query OrHavingNotTime(string column, string op, object value) { - return OrHavingNotDatePart(part, column, "=", value); + Havings.OrWhereNotTime(column, op, value); + return this; } - public Query HavingDate(string column, object value) - { - return HavingDate(column, "=", value); - } - public Query HavingNotDate(string column, object value) - { - return HavingNotDate(column, "=", value); - } - public Query OrHavingDate(string column, object value) - { - return OrHavingDate(column, "=", value); - } - public Query OrHavingNotDate(string column, object value) - { - return OrHavingNotDate(column, "=", value); - } + public Query HavingDatePart(string part, string column, object value) => HavingDatePart(part, column, "=", value); + public Query HavingNotDatePart(string part, string column, object value) => HavingNotDatePart(part, column, "=", value); + public Query OrHavingDatePart(string part, string column, object value) => OrHavingDatePart(part, column, "=", value); + public Query OrHavingNotDatePart(string part, string column, object value) => OrHavingNotDatePart(part, column, "=", value); - public Query HavingTime(string column, object value) - { - return HavingTime(column, "=", value); - } - public Query HavingNotTime(string column, object value) - { - return HavingNotTime(column, "=", value); - } - public Query OrHavingTime(string column, object value) - { - return OrHavingTime(column, "=", value); - } - public Query OrHavingNotTime(string column, object value) - { - return OrHavingNotTime(column, "=", value); - } + public Query HavingDate(string column, object value) => HavingDate(column, "=", value); + public Query HavingNotDate(string column, object value) => HavingNotDate(column, "=", value); + public Query OrHavingDate(string column, object value) => OrHavingDate(column, "=", value); + public Query OrHavingNotDate(string column, object value) => OrHavingNotDate(column, "=", value); + public Query HavingTime(string column, object value) => HavingTime(column, "=", value); + public Query HavingNotTime(string column, object value) => HavingNotTime(column, "=", value); + public Query OrHavingTime(string column, object value) => OrHavingTime(column, "=", value); + public Query OrHavingNotTime(string column, object value) => OrHavingNotTime(column, "=", value); #endregion } } diff --git a/QueryBuilder/Query.Join.cs b/QueryBuilder/Query.Join.cs index 3fe3bbb1..55929816 100644 --- a/QueryBuilder/Query.Join.cs +++ b/QueryBuilder/Query.Join.cs @@ -9,10 +9,15 @@ private Query Join(Func callback) { var join = callback.Invoke(new Join().AsInner()); - return AddComponent("join", new BaseJoin + var baseJoin = new BaseJoin { - Join = join - }); + Join = join, + Component = "join", + Engine = EngineScope + }; + Joins.Add(baseJoin); + + return this; } public Query Join( diff --git a/QueryBuilder/Query.Where.cs b/QueryBuilder/Query.Where.cs new file mode 100644 index 00000000..54a515e6 --- /dev/null +++ b/QueryBuilder/Query.Where.cs @@ -0,0 +1,566 @@ +using System; +using System.Collections.Generic; + +namespace SqlKata +{ + public partial class Query + { + public Query Where(string column, string op, object value) + { + Wheres.Where(column, op, value); + return this; + } + + public Query WhereNot(string column, string op, object value) + { + Wheres.WhereNot(column, op, value); + return this; + } + + public Query OrWhere(string column, string op, object value) + { + Wheres.OrWhere(column, op, value); + return this; + } + + public Query OrWhereNot(string column, string op, object value) + { + Wheres.OrWhereNot(column, op, value); + return this; + } + + public Query Where(string column, object value) => Where(column, "=", value); + public Query WhereNot(string column, object value) => WhereNot(column, "=", value); + public Query OrWhere(string column, object value) => OrWhere(column, "=", value); + public Query OrWhereNot(string column, object value) => OrWhereNot(column, "=", value); + + public Query Where(object constraints) + { + Wheres.Where(constraints); + return this; + } + + public Query Where(IEnumerable> values) + { + Wheres.Where(values); + return this; + } + + public Query WhereRaw(string sql, params object[] bindings) + { + Wheres.WhereRaw(sql, bindings); + return this; + } + + public Query OrWhereRaw(string sql, params object[] bindings) + { + Wheres.OrWhereRaw(sql, bindings); + return this; + } + + public Query Where(Func callback) + { + var query = callback.Invoke(NewChild()); + + if (query.Wheres.HasConditions(EngineScope)) + { + Wheres.AddComponent("where", new NestedCondition + { + Conditions = query.Wheres, + IsNot = Wheres.GetNot(), + IsOr = Wheres.GetOr(), + }); + } + + return this; + } + + public Query Where(ConditionGroup group) + { + if (group.HasConditions(EngineScope)) + { + Wheres.AddComponent("where", new NestedCondition + { + Conditions = group, + IsNot = Wheres.GetNot(), + IsOr = Wheres.GetOr(), + }); + } + + return this; + } + + public Query WhereNot(Func callback) + { + Wheres.Not(); + return Where(callback); + } + + public Query WhereNot(ConditionGroup group) + { + Wheres.Not(); + return Where(group); + } + + public Query OrWhere(Func callback) + { + Wheres.Or(); + return Where(callback); + } + + public Query OrWhere(ConditionGroup group) + { + Wheres.Or(); + return Where(group); + } + + public Query OrWhereNot(Func callback) + { + Wheres.Or().Not(); + return Where(callback); + } + + public Query OrWhereNot(ConditionGroup group) + { + Wheres.Or().Not(); + return Where(group); + } + + public Query WhereColumns(string first, string op, string second) + { + Wheres.WhereColumns(first, op, second); + return this; + } + + public Query OrWhereColumns(string first, string op, string second) + { + Wheres.OrWhereColumns(first, op, second); + return this; + } + + public Query WhereNull(string column) + { + Wheres.WhereNull(column); + return this; + } + + public Query WhereNotNull(string column) + { + Wheres.WhereNotNull(column); + return this; + } + + public Query OrWhereNull(string column) + { + Wheres.OrWhereNull(column); + return this; + } + + public Query OrWhereNotNull(string column) + { + Wheres.OrWhereNotNull(column); + return this; + } + + public Query WhereTrue(string column) + { + Wheres.WhereTrue(column); + return this; + } + + public Query WhereFalse(string column) + { + Wheres.WhereFalse(column); + return this; + } + + public Query OrWhereTrue(string column) + { + Wheres.OrWhereTrue(column); + return this; + } + + public Query OrWhereFalse(string column) + { + Wheres.OrWhereFalse(column); + return this; + } + + public Query WhereLike(string column, object value, bool caseSensitive = false, string escapeCharacter = null) + { + Wheres.WhereLike(column, value, caseSensitive, escapeCharacter); + return this; + } + + public Query WhereNotLike(string column, object value, bool caseSensitive = false, string escapeCharacter = null) + { + Wheres.WhereNotLike(column, value, caseSensitive, escapeCharacter); + return this; + } + + public Query OrWhereLike(string column, object value, bool caseSensitive = false, string escapeCharacter = null) + { + Wheres.OrWhereLike(column, value, caseSensitive, escapeCharacter); + return this; + } + + public Query OrWhereNotLike(string column, object value, bool caseSensitive = false, string escapeCharacter = null) + { + Wheres.OrWhereNotLike(column, value, caseSensitive, escapeCharacter); + return this; + } + + public Query WhereStarts(string column, object value, bool caseSensitive = false, string escapeCharacter = null) + { + Wheres.WhereStarts(column, value, caseSensitive, escapeCharacter); + return this; + } + + public Query WhereNotStarts(string column, object value, bool caseSensitive = false, string escapeCharacter = null) + { + Wheres.WhereNotStarts(column, value, caseSensitive, escapeCharacter); + return this; + } + + public Query OrWhereStarts(string column, object value, bool caseSensitive = false, string escapeCharacter = null) + { + Wheres.OrWhereStarts(column, value, caseSensitive, escapeCharacter); + return this; + } + + public Query OrWhereNotStarts(string column, object value, bool caseSensitive = false, string escapeCharacter = null) + { + Wheres.OrWhereNotStarts(column, value, caseSensitive, escapeCharacter); + return this; + } + + public Query WhereEnds(string column, object value, bool caseSensitive = false, string escapeCharacter = null) + { + Wheres.WhereEnds(column, value, caseSensitive, escapeCharacter); + return this; + } + + public Query WhereNotEnds(string column, object value, bool caseSensitive = false, string escapeCharacter = null) + { + Wheres.WhereNotEnds(column, value, caseSensitive, escapeCharacter); + return this; + } + + public Query OrWhereEnds(string column, object value, bool caseSensitive = false, string escapeCharacter = null) + { + Wheres.OrWhereEnds(column, value, caseSensitive, escapeCharacter); + return this; + } + + public Query OrWhereNotEnds(string column, object value, bool caseSensitive = false, string escapeCharacter = null) + { + Wheres.OrWhereNotEnds(column, value, caseSensitive, escapeCharacter); + return this; + } + + public Query WhereContains(string column, object value, bool caseSensitive = false, string escapeCharacter = null) + { + Wheres.WhereContains(column, value, caseSensitive, escapeCharacter); + return this; + } + + public Query WhereNotContains(string column, object value, bool caseSensitive = false, string escapeCharacter = null) + { + Wheres.WhereNotContains(column, value, caseSensitive, escapeCharacter); + return this; + } + + public Query OrWhereContains(string column, object value, bool caseSensitive = false, string escapeCharacter = null) + { + Wheres.OrWhereContains(column, value, caseSensitive, escapeCharacter); + return this; + } + + public Query OrWhereNotContains(string column, object value, bool caseSensitive = false, string escapeCharacter = null) + { + Wheres.OrWhereNotContains(column, value, caseSensitive, escapeCharacter); + return this; + } + + public Query WhereBetween(string column, T lower, T higher) + { + Wheres.WhereBetween(column, lower, higher); + return this; + } + + public Query WhereNotBetween(string column, T lower, T higher) + { + Wheres.WhereNotBetween(column, lower, higher); + return this; + } + + public Query OrWhereBetween(string column, T lower, T higher) + { + Wheres.OrWhereBetween(column, lower, higher); + return this; + } + + public Query OrWhereNotBetween(string column, T lower, T higher) + { + Wheres.OrWhereNotBetween(column, lower, higher); + return this; + } + + public Query WhereIn(string column, IEnumerable values) + { + Wheres.WhereIn(column, values); + return this; + } + + public Query WhereNotIn(string column, IEnumerable values) + { + Wheres.WhereNotIn(column, values); + return this; + } + + public Query OrWhereIn(string column, IEnumerable values) + { + Wheres.OrWhereIn(column, values); + return this; + } + + public Query OrWhereNotIn(string column, IEnumerable values) + { + Wheres.OrWhereNotIn(column, values); + return this; + } + + public Query WhereIn(string column, Query query) + { + Wheres.WhereIn(column, query); + return this; + } + + public Query WhereIn(string column, Func callback) + { + Wheres.WhereIn(column, callback); + return this; + } + + public Query WhereNotIn(string column, Query query) + { + Wheres.WhereNotIn(column, query); + return this; + } + + public Query WhereNotIn(string column, Func callback) + { + Wheres.WhereNotIn(column, callback); + return this; + } + + public Query OrWhereIn(string column, Query query) + { + Wheres.OrWhereIn(column, query); + return this; + } + + public Query OrWhereIn(string column, Func callback) + { + Wheres.OrWhereIn(column, callback); + return this; + } + + public Query OrWhereNotIn(string column, Query query) + { + Wheres.OrWhereNotIn(column, query); + return this; + } + + public Query OrWhereNotIn(string column, Func callback) + { + Wheres.OrWhereNotIn(column, callback); + return this; + } + + public Query Where(string column, string op, Query query) + { + Wheres.Where(column, op, query); + return this; + } + + public Query Where(string column, string op, Func callback) + { + Wheres.Where(column, op, callback); + return this; + } + + public Query OrWhere(string column, string op, Query query) + { + Wheres.OrWhere(column, op, query); + return this; + } + + public Query OrWhere(string column, string op, Func callback) + { + Wheres.OrWhere(column, op, callback); + return this; + } + + public Query WhereSub(Query query, object value) + { + Wheres.WhereSub(query, value); + return this; + } + + public Query WhereSub(Query query, string op, object value) + { + Wheres.WhereSub(query, op, value); + return this; + } + + public Query OrWhereSub(Query query, object value) + { + Wheres.OrWhereSub(query, value); + return this; + } + + public Query OrWhereSub(Query query, string op, object value) + { + Wheres.OrWhereSub(query, op, value); + return this; + } + + public Query WhereExists(Query query) + { + Wheres.WhereExists(query); + return this; + } + + public Query WhereExists(Func callback) + { + Wheres.WhereExists(callback); + return this; + } + + public Query WhereNotExists(Query query) + { + Wheres.WhereNotExists(query); + return this; + } + + public Query WhereNotExists(Func callback) + { + Wheres.WhereNotExists(callback); + return this; + } + + public Query OrWhereExists(Query query) + { + Wheres.OrWhereExists(query); + return this; + } + + public Query OrWhereExists(Func callback) + { + Wheres.OrWhereExists(callback); + return this; + } + + public Query OrWhereNotExists(Query query) + { + Wheres.OrWhereNotExists(query); + return this; + } + + public Query OrWhereNotExists(Func callback) + { + Wheres.OrWhereNotExists(callback); + return this; + } + + #region Date + public Query WhereDatePart(string part, string column, string op, object value) + { + Wheres.WhereDatePart(part, column, op, value); + return this; + } + + public Query WhereNotDatePart(string part, string column, string op, object value) + { + Wheres.WhereNotDatePart(part, column, op, value); + return this; + } + + public Query OrWhereDatePart(string part, string column, string op, object value) + { + Wheres.OrWhereDatePart(part, column, op, value); + return this; + } + + public Query OrWhereNotDatePart(string part, string column, string op, object value) + { + Wheres.OrWhereNotDatePart(part, column, op, value); + return this; + } + + public Query WhereDate(string column, string op, object value) + { + Wheres.WhereDate(column, op, value); + return this; + } + + public Query WhereNotDate(string column, string op, object value) + { + Wheres.WhereNotDate(column, op, value); + return this; + } + + public Query OrWhereDate(string column, string op, object value) + { + Wheres.OrWhereDate(column, op, value); + return this; + } + + public Query OrWhereNotDate(string column, string op, object value) + { + Wheres.OrWhereNotDate(column, op, value); + return this; + } + + public Query WhereTime(string column, string op, object value) + { + Wheres.WhereTime(column, op, value); + return this; + } + + public Query WhereNotTime(string column, string op, object value) + { + Wheres.WhereNotTime(column, op, value); + return this; + } + + public Query OrWhereTime(string column, string op, object value) + { + Wheres.OrWhereTime(column, op, value); + return this; + } + + public Query OrWhereNotTime(string column, string op, object value) + { + Wheres.OrWhereNotTime(column, op, value); + return this; + } + + public Query WhereDatePart(string part, string column, object value) => WhereDatePart(part, column, "=", value); + public Query WhereNotDatePart(string part, string column, object value) => WhereNotDatePart(part, column, "=", value); + public Query OrWhereDatePart(string part, string column, object value) => OrWhereDatePart(part, column, "=", value); + public Query OrWhereNotDatePart(string part, string column, object value) => OrWhereNotDatePart(part, column, "=", value); + + public Query WhereDate(string column, object value) => WhereDate(column, "=", value); + public Query WhereNotDate(string column, object value) => WhereNotDate(column, "=", value); + public Query OrWhereDate(string column, object value) => OrWhereDate(column, "=", value); + public Query OrWhereNotDate(string column, object value) => OrWhereNotDate(column, "=", value); + + public Query WhereTime(string column, object value) => WhereTime(column, "=", value); + public Query WhereNotTime(string column, object value) => WhereNotTime(column, "=", value); + public Query OrWhereTime(string column, object value) => OrWhereTime(column, "=", value); + public Query OrWhereNotTime(string column, object value) => OrWhereNotTime(column, "=", value); + #endregion + } +} diff --git a/QueryBuilder/Query.cs b/QueryBuilder/Query.cs index 8f5bd22f..3d7f8a05 100755 --- a/QueryBuilder/Query.cs +++ b/QueryBuilder/Query.cs @@ -6,8 +6,117 @@ namespace SqlKata { - public partial class Query : BaseQuery + public partial class Query { + protected List _clauses = new List(); + + private ConditionGroup _wheres; + public ConditionGroup Wheres + { + get => _wheres; + set + { + _wheres = value; + if (_wheres != null) + { + _wheres.EngineScope = this.EngineScope; + _wheres.OnGetOr = () => Havings?.SetOr(false); + _wheres.OnGetNot = () => Havings?.SetNot(false); + } + } + } + + private ConditionGroup _havings; + public ConditionGroup Havings + { + get => _havings; + set + { + _havings = value; + if (_havings != null) + { + _havings.EngineScope = this.EngineScope; + _havings.OnGetOr = () => Wheres?.SetOr(false); + _havings.OnGetNot = () => Wheres?.SetNot(false); + } + } + } + + public List Joins { get; set; } = new List(); + + [Obsolete("Use dedicated properties (Wheres, Havings, Joins) instead.")] + public List Clauses + { + get + { + var all = new List(_clauses); + all.AddRange(Joins); + all.AddRange(Wheres.Clauses); + all.AddRange(Havings.Clauses); + return all; + } + set + { + _clauses.Clear(); + Wheres.Clauses.Clear(); + Havings.Clauses.Clear(); + Joins.Clear(); + if (value != null) + { + foreach (var clause in value) + { + if (clause.Component == "where") + { + Wheres.Clauses.Add(clause); + } + else if (clause.Component == "having") + { + Havings.Clauses.Add(clause); + } + else if (clause is BaseJoin bj) + { + Joins.Add(bj); + } + else + { + _clauses.Add(clause); + } + } + } + } + } + + private string _engineScope = null; + public string EngineScope + { + get => _engineScope; + set + { + _engineScope = value; + if (Wheres != null) Wheres.EngineScope = value; + if (Havings != null) Havings.EngineScope = value; + } + } + + public Query SetEngineScope(string engine) + { + this.EngineScope = engine; + return this; + } + + public Query Parent { get; set; } + + public Query SetParent(Query parent) + { + if (this == parent) + { + throw new ArgumentException($"Cannot set the same {nameof(Query)} as a parent of itself"); + } + + this.Parent = parent; + return this; + } + private string comment; public bool IsDistinct { get; set; } = false; @@ -16,16 +125,259 @@ public partial class Query : BaseQuery public List Includes = new List(); public Dictionary Variables = new Dictionary(); - public Query() : base() + public Query() { + Wheres = new ConditionGroup(); + Havings = new ConditionGroup(); } - public Query(string table, string comment = null) : base() + public Query(string table, string comment = null) : this() { From(table); Comment(comment); } + /// + /// Copy all clauses, conditions, and joins from the source query. + /// + public Query CopyClausesFrom(Query source) + { + this._clauses = source._clauses.Select(x => x.Clone()).ToList(); + this.Wheres = new ConditionGroup { Clauses = source.Wheres.Clauses.Select(x => x.Clone()).ToList() }; + this.Havings = new ConditionGroup { Clauses = source.Havings.Clauses.Select(x => x.Clone()).ToList() }; + this.Joins = source.Joins.Select(x => (BaseJoin)x.Clone()).ToList(); + return this; + } + + public virtual Query Clone() + { + var clone = new Query(); + clone.CopyClausesFrom(this); + clone.Parent = Parent; + clone.EngineScope = EngineScope; + clone.QueryAlias = QueryAlias; + clone.IsDistinct = IsDistinct; + clone.Method = Method; + clone.Includes = Includes.Select(i => i.Clone()).ToList(); + clone.Variables = new Dictionary(Variables); + return clone; + } + + public Query NewChild() + { + var newQuery = NewQuery().SetParent(this); + newQuery.EngineScope = this.EngineScope; + return newQuery; + } + + /// + /// Add a component clause to the query. + /// + /// + /// + /// + /// + public Query AddComponent(string component, AbstractClause clause, string engineCode = null) + { + if (engineCode == null) + { + engineCode = EngineScope; + } + + clause.Engine = engineCode; + clause.Component = component; + + switch (component) + { + case "where": + Wheres.AddComponent("where", clause, engineCode); + break; + case "having": + Havings.AddComponent("having", clause, engineCode); + break; + case "join" when clause is BaseJoin bj: + Joins.Add(bj); + break; + default: + _clauses.Add(clause); + break; + } + + return this; + } + + /// + /// If the query already contains a clause for the given component + /// and engine, replace it with the specified clause. Otherwise, just + /// add the clause. + /// + /// + /// + /// + /// + public Query AddOrReplaceComponent(string component, AbstractClause clause, string engineCode = null) + { + engineCode = engineCode ?? EngineScope; + + switch (component) + { + case "where": + var currentWhere = Wheres.Clauses.SingleOrDefault(c => (engineCode == null || c.Engine == engineCode)); + if (currentWhere != null) + Wheres.Clauses.Remove(currentWhere); + Wheres.AddComponent("where", clause, engineCode); + break; + case "having": + var currentHaving = Havings.Clauses.SingleOrDefault(c => (engineCode == null || c.Engine == engineCode)); + if (currentHaving != null) + Havings.Clauses.Remove(currentHaving); + Havings.AddComponent("having", clause, engineCode); + break; + case "join" when clause is BaseJoin bj: + var currentJoin = Joins.SingleOrDefault(c => (engineCode == null || c.Engine == engineCode)); + if (currentJoin != null) + Joins.Remove(currentJoin); + Joins.Add(bj); + break; + default: + var current = _clauses.Where(x => x.Component == component).SingleOrDefault(c => c.Engine == engineCode); + if (current != null) + _clauses.Remove(current); + _clauses.Add(clause); + clause.Engine = engineCode; + clause.Component = component; + break; + } + + return this; + } + + /// + /// Get the list of clauses for a component. + /// + /// + public List GetComponents(string component, string engineCode = null) where C : AbstractClause + { + if (engineCode == null) + { + engineCode = EngineScope; + } + + return component switch + { + "where" => Wheres.GetComponents(null, engineCode), + "having" => Havings.GetComponents(null, engineCode), + "join" => Joins + .Where(x => engineCode == null || x.Engine == null || engineCode == x.Engine) + .Cast() + .ToList(), + _ => _clauses + .Where(x => x.Component == component) + .Where(x => engineCode == null || x.Engine == null || engineCode == x.Engine) + .Cast() + .ToList() + }; + } + + /// + /// Get the list of clauses for a component. + /// + /// + /// + /// + public List GetComponents(string component, string engineCode = null) + { + return GetComponents(component, engineCode); + } + + /// + /// Get a single component clause from the query. + /// + /// + public C GetOneComponent(string component, string engineCode = null) where C : AbstractClause + { + engineCode = engineCode ?? EngineScope; + + var all = GetComponents(component, engineCode); + return all.FirstOrDefault(c => c.Engine == engineCode) ?? all.FirstOrDefault(c => c.Engine == null); + } + + /// + /// Get a single component clause from the query. + /// + /// + /// + /// + public AbstractClause GetOneComponent(string component, string engineCode = null) + { + return GetOneComponent(component, engineCode); + } + + /// + /// Return whether the query has clauses for a component. + /// + /// + /// + /// + public bool HasComponent(string component, string engineCode = null) + { + if (engineCode == null) + { + engineCode = EngineScope; + } + + return component switch + { + "where" => Wheres.HasConditions(engineCode), + "having" => Havings.HasConditions(engineCode), + "join" => Joins.Any(x => engineCode == null || x.Engine == null || engineCode == x.Engine), + _ => _clauses.Any(x => x.Component == component && (engineCode == null || x.Engine == null || engineCode == x.Engine)) + }; + } + + /// + /// Remove all clauses for a component. + /// + /// + /// + /// + public Query ClearComponent(string component, string engineCode = null) + { + engineCode ??= EngineScope; + + switch (component) + { + case "where": + Wheres.Clear(engineCode); + break; + case "having": + Havings.Clear(engineCode); + break; + case "join": + Joins.RemoveAll(x => engineCode == null || x.Engine == null || engineCode == x.Engine); + break; + default: + _clauses.RemoveAll(x => x.Component == component && (engineCode == null || x.Engine == null || engineCode == x.Engine)); + break; + } + + return this; + } + + public Query Or() + { + Wheres.Or(); + Havings.Or(); + return this; + } + + public Query Not(bool flag = true) + { + Wheres.Not(flag); + Havings.Not(flag); + return this; + } + public string GetComment() => comment ?? ""; public bool HasOffset(string engineCode = null) => GetOffset(engineCode) > 0; @@ -48,18 +400,6 @@ internal int GetLimit(string engineCode = null) return limit?.Limit ?? 0; } - public override Query Clone() - { - var clone = base.Clone(); - clone.Parent = Parent; - clone.QueryAlias = QueryAlias; - clone.IsDistinct = IsDistinct; - clone.Method = Method; - clone.Includes = Includes.Select(i => i.Clone()).ToList(); - clone.Variables = new Dictionary(Variables); - return clone; - } - public Query As(string alias) { QueryAlias = alias; @@ -336,7 +676,7 @@ public Query GroupByRaw(string expression, params object[] bindings) return this; } - public override Query NewQuery() + public virtual Query NewQuery() { return new Query(); } diff --git a/SqlKata.Execution/QueryFactory.cs b/SqlKata.Execution/QueryFactory.cs index 5ff399aa..779b445b 100644 --- a/SqlKata.Execution/QueryFactory.cs +++ b/SqlKata.Execution/QueryFactory.cs @@ -56,7 +56,7 @@ public Query FromQuery(Query query) xQuery.QueryFactory = this; - xQuery.Clauses = query.Clauses.Select(x => x.Clone()).ToList(); + xQuery.CopyClausesFrom(query); xQuery.SetParent(query.Parent); xQuery.QueryAlias = query.QueryAlias; diff --git a/SqlKata.Execution/XQuery.cs b/SqlKata.Execution/XQuery.cs index ef52cafe..81387ff3 100644 --- a/SqlKata.Execution/XQuery.cs +++ b/SqlKata.Execution/XQuery.cs @@ -30,7 +30,7 @@ public override Query Clone() query.QueryFactory.QueryTimeout = this.QueryFactory?.QueryTimeout ?? 30; } - query.Clauses = this.Clauses.Select(x => x.Clone()).ToList(); + query.CopyClausesFrom(this); query.Logger = this.Logger; query.QueryAlias = QueryAlias; From 7731df25053e85aa4d01ef9e3f73a1718b459aba Mon Sep 17 00:00:00 2001 From: Ahmad Date: Tue, 13 Oct 2026 22:57:33 +0300 Subject: [PATCH 2/8] feat: support outer variable resolution in subqueries using a compiler query stack --- QueryBuilder.Tests/WhereTests.cs | 112 +++++++++++++++++ QueryBuilder/Compilers/Compiler.Conditions.cs | 8 +- QueryBuilder/Compilers/Compiler.cs | 114 ++++++++++++------ QueryBuilder/IConditional.cs | 1 + QueryBuilder/Query.From.cs | 5 +- QueryBuilder/Query.Having.cs | 32 ++--- QueryBuilder/Query.Select.cs | 4 +- QueryBuilder/Query.Where.cs | 32 ++--- QueryBuilder/Query.cs | 6 - 9 files changed, 220 insertions(+), 94 deletions(-) diff --git a/QueryBuilder.Tests/WhereTests.cs b/QueryBuilder.Tests/WhereTests.cs index 2821134c..9688c6c5 100644 --- a/QueryBuilder.Tests/WhereTests.cs +++ b/QueryBuilder.Tests/WhereTests.cs @@ -134,5 +134,117 @@ public void Or_Interleaved_HavingThenWhereThenHaving() c[EngineCodes.SqlServer] ); } + + [Fact] + public void CallbackSubqueries_ResolveOuterVariables_WhereIn() + { + var q = new Query("Orders") + .Define("tenantId", 42) + .WhereIn("UserId", sub => sub.From("Users").Where("TenantId", Expressions.Variable("tenantId"))); + + var c = Compilers.Compile(q); + + Assert.Equal("SELECT * FROM [Orders] WHERE [UserId] IN (SELECT * FROM [Users] WHERE [TenantId] = ?)", c[EngineCodes.SqlServer].RawSql); + Assert.Contains(42, c[EngineCodes.SqlServer].Bindings); + } + + [Fact] + public void CallbackSubqueries_ResolveOuterVariables_Where() + { + var q = new Query("Orders") + .Define("tenantId", 42) + .Where("Total", ">", sub => sub.From("Stats").Select("AvgTotal").Where("TenantId", Expressions.Variable("tenantId"))); + + var c = Compilers.Compile(q); + + Assert.Contains(42, c[EngineCodes.SqlServer].Bindings); + } + + [Fact] + public void CallbackSubqueries_ResolveOuterVariables_WhereExists() + { + var q = new Query("Orders") + .Define("tenantId", 42) + .WhereExists(sub => sub.From("Users").Where("TenantId", Expressions.Variable("tenantId"))); + + var c = Compilers.Compile(q); + + Assert.Contains(42, c[EngineCodes.SqlServer].Bindings); + } + + [Fact] + public void CallbackSubqueries_ResolveOuterVariables_Join() + { + var q = new Query("Orders") + .Define("tenantId", 42) + .Join("Users", j => j.On("Orders.UserId", "Users.Id") + .WhereIn("Users.TenantId", sub => sub.From("Tenants").Select("Id").Where("Id", Expressions.Variable("tenantId")))); + + var c = Compilers.Compile(q); + + Assert.Contains(42, c[EngineCodes.SqlServer].Bindings); + } + + [Fact] + public void ExternalSubquery_ResolvesOuterVariables_WithoutParentConnection() + { + // sub is completely isolated, no Parent or callbacks used + var sub = new Query("Users").Where("TenantId", Expressions.Variable("tenantId")); + + var q = new Query("Orders") + .Define("tenantId", 42) + .WhereIn("UserId", sub); + + var c = Compilers.Compile(q); + + Assert.Equal("SELECT * FROM [Orders] WHERE [UserId] IN (SELECT * FROM [Users] WHERE [TenantId] = ?)", c[EngineCodes.SqlServer].RawSql); + Assert.Contains(42, c[EngineCodes.SqlServer].Bindings); + } + + [Fact] + public void CallbackSubqueries_ResolveOuterVariables_From() + { + var q = new Query() + .Define("tenantId", 42) + .From(sub => sub.From("Users").Where("TenantId", Expressions.Variable("tenantId")), "u"); + + var c = Compilers.Compile(q); + + Assert.Equal("SELECT * FROM (SELECT * FROM [Users] WHERE [TenantId] = ?) AS [u]", c[EngineCodes.SqlServer].RawSql); + Assert.Contains(42, c[EngineCodes.SqlServer].Bindings); + } + + [Fact] + public void ExternalSubquery_ResolveOuterVariables_From() + { + var sub = new Query("Users").Where("TenantId", Expressions.Variable("tenantId")); + + var q = new Query() + .Define("tenantId", 42) + .From(sub, "u"); + + var c = Compilers.Compile(q); + + Assert.Equal("SELECT * FROM (SELECT * FROM [Users] WHERE [TenantId] = ?) AS [u]", c[EngineCodes.SqlServer].RawSql); + Assert.Contains(42, c[EngineCodes.SqlServer].Bindings); + } + + [Fact] + public void SqlServer_LegacyPagination_InSubquery_ResolvesOuterVariables() + { + var compiler = new SqlServerCompiler { UseLegacyPagination = true }; + + var q = new Query("Orders") + .Define("tenantId", 42) + .WhereIn("UserId", sub => sub.From("Users") + .Where("TenantId", Expressions.Variable("tenantId")) + .OrderBy("Id") + .Offset(10) + .Limit(5)); + + var result = compiler.Compile(q); + + Assert.Contains(42, result.Bindings); + } } } diff --git a/QueryBuilder/Compilers/Compiler.Conditions.cs b/QueryBuilder/Compilers/Compiler.Conditions.cs index eba6c516..cbb49b8e 100644 --- a/QueryBuilder/Compilers/Compiler.Conditions.cs +++ b/QueryBuilder/Compilers/Compiler.Conditions.cs @@ -58,7 +58,7 @@ protected virtual string CompileRawCondition(SqlResult ctx, RawCondition x) protected virtual string CompileQueryCondition(SqlResult ctx, QueryCondition x) { - var subCtx = CompileSelectQuery(x.Query); + var subCtx = CompileSubQuery(ctx, x.Query); ctx.Bindings.AddRange(subCtx.Bindings); @@ -67,7 +67,7 @@ protected virtual string CompileQueryCondition(SqlResult ctx, QueryCondition x) protected virtual string CompileSubQueryCondition(SqlResult ctx, SubQueryCondition x) { - var subCtx = CompileSelectQuery(x.Query); + var subCtx = CompileSubQuery(ctx, x.Query); ctx.Bindings.AddRange(subCtx.Bindings); @@ -217,7 +217,7 @@ protected virtual string CompileInCondition(SqlResult ctx, InCondition item) protected virtual string CompileInQueryCondition(SqlResult ctx, InQueryCondition item) { - var subCtx = CompileSelectQuery(item.Query); + var subCtx = CompileSubQuery(ctx, item.Query); ctx.Bindings.AddRange(subCtx.Bindings); @@ -255,7 +255,7 @@ protected virtual string CompileExistsCondition(SqlResult ctx, ExistsCondition i query.ClearComponent("select").SelectRaw("1"); } - var subCtx = CompileSelectQuery(query); + var subCtx = CompileSubQuery(ctx, query); ctx.Bindings.AddRange(subCtx.Bindings); diff --git a/QueryBuilder/Compilers/Compiler.cs b/QueryBuilder/Compilers/Compiler.cs index b685e1b0..e051e256 100644 --- a/QueryBuilder/Compilers/Compiler.cs +++ b/QueryBuilder/Compilers/Compiler.cs @@ -108,43 +108,51 @@ private Query TransformAggregateQuery(Query query) protected virtual SqlResult CompileRaw(Query query) { - 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 + QueryStack.Push(query); + try { - if (query.Method == "aggregate") + 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 { - query.ClearComponent("limit") - .ClearComponent("order") - .ClearComponent("group"); + if (query.Method == "aggregate") + { + query.ClearComponent("limit") + .ClearComponent("order") + .ClearComponent("group"); + + query = TransformAggregateQuery(query); + } - query = TransformAggregateQuery(query); + ctx = CompileSelectQuery(query); } - ctx = CompileSelectQuery(query); - } + // handle CTEs + if (query.HasComponent("cte", EngineCode)) + { + ctx = CompileCteQuery(ctx, query); + } + + ctx.RawSql = Helper.ExpandParameters(ctx.RawSql, parameterPlaceholder, EscapeCharacter, ctx.Bindings.ToArray()); - // handle CTEs - if (query.HasComponent("cte", EngineCode)) + return ctx; + } + finally { - ctx = CompileCteQuery(ctx, query); + QueryStack.Pop(); } - - ctx.RawSql = Helper.ExpandParameters(ctx.RawSql, parameterPlaceholder, EscapeCharacter, ctx.Bindings.ToArray()); - - return ctx; } /// @@ -196,6 +204,40 @@ public virtual SqlResult Compile(IEnumerable queries) 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"); + } + + protected virtual SqlResult CompileSubQuery(SqlResult parentCtx, Query query) + { + QueryStack.Push(query); + try + { + return CompileSelectQuery(query); + } + finally + { + QueryStack.Pop(); + } + } + protected virtual SqlResult CompileSelectQuery(Query query) { var ctx = new SqlResult(parameterPlaceholder, EscapeCharacter) @@ -424,7 +466,7 @@ protected virtual SqlResult CompileInsertQueryClause( { string columns = GetInsertColumnsList(clause.Columns); - var subCtx = CompileSelectQuery(clause.Query); + var subCtx = CompileSubQuery(ctx, clause.Query); ctx.Bindings.AddRange(subCtx.Bindings); ctx.RawSql = $"{SingleInsertStartClause} {table}{columns} {subCtx.RawSql}"; @@ -523,7 +565,7 @@ public virtual string CompileColumn(SqlResult ctx, AbstractColumn column) alias = $" {ColumnAsKeyword}{WrapValue(queryColumn.Query.QueryAlias)}"; } - var subCtx = CompileSelectQuery(queryColumn.Query); + var subCtx = CompileSubQuery(ctx, queryColumn.Query); ctx.Bindings.AddRange(subCtx.Bindings); @@ -585,7 +627,7 @@ public virtual SqlResult CompileCte(AbstractFrom cte) } else if (cte is QueryFromClause queryFromClause) { - var subCtx = CompileSelectQuery(queryFromClause.Query); + var subCtx = CompileSubQuery(ctx, queryFromClause.Query); ctx.Bindings.AddRange(subCtx.Bindings); ctx.RawSql = $"{WrapValue(queryFromClause.Alias)} AS ({subCtx.RawSql})"; @@ -665,7 +707,7 @@ public virtual string CompileUnion(SqlResult ctx) { var combineOperator = combineClause.Operation.ToUpperInvariant() + " " + (combineClause.All ? "ALL " : ""); - var subCtx = CompileSelectQuery(combineClause.Query); + var subCtx = CompileSubQuery(ctx, combineClause.Query); ctx.Bindings.AddRange(subCtx.Bindings); @@ -700,7 +742,7 @@ public virtual string CompileTableExpression(SqlResult ctx, AbstractFrom from) var alias = string.IsNullOrEmpty(fromQuery.QueryAlias) ? "" : $" {TableAsKeyword}" + WrapValue(fromQuery.QueryAlias); - var subCtx = CompileSelectQuery(fromQuery); + var subCtx = CompileSubQuery(ctx, fromQuery); ctx.Bindings.AddRange(subCtx.Bindings); @@ -992,7 +1034,7 @@ public virtual object Resolve(SqlResult ctx, object parameter) // if we face a variable we have to lookup the variable from the predefined variables if (parameter is Variable variable) { - var value = ctx.Query.FindVariable(variable.Name); + var value = FindVariable(variable.Name); return value; } @@ -1017,7 +1059,7 @@ public virtual string Parameter(SqlResult ctx, object parameter) // if we face a variable we have to lookup the variable from the predefined variables if (parameter is Variable variable) { - var value = ctx.Query.FindVariable(variable.Name); + var value = FindVariable(variable.Name); ctx.Bindings.Add(value); return parameterPlaceholder; } diff --git a/QueryBuilder/IConditional.cs b/QueryBuilder/IConditional.cs index 2cc7b92e..fea924f0 100644 --- a/QueryBuilder/IConditional.cs +++ b/QueryBuilder/IConditional.cs @@ -1,3 +1,4 @@ +using System; using System.Collections.Generic; namespace SqlKata diff --git a/QueryBuilder/Query.From.cs b/QueryBuilder/Query.From.cs index a5a3a82d..85f0228c 100644 --- a/QueryBuilder/Query.From.cs +++ b/QueryBuilder/Query.From.cs @@ -20,7 +20,6 @@ public Query From(string table) public Query From(Query query, string alias = null) { query = query.Clone(); - query.SetParent(this); if (alias != null) { @@ -44,9 +43,7 @@ public Query FromRaw(string sql, params object[] bindings) public Query From(Func callback, string alias = null) { - var query = new Query(); - - query.SetParent(this); + var query = NewQuery(); return From(callback.Invoke(query), alias); } diff --git a/QueryBuilder/Query.Having.cs b/QueryBuilder/Query.Having.cs index 834625c9..78a6cca5 100644 --- a/QueryBuilder/Query.Having.cs +++ b/QueryBuilder/Query.Having.cs @@ -60,7 +60,7 @@ public Query OrHavingRaw(string sql, params object[] bindings) public Query Having(Func callback) { - var query = callback.Invoke(NewChild()); + var query = callback.Invoke(NewQuery()); if (query.Havings.HasConditions(EngineScope)) { @@ -338,8 +338,7 @@ public Query HavingIn(string column, Query query) public Query HavingIn(string column, Func callback) { - Havings.WhereIn(column, callback); - return this; + return HavingIn(column, callback.Invoke(NewQuery())); } public Query HavingNotIn(string column, Query query) @@ -350,8 +349,7 @@ public Query HavingNotIn(string column, Query query) public Query HavingNotIn(string column, Func callback) { - Havings.WhereNotIn(column, callback); - return this; + return HavingNotIn(column, callback.Invoke(NewQuery())); } public Query OrHavingIn(string column, Query query) @@ -362,8 +360,7 @@ public Query OrHavingIn(string column, Query query) public Query OrHavingIn(string column, Func callback) { - Havings.OrWhereIn(column, callback); - return this; + return OrHavingIn(column, callback.Invoke(NewQuery())); } public Query OrHavingNotIn(string column, Query query) @@ -374,8 +371,7 @@ public Query OrHavingNotIn(string column, Query query) public Query OrHavingNotIn(string column, Func callback) { - Havings.OrWhereNotIn(column, callback); - return this; + return OrHavingNotIn(column, callback.Invoke(NewQuery())); } public Query Having(string column, string op, Query query) @@ -386,8 +382,7 @@ public Query Having(string column, string op, Query query) public Query Having(string column, string op, Func callback) { - Havings.Where(column, op, callback); - return this; + return Having(column, op, callback.Invoke(NewQuery())); } public Query OrHaving(string column, string op, Query query) @@ -398,8 +393,7 @@ public Query OrHaving(string column, string op, Query query) public Query OrHaving(string column, string op, Func callback) { - Havings.OrWhere(column, op, callback); - return this; + return OrHaving(column, op, callback.Invoke(NewQuery())); } public Query HavingSub(Query query, object value) @@ -434,8 +428,7 @@ public Query HavingExists(Query query) public Query HavingExists(Func callback) { - Havings.WhereExists(callback); - return this; + return HavingExists(callback.Invoke(NewQuery())); } public Query HavingNotExists(Query query) @@ -446,8 +439,7 @@ public Query HavingNotExists(Query query) public Query HavingNotExists(Func callback) { - Havings.WhereNotExists(callback); - return this; + return HavingNotExists(callback.Invoke(NewQuery())); } public Query OrHavingExists(Query query) @@ -458,8 +450,7 @@ public Query OrHavingExists(Query query) public Query OrHavingExists(Func callback) { - Havings.OrWhereExists(callback); - return this; + return OrHavingExists(callback.Invoke(NewQuery())); } public Query OrHavingNotExists(Query query) @@ -470,8 +461,7 @@ public Query OrHavingNotExists(Query query) public Query OrHavingNotExists(Func callback) { - Havings.OrWhereNotExists(callback); - return this; + return OrHavingNotExists(callback.Invoke(NewQuery())); } #region Date diff --git a/QueryBuilder/Query.Select.cs b/QueryBuilder/Query.Select.cs index 9502c024..58908354 100644 --- a/QueryBuilder/Query.Select.cs +++ b/QueryBuilder/Query.Select.cs @@ -66,7 +66,7 @@ public Query Select(Query query, string alias) public Query Select(Func callback, string alias) { - return Select(callback.Invoke(NewChild()), alias); + return Select(callback.Invoke(NewQuery()), alias); } public Query SelectAggregate(string aggregate, string column, Query filter = null) @@ -90,7 +90,7 @@ public Query SelectAggregate(string aggregate, string column, Func return SelectAggregate(aggregate, column); } - return SelectAggregate(aggregate, column, filter.Invoke(NewChild())); + return SelectAggregate(aggregate, column, filter.Invoke(NewQuery())); } public Query SelectSum(string column, Func filter = null) diff --git a/QueryBuilder/Query.Where.cs b/QueryBuilder/Query.Where.cs index 54a515e6..361bcfb7 100644 --- a/QueryBuilder/Query.Where.cs +++ b/QueryBuilder/Query.Where.cs @@ -60,7 +60,7 @@ public Query OrWhereRaw(string sql, params object[] bindings) public Query Where(Func callback) { - var query = callback.Invoke(NewChild()); + var query = callback.Invoke(NewQuery()); if (query.Wheres.HasConditions(EngineScope)) { @@ -338,8 +338,7 @@ public Query WhereIn(string column, Query query) public Query WhereIn(string column, Func callback) { - Wheres.WhereIn(column, callback); - return this; + return WhereIn(column, callback.Invoke(NewQuery())); } public Query WhereNotIn(string column, Query query) @@ -350,8 +349,7 @@ public Query WhereNotIn(string column, Query query) public Query WhereNotIn(string column, Func callback) { - Wheres.WhereNotIn(column, callback); - return this; + return WhereNotIn(column, callback.Invoke(NewQuery())); } public Query OrWhereIn(string column, Query query) @@ -362,8 +360,7 @@ public Query OrWhereIn(string column, Query query) public Query OrWhereIn(string column, Func callback) { - Wheres.OrWhereIn(column, callback); - return this; + return OrWhereIn(column, callback.Invoke(NewQuery())); } public Query OrWhereNotIn(string column, Query query) @@ -374,8 +371,7 @@ public Query OrWhereNotIn(string column, Query query) public Query OrWhereNotIn(string column, Func callback) { - Wheres.OrWhereNotIn(column, callback); - return this; + return OrWhereNotIn(column, callback.Invoke(NewQuery())); } public Query Where(string column, string op, Query query) @@ -386,8 +382,7 @@ public Query Where(string column, string op, Query query) public Query Where(string column, string op, Func callback) { - Wheres.Where(column, op, callback); - return this; + return Where(column, op, callback.Invoke(NewQuery())); } public Query OrWhere(string column, string op, Query query) @@ -398,8 +393,7 @@ public Query OrWhere(string column, string op, Query query) public Query OrWhere(string column, string op, Func callback) { - Wheres.OrWhere(column, op, callback); - return this; + return OrWhere(column, op, callback.Invoke(NewQuery())); } public Query WhereSub(Query query, object value) @@ -434,8 +428,7 @@ public Query WhereExists(Query query) public Query WhereExists(Func callback) { - Wheres.WhereExists(callback); - return this; + return WhereExists(callback.Invoke(NewQuery())); } public Query WhereNotExists(Query query) @@ -446,8 +439,7 @@ public Query WhereNotExists(Query query) public Query WhereNotExists(Func callback) { - Wheres.WhereNotExists(callback); - return this; + return WhereNotExists(callback.Invoke(NewQuery())); } public Query OrWhereExists(Query query) @@ -458,8 +450,7 @@ public Query OrWhereExists(Query query) public Query OrWhereExists(Func callback) { - Wheres.OrWhereExists(callback); - return this; + return OrWhereExists(callback.Invoke(NewQuery())); } public Query OrWhereNotExists(Query query) @@ -470,8 +461,7 @@ public Query OrWhereNotExists(Query query) public Query OrWhereNotExists(Func callback) { - Wheres.OrWhereNotExists(callback); - return this; + return OrWhereNotExists(callback.Invoke(NewQuery())); } #region Date diff --git a/QueryBuilder/Query.cs b/QueryBuilder/Query.cs index 3d7f8a05..8e902eb2 100755 --- a/QueryBuilder/Query.cs +++ b/QueryBuilder/Query.cs @@ -163,12 +163,6 @@ public virtual Query Clone() return clone; } - public Query NewChild() - { - var newQuery = NewQuery().SetParent(this); - newQuery.EngineScope = this.EngineScope; - return newQuery; - } /// /// Add a component clause to the query. From 6470bb96b3215e3ab8a1572b59077b47a5b2a3ed Mon Sep 17 00:00:00 2001 From: Ahmad Date: Tue, 13 Oct 2026 23:04:05 +0300 Subject: [PATCH 3/8] refactor: improve component handling and engine scope resolution in Join and Compiler classes --- QueryBuilder.Tests/GeneralTests.cs | 47 +++++++++++++++++ QueryBuilder/Compilers/Compiler.cs | 2 +- QueryBuilder/Join.cs | 83 ++++++++++++++++++++++++++++-- 3 files changed, 127 insertions(+), 5 deletions(-) diff --git a/QueryBuilder.Tests/GeneralTests.cs b/QueryBuilder.Tests/GeneralTests.cs index b6e355d9..3b56a0b6 100644 --- a/QueryBuilder.Tests/GeneralTests.cs +++ b/QueryBuilder.Tests/GeneralTests.cs @@ -249,6 +249,53 @@ public void AddOrReplace_Throws_MoreThanOne() Assert.Throws(act); } + [Fact] + public void Join_AddOrReplace_Replaces_Condition() + { + var join = new Join(); + join.AddOrReplaceComponent("where", new BasicCondition { Column = "c1", Operator = "=", Value = 1 }); + join.AddOrReplaceComponent("where", new BasicCondition { Column = "c2", Operator = "=", Value = 2 }); + + Assert.Single(join.Conditions.Clauses); + var cond = join.Conditions.Clauses.Single() as BasicCondition; + Assert.NotNull(cond); + Assert.Equal("c2", cond.Column); + } + + [Fact] + public void Join_AddOrReplace_Throws_When_MoreThanOneCondition() + { + var join = new Join(); + join.On("a", "b"); + join.On("c", "d"); + + Action act = () => join.AddOrReplaceComponent("where", new BasicCondition()); + Assert.Throws(act); + } + + [Fact] + public void Join_AddOrReplace_FromClause_Respects_EngineSelection() + { + var join = new Join(); + join.AddOrReplaceComponent("from", new FromClause { Table = "generic_table" }); + join.AddOrReplaceComponent("from", new FromClause { Table = "mssql_table", Engine = EngineCodes.SqlServer }, EngineCodes.SqlServer); + + // Generic FromClause should not be overwritten + Assert.NotNull(join.FromClause); + Assert.Equal("generic_table", (join.FromClause as FromClause)?.Table); + Assert.Null(join.FromClause.Engine); + + // SqlServer specific FromClause should be accessible via GetComponents with EngineCode + var mssqlFrom = join.GetComponents("from", EngineCodes.SqlServer); + Assert.Contains(mssqlFrom, f => f.Table == "mssql_table"); + + // Replacing the SqlServer specific FromClause replaces it instead of accumulating + join.AddOrReplaceComponent("from", new FromClause { Table = "mssql_table_updated", Engine = EngineCodes.SqlServer }, EngineCodes.SqlServer); + var updatedMssqlFrom = join.GetComponents("from", EngineCodes.SqlServer).Where(f => f.Engine == EngineCodes.SqlServer).ToList(); + Assert.Single(updatedMssqlFrom); + Assert.Equal("mssql_table_updated", updatedMssqlFrom.Single().Table); + } + [Fact] public void OneLimitPerEngine() { diff --git a/QueryBuilder/Compilers/Compiler.cs b/QueryBuilder/Compilers/Compiler.cs index e051e256..7c38d52d 100644 --- a/QueryBuilder/Compilers/Compiler.cs +++ b/QueryBuilder/Compilers/Compiler.cs @@ -787,7 +787,7 @@ public virtual string CompileJoin(SqlResult ctx, Join join, bool isNested = fals { #pragma warning disable CS0618 - var from = join.FromClause ?? join.GetOneComponent("from", EngineCode); + var from = join.GetOneComponent("from", EngineCode) ?? join.FromClause; #pragma warning restore CS0618 var conditions = join.Conditions.GetConditions(EngineCode); diff --git a/QueryBuilder/Join.cs b/QueryBuilder/Join.cs index 7008ccb0..843dfd5a 100644 --- a/QueryBuilder/Join.cs +++ b/QueryBuilder/Join.cs @@ -154,7 +154,72 @@ public Join AddComponent(string component, AbstractClause clause, string engineC [Obsolete("Use dedicated properties (Conditions, FromClause) instead.")] public Join AddOrReplaceComponent(string component, AbstractClause clause, string engineCode = null) { - return AddComponent(component, clause, engineCode); + engineCode ??= EngineScope; + clause.Engine = engineCode; + clause.Component = component; + + if (clause is AbstractFrom from || component == "from") + { + var fromClause = clause as AbstractFrom; + if (fromClause != null) + { + if (FromClause == null || FromClause.Engine == engineCode) + { + FromClause = fromClause; + var current = _clauses.Where(x => x.Component == "from").SingleOrDefault(c => c.Engine == engineCode); + if (current != null) + { + _clauses.Remove(current); + } + } + else if (engineCode == null) + { + // FromClause has an engine-specific clause, while incoming clause is generic. + // Preserve the engine-specific clause in _clauses, and set FromClause to generic. + if (!_clauses.Contains(FromClause)) + { + _clauses.Add(FromClause); + } + var current = _clauses.Where(x => x.Component == "from").SingleOrDefault(c => c.Engine == null); + if (current != null) + { + _clauses.Remove(current); + } + FromClause = fromClause; + } + else + { + // FromClause has a different engine (or is generic). + // Keep FromClause, and replace in _clauses for this specific engineCode. + var current = _clauses.Where(x => x.Component == "from").SingleOrDefault(c => c.Engine == engineCode); + if (current != null) + { + _clauses.Remove(current); + } + _clauses.Add(fromClause); + } + } + } + else if (clause is AbstractCondition || component == "where") + { + var currentWhere = Conditions.Clauses.SingleOrDefault(c => engineCode == null || c.Engine == engineCode); + if (currentWhere != null) + { + Conditions.Clauses.Remove(currentWhere); + } + Conditions.AddComponent(component, clause, engineCode); + } + else + { + var current = _clauses.Where(x => x.Component == component).SingleOrDefault(c => c.Engine == engineCode); + if (current != null) + { + _clauses.Remove(current); + } + _clauses.Add(clause); + } + + return this; } [Obsolete("Use dedicated properties (Conditions, FromClause) instead.")] @@ -162,9 +227,18 @@ public List GetComponents(string component, string engineCode = null) wher { engineCode ??= EngineScope; - if (component == "from" && FromClause is C cFrom && (engineCode == null || FromClause.Engine == null || FromClause.Engine == engineCode)) + if (component == "from") { - return new List { cFrom }; + var list = new List(); + if (FromClause is C cFrom && (engineCode == null || FromClause.Engine == null || FromClause.Engine == engineCode)) + { + list.Add(cFrom); + } + list.AddRange(_clauses + .Where(x => x.Component == component) + .Where(x => engineCode == null || x.Engine == null || engineCode == x.Engine) + .Cast()); + return list; } if (component == "where") @@ -203,7 +277,7 @@ public AbstractClause GetOneComponent(string component, string engineCode = null public bool HasComponent(string component, string engineCode = null) { engineCode ??= EngineScope; - if (component == "from") return FromClause != null && (engineCode == null || FromClause.Engine == null || FromClause.Engine == engineCode); + if (component == "from") return (FromClause != null && (engineCode == null || FromClause.Engine == null || FromClause.Engine == engineCode)) || _clauses.Any(x => x.Component == "from" && (engineCode == null || x.Engine == null || engineCode == x.Engine)); if (component == "where") return Conditions.HasConditions(engineCode); return _clauses.Any(x => x.Component == component && (engineCode == null || x.Engine == null || engineCode == x.Engine)); } @@ -219,6 +293,7 @@ public Join ClearComponent(string component, string engineCode = null) { FromClause = null; } + _clauses.RemoveAll(x => x.Component == "from" && (engineCode == null || x.Engine == null || engineCode == x.Engine)); } else if (component == "where") { From 5ad74555886a846f123f7b87a879243c16a9d832 Mon Sep 17 00:00:00 2001 From: Ahmad Date: Wed, 14 Oct 2026 01:19:19 +0300 Subject: [PATCH 4/8] refactor: remove obsolete Join component methods and update compiler fallback logic --- QueryBuilder.Tests/GeneralTests.cs | 47 ----------------- QueryBuilder/Compilers/Compiler.cs | 2 +- QueryBuilder/Join.cs | 85 ++---------------------------- 3 files changed, 4 insertions(+), 130 deletions(-) diff --git a/QueryBuilder.Tests/GeneralTests.cs b/QueryBuilder.Tests/GeneralTests.cs index 3b56a0b6..b6e355d9 100644 --- a/QueryBuilder.Tests/GeneralTests.cs +++ b/QueryBuilder.Tests/GeneralTests.cs @@ -249,53 +249,6 @@ public void AddOrReplace_Throws_MoreThanOne() Assert.Throws(act); } - [Fact] - public void Join_AddOrReplace_Replaces_Condition() - { - var join = new Join(); - join.AddOrReplaceComponent("where", new BasicCondition { Column = "c1", Operator = "=", Value = 1 }); - join.AddOrReplaceComponent("where", new BasicCondition { Column = "c2", Operator = "=", Value = 2 }); - - Assert.Single(join.Conditions.Clauses); - var cond = join.Conditions.Clauses.Single() as BasicCondition; - Assert.NotNull(cond); - Assert.Equal("c2", cond.Column); - } - - [Fact] - public void Join_AddOrReplace_Throws_When_MoreThanOneCondition() - { - var join = new Join(); - join.On("a", "b"); - join.On("c", "d"); - - Action act = () => join.AddOrReplaceComponent("where", new BasicCondition()); - Assert.Throws(act); - } - - [Fact] - public void Join_AddOrReplace_FromClause_Respects_EngineSelection() - { - var join = new Join(); - join.AddOrReplaceComponent("from", new FromClause { Table = "generic_table" }); - join.AddOrReplaceComponent("from", new FromClause { Table = "mssql_table", Engine = EngineCodes.SqlServer }, EngineCodes.SqlServer); - - // Generic FromClause should not be overwritten - Assert.NotNull(join.FromClause); - Assert.Equal("generic_table", (join.FromClause as FromClause)?.Table); - Assert.Null(join.FromClause.Engine); - - // SqlServer specific FromClause should be accessible via GetComponents with EngineCode - var mssqlFrom = join.GetComponents("from", EngineCodes.SqlServer); - Assert.Contains(mssqlFrom, f => f.Table == "mssql_table"); - - // Replacing the SqlServer specific FromClause replaces it instead of accumulating - join.AddOrReplaceComponent("from", new FromClause { Table = "mssql_table_updated", Engine = EngineCodes.SqlServer }, EngineCodes.SqlServer); - var updatedMssqlFrom = join.GetComponents("from", EngineCodes.SqlServer).Where(f => f.Engine == EngineCodes.SqlServer).ToList(); - Assert.Single(updatedMssqlFrom); - Assert.Equal("mssql_table_updated", updatedMssqlFrom.Single().Table); - } - [Fact] public void OneLimitPerEngine() { diff --git a/QueryBuilder/Compilers/Compiler.cs b/QueryBuilder/Compilers/Compiler.cs index 7c38d52d..e051e256 100644 --- a/QueryBuilder/Compilers/Compiler.cs +++ b/QueryBuilder/Compilers/Compiler.cs @@ -787,7 +787,7 @@ public virtual string CompileJoin(SqlResult ctx, Join join, bool isNested = fals { #pragma warning disable CS0618 - var from = join.GetOneComponent("from", EngineCode) ?? join.FromClause; + var from = join.FromClause ?? join.GetOneComponent("from", EngineCode); #pragma warning restore CS0618 var conditions = join.Conditions.GetConditions(EngineCode); diff --git a/QueryBuilder/Join.cs b/QueryBuilder/Join.cs index 843dfd5a..27d835c0 100644 --- a/QueryBuilder/Join.cs +++ b/QueryBuilder/Join.cs @@ -151,94 +151,16 @@ public Join AddComponent(string component, AbstractClause clause, string engineC return this; } - [Obsolete("Use dedicated properties (Conditions, FromClause) instead.")] - public Join AddOrReplaceComponent(string component, AbstractClause clause, string engineCode = null) - { - engineCode ??= EngineScope; - clause.Engine = engineCode; - clause.Component = component; - - if (clause is AbstractFrom from || component == "from") - { - var fromClause = clause as AbstractFrom; - if (fromClause != null) - { - if (FromClause == null || FromClause.Engine == engineCode) - { - FromClause = fromClause; - var current = _clauses.Where(x => x.Component == "from").SingleOrDefault(c => c.Engine == engineCode); - if (current != null) - { - _clauses.Remove(current); - } - } - else if (engineCode == null) - { - // FromClause has an engine-specific clause, while incoming clause is generic. - // Preserve the engine-specific clause in _clauses, and set FromClause to generic. - if (!_clauses.Contains(FromClause)) - { - _clauses.Add(FromClause); - } - var current = _clauses.Where(x => x.Component == "from").SingleOrDefault(c => c.Engine == null); - if (current != null) - { - _clauses.Remove(current); - } - FromClause = fromClause; - } - else - { - // FromClause has a different engine (or is generic). - // Keep FromClause, and replace in _clauses for this specific engineCode. - var current = _clauses.Where(x => x.Component == "from").SingleOrDefault(c => c.Engine == engineCode); - if (current != null) - { - _clauses.Remove(current); - } - _clauses.Add(fromClause); - } - } - } - else if (clause is AbstractCondition || component == "where") - { - var currentWhere = Conditions.Clauses.SingleOrDefault(c => engineCode == null || c.Engine == engineCode); - if (currentWhere != null) - { - Conditions.Clauses.Remove(currentWhere); - } - Conditions.AddComponent(component, clause, engineCode); - } - else - { - var current = _clauses.Where(x => x.Component == component).SingleOrDefault(c => c.Engine == engineCode); - if (current != null) - { - _clauses.Remove(current); - } - _clauses.Add(clause); - } - return this; - } [Obsolete("Use dedicated properties (Conditions, FromClause) instead.")] public List GetComponents(string component, string engineCode = null) where C : AbstractClause { engineCode ??= EngineScope; - if (component == "from") + if (component == "from" && FromClause is C cFrom && (engineCode == null || FromClause.Engine == null || FromClause.Engine == engineCode)) { - var list = new List(); - if (FromClause is C cFrom && (engineCode == null || FromClause.Engine == null || FromClause.Engine == engineCode)) - { - list.Add(cFrom); - } - list.AddRange(_clauses - .Where(x => x.Component == component) - .Where(x => engineCode == null || x.Engine == null || engineCode == x.Engine) - .Cast()); - return list; + return new List { cFrom }; } if (component == "where") @@ -277,7 +199,7 @@ public AbstractClause GetOneComponent(string component, string engineCode = null public bool HasComponent(string component, string engineCode = null) { engineCode ??= EngineScope; - if (component == "from") return (FromClause != null && (engineCode == null || FromClause.Engine == null || FromClause.Engine == engineCode)) || _clauses.Any(x => x.Component == "from" && (engineCode == null || x.Engine == null || engineCode == x.Engine)); + if (component == "from") return FromClause != null && (engineCode == null || FromClause.Engine == null || FromClause.Engine == engineCode); if (component == "where") return Conditions.HasConditions(engineCode); return _clauses.Any(x => x.Component == component && (engineCode == null || x.Engine == null || engineCode == x.Engine)); } @@ -293,7 +215,6 @@ public Join ClearComponent(string component, string engineCode = null) { FromClause = null; } - _clauses.RemoveAll(x => x.Component == "from" && (engineCode == null || x.Engine == null || engineCode == x.Engine)); } else if (component == "where") { From 9a574ac57b47687c9897b6a1488be240a39ce192 Mon Sep 17 00:00:00 2001 From: Ahmad Date: Wed, 14 Oct 2026 01:28:41 +0300 Subject: [PATCH 5/8] refactor: remove Parent property and related query hierarchy methods --- QueryBuilder/Query.cs | 18 ------------------ SqlKata.Execution/QueryFactory.cs | 1 - 2 files changed, 19 deletions(-) diff --git a/QueryBuilder/Query.cs b/QueryBuilder/Query.cs index 8e902eb2..cffd9bdc 100755 --- a/QueryBuilder/Query.cs +++ b/QueryBuilder/Query.cs @@ -104,18 +104,6 @@ public Query SetEngineScope(string engine) return this; } - public Query Parent { get; set; } - - public Query SetParent(Query parent) - { - if (this == parent) - { - throw new ArgumentException($"Cannot set the same {nameof(Query)} as a parent of itself"); - } - - this.Parent = parent; - return this; - } private string comment; @@ -153,7 +141,6 @@ public virtual Query Clone() { var clone = new Query(); clone.CopyClausesFrom(this); - clone.Parent = Parent; clone.EngineScope = EngineScope; clone.QueryAlias = QueryAlias; clone.IsDistinct = IsDistinct; @@ -719,11 +706,6 @@ public object FindVariable(string variable) return Variables[variable]; } - if (Parent != null) - { - return (Parent as Query).FindVariable(variable); - } - throw new Exception($"Variable '{variable}' not found"); } diff --git a/SqlKata.Execution/QueryFactory.cs b/SqlKata.Execution/QueryFactory.cs index 779b445b..263e2a62 100644 --- a/SqlKata.Execution/QueryFactory.cs +++ b/SqlKata.Execution/QueryFactory.cs @@ -58,7 +58,6 @@ public Query FromQuery(Query query) xQuery.CopyClausesFrom(query); - xQuery.SetParent(query.Parent); xQuery.QueryAlias = query.QueryAlias; xQuery.IsDistinct = query.IsDistinct; xQuery.Method = query.Method; From bf6376bdae7251856dc49742d9756985d7a99488 Mon Sep 17 00:00:00 2001 From: Ahmad Date: Wed, 14 Oct 2026 01:49:47 +0300 Subject: [PATCH 6/8] refactor: add dedicated FromClauses and SelectClauses properties to Query class --- QueryBuilder.Tests/GeneralTests.cs | 11 +++++ QueryBuilder/Query.cs | 65 +++++++++++++++++++++++++++--- 2 files changed, 70 insertions(+), 6 deletions(-) diff --git a/QueryBuilder.Tests/GeneralTests.cs b/QueryBuilder.Tests/GeneralTests.cs index b6e355d9..031303bd 100644 --- a/QueryBuilder.Tests/GeneralTests.cs +++ b/QueryBuilder.Tests/GeneralTests.cs @@ -249,6 +249,17 @@ public void AddOrReplace_Throws_MoreThanOne() Assert.Throws(act); } + [Fact] + public void DedicatedFromAndSelectClauses() + { + var query = new Query("users") + .Select("id", "name") + .ForSqlServer(q => q.From("mssql_users").Select("created_at")); + + Assert.Equal(2, query.FromClauses.Count); + Assert.Equal(3, query.SelectClauses.Count); + } + [Fact] public void OneLimitPerEngine() { diff --git a/QueryBuilder/Query.cs b/QueryBuilder/Query.cs index cffd9bdc..2a0033fd 100755 --- a/QueryBuilder/Query.cs +++ b/QueryBuilder/Query.cs @@ -43,13 +43,19 @@ public ConditionGroup Havings } public List Joins { get; set; } = new List(); + public List FromClauses { get; set; } = new List(); + public List SelectClauses { get; set; } = new List(); - [Obsolete("Use dedicated properties (Wheres, Havings, Joins) instead.")] + + + [Obsolete("Use dedicated properties (Wheres, Havings, Joins, FromClauses, SelectClauses) instead.")] public List Clauses { get { var all = new List(_clauses); + all.AddRange(FromClauses); + all.AddRange(SelectClauses); all.AddRange(Joins); all.AddRange(Wheres.Clauses); all.AddRange(Havings.Clauses); @@ -61,6 +67,8 @@ public List Clauses Wheres.Clauses.Clear(); Havings.Clauses.Clear(); Joins.Clear(); + FromClauses.Clear(); + SelectClauses.Clear(); if (value != null) { foreach (var clause in value) @@ -77,6 +85,14 @@ public List Clauses { Joins.Add(bj); } + else if (clause is AbstractFrom from) + { + FromClauses.Add(from); + } + else if (clause is AbstractColumn col) + { + SelectClauses.Add(col); + } else { _clauses.Add(clause); @@ -134,6 +150,8 @@ public Query CopyClausesFrom(Query source) this.Wheres = new ConditionGroup { Clauses = source.Wheres.Clauses.Select(x => x.Clone()).ToList() }; this.Havings = new ConditionGroup { Clauses = source.Havings.Clauses.Select(x => x.Clone()).ToList() }; this.Joins = source.Joins.Select(x => (BaseJoin)x.Clone()).ToList(); + this.FromClauses = source.FromClauses.Select(x => (AbstractFrom)x.Clone()).ToList(); + this.SelectClauses = source.SelectClauses.Select(x => (AbstractColumn)x.Clone()).ToList(); return this; } @@ -179,6 +197,12 @@ public Query AddComponent(string component, AbstractClause clause, string engine case "join" when clause is BaseJoin bj: Joins.Add(bj); break; + case "from" when clause is AbstractFrom from: + FromClauses.Add(from); + break; + case "select" when clause is AbstractColumn col: + SelectClauses.Add(col); + break; default: _clauses.Add(clause); break; @@ -200,33 +224,46 @@ public Query AddOrReplaceComponent(string component, AbstractClause clause, stri { engineCode = engineCode ?? EngineScope; + clause.Engine = engineCode; + clause.Component = component; + switch (component) { case "where": - var currentWhere = Wheres.Clauses.SingleOrDefault(c => (engineCode == null || c.Engine == engineCode)); + var currentWhere = Wheres.Clauses.SingleOrDefault(c => c.Engine == engineCode); if (currentWhere != null) Wheres.Clauses.Remove(currentWhere); Wheres.AddComponent("where", clause, engineCode); break; case "having": - var currentHaving = Havings.Clauses.SingleOrDefault(c => (engineCode == null || c.Engine == engineCode)); + var currentHaving = Havings.Clauses.SingleOrDefault(c => c.Engine == engineCode); if (currentHaving != null) Havings.Clauses.Remove(currentHaving); Havings.AddComponent("having", clause, engineCode); break; case "join" when clause is BaseJoin bj: - var currentJoin = Joins.SingleOrDefault(c => (engineCode == null || c.Engine == engineCode)); + var currentJoin = Joins.SingleOrDefault(c => c.Engine == engineCode); if (currentJoin != null) Joins.Remove(currentJoin); Joins.Add(bj); break; + case "from" when clause is AbstractFrom from: + var currentFrom = FromClauses.SingleOrDefault(c => c.Engine == engineCode); + if (currentFrom != null) + FromClauses.Remove(currentFrom); + FromClauses.Add(from); + break; + case "select" when clause is AbstractColumn col: + var currentSelect = SelectClauses.SingleOrDefault(c => c.Engine == engineCode); + if (currentSelect != null) + SelectClauses.Remove(currentSelect); + SelectClauses.Add(col); + break; default: var current = _clauses.Where(x => x.Component == component).SingleOrDefault(c => c.Engine == engineCode); if (current != null) _clauses.Remove(current); _clauses.Add(clause); - clause.Engine = engineCode; - clause.Component = component; break; } @@ -252,6 +289,14 @@ public List GetComponents(string component, string engineCode = null) wher .Where(x => engineCode == null || x.Engine == null || engineCode == x.Engine) .Cast() .ToList(), + "from" => FromClauses + .Where(x => engineCode == null || x.Engine == null || engineCode == x.Engine) + .Cast() + .ToList(), + "select" => SelectClauses + .Where(x => engineCode == null || x.Engine == null || engineCode == x.Engine) + .Cast() + .ToList(), _ => _clauses .Where(x => x.Component == component) .Where(x => engineCode == null || x.Engine == null || engineCode == x.Engine) @@ -312,6 +357,8 @@ public bool HasComponent(string component, string engineCode = null) "where" => Wheres.HasConditions(engineCode), "having" => Havings.HasConditions(engineCode), "join" => Joins.Any(x => engineCode == null || x.Engine == null || engineCode == x.Engine), + "from" => FromClauses.Any(x => engineCode == null || x.Engine == null || engineCode == x.Engine), + "select" => SelectClauses.Any(x => engineCode == null || x.Engine == null || engineCode == x.Engine), _ => _clauses.Any(x => x.Component == component && (engineCode == null || x.Engine == null || engineCode == x.Engine)) }; } @@ -337,6 +384,12 @@ public Query ClearComponent(string component, string engineCode = null) case "join": Joins.RemoveAll(x => engineCode == null || x.Engine == null || engineCode == x.Engine); break; + case "from": + FromClauses.RemoveAll(x => engineCode == null || x.Engine == null || engineCode == x.Engine); + break; + case "select": + SelectClauses.RemoveAll(x => engineCode == null || x.Engine == null || engineCode == x.Engine); + break; default: _clauses.RemoveAll(x => x.Component == component && (engineCode == null || x.Engine == null || engineCode == x.Engine)); break; From 0f0e818bb381a8986ecd058986d7b18e522e2d00 Mon Sep 17 00:00:00 2001 From: Ahmad Date: Wed, 14 Oct 2026 02:24:56 +0300 Subject: [PATCH 7/8] refactor: explicit clauses vs component lists --- QueryBuilder.Tests/GeneralTests.cs | 67 ++- QueryBuilder/Compilers/Compiler.Conditions.cs | 3 +- QueryBuilder/Compilers/Compiler.cs | 123 +++--- QueryBuilder/Compilers/CteFinder.cs | 3 +- QueryBuilder/Compilers/FirebirdCompiler.cs | 6 +- QueryBuilder/Compilers/OracleCompiler.cs | 2 +- QueryBuilder/Compilers/SqlServerCompiler.cs | 8 +- QueryBuilder/Extensions/ClauseExtensions.cs | 42 ++ .../Extensions/ConditionExtensions.cs | 2 +- QueryBuilder/Query.Aggregate.cs | 5 +- QueryBuilder/Query.Combine.cs | 10 +- QueryBuilder/Query.From.cs | 6 +- QueryBuilder/Query.Having.cs | 8 +- QueryBuilder/Query.Insert.cs | 21 +- QueryBuilder/Query.Select.cs | 14 +- QueryBuilder/Query.Update.cs | 14 +- QueryBuilder/Query.Where.cs | 8 +- QueryBuilder/Query.cs | 412 ++++++------------ SqlKata.Execution/QueryFactory.cs | 17 +- 19 files changed, 360 insertions(+), 411 deletions(-) create mode 100644 QueryBuilder/Extensions/ClauseExtensions.cs diff --git a/QueryBuilder.Tests/GeneralTests.cs b/QueryBuilder.Tests/GeneralTests.cs index 031303bd..e9a84c81 100644 --- a/QueryBuilder.Tests/GeneralTests.cs +++ b/QueryBuilder.Tests/GeneralTests.cs @@ -199,7 +199,7 @@ public void OneFromPerEngine() var engines = new[] { EngineCodes.SqlServer, EngineCodes.MySql, EngineCodes.PostgreSql }; var c = Compilers.Compile(engines, query); - Assert.Equal(2, query.Clauses.OfType().Count()); + Assert.Equal(2, query.FromClauses.Count); Assert.Equal("SELECT * FROM [mssql]", c[EngineCodes.SqlServer].RawSql); Assert.Equal("SELECT * FROM \"generic\"", c[EngineCodes.PostgreSql].RawSql); Assert.Equal("SELECT * FROM `generic`", c[EngineCodes.MySql].RawSql); @@ -214,25 +214,25 @@ public void AddOrReplace_Works(string table, string engine) { var query = new Query(); if (table != null) - query.From(table); - query.AddOrReplaceComponent("from", new FromClause() { Table = "updated", Engine = engine }); - var froms = query.Clauses.OfType(); + query.For(engine, q => q.From(table)); + query.For(engine, q => q.From("updated")); + var froms = query.FromClauses.ForEngine(engine); Assert.Single(froms); - Assert.Equal("updated", froms.Single().Table); + Assert.Equal("updated", (froms.Single() as FromClause).Table); } [Theory] [InlineData(null, "generic")] [InlineData(EngineCodes.SqlServer, "mssql")] [InlineData(EngineCodes.MySql, "generic")] - public void GetOneComponent_Prefers_Engine(string engine, string column) + public void ForEngineOrDefault_Prefers_Engine(string engine, string column) { var query = new Query() .Where("generic", "foo") .ForSqlServer(q => q.Where("mssql", "foo")); - var where = query.GetOneComponent("where", engine) as BasicCondition; + var where = query.Wheres.Clauses.ForEngineOrDefault(engine) as BasicCondition; Assert.NotNull(where); Assert.Equal(column, where.Column); @@ -241,11 +241,11 @@ public void GetOneComponent_Prefers_Engine(string engine, string column) [Fact] public void AddOrReplace_Throws_MoreThanOne() { - var query = new Query() - .Where("a", "b") - .Where("c", "d"); + var query = new Query(); + query.FromClauses.Add(new FromClause { Table = "a" }); + query.FromClauses.Add(new FromClause { Table = "b" }); - Action act = () => query.AddOrReplaceComponent("where", new BasicCondition()); + Action act = () => query.From("c"); Assert.Throws(act); } @@ -260,6 +260,39 @@ public void DedicatedFromAndSelectClauses() Assert.Equal(3, query.SelectClauses.Count); } + [Fact] + public void DedicatedExplicitClauseProperties() + { + var query = new Query("users") + .With("active_users", new Query("users").Where("active", true)) + .Select("id", "name") + .OrderBy("name") + .GroupBy("id", "name") + .Limit(10) + .Offset(20) + .Union(new Query("archived_users")); + + Assert.Single(query.CteClauses); + Assert.Equal(2, query.SelectClauses.Count); + Assert.Single(query.OrderByClauses); + Assert.Equal(2, query.GroupByClauses.Count); + Assert.Single(query.LimitClauses); + Assert.Equal(10, query.LimitClauses[0].Limit); + Assert.Single(query.OffsetClauses); + Assert.Equal(20, query.OffsetClauses[0].Offset); + Assert.Single(query.CombineClauses); + + var cloned = query.Clone(); + Assert.NotSame(query.CteClauses, cloned.CteClauses); + Assert.NotSame(query.OrderByClauses, cloned.OrderByClauses); + Assert.NotSame(query.GroupByClauses, cloned.GroupByClauses); + Assert.NotSame(query.LimitClauses, cloned.LimitClauses); + Assert.NotSame(query.OffsetClauses, cloned.OffsetClauses); + Assert.NotSame(query.CombineClauses, cloned.CombineClauses); + Assert.Single(cloned.CteClauses); + Assert.Equal(10, cloned.LimitClauses[0].Limit); + } + [Fact] public void OneLimitPerEngine() { @@ -267,7 +300,7 @@ public void OneLimitPerEngine() .ForSqlServer(q => q.Limit(5)) .ForSqlServer(q => q.Limit(10)); - var limits = query.GetComponents("limit", EngineCodes.SqlServer); + var limits = query.LimitClauses.ForEngine(EngineCodes.SqlServer).ToList(); Assert.Single(limits); Assert.Equal(10, limits.Single().Limit); } @@ -282,7 +315,7 @@ public void CompilerSpecificLimit() var engines = new[] { EngineCodes.SqlServer, EngineCodes.MySql, EngineCodes.PostgreSql }; var c = Compilers.Compile(engines, query); - Assert.Equal(2, query.GetComponents("limit").Count); + Assert.Equal(2, query.LimitClauses.Count); Assert.Equal("SELECT TOP (5) * FROM [mytable]", c[EngineCodes.SqlServer].ToString()); Assert.Equal("SELECT * FROM \"mytable\" LIMIT 10", c[EngineCodes.PostgreSql].ToString()); Assert.Equal("SELECT * FROM `mytable`", c[EngineCodes.MySql].ToString()); @@ -295,9 +328,9 @@ public void OneOffsetPerEngine() .ForSqlServer(q => q.Offset(5)) .ForSqlServer(q => q.Offset(10)); - var limits = query.GetComponents("offset", EngineCodes.SqlServer); - Assert.Single(limits); - Assert.Equal(10, limits.Single().Offset); + var offsets = query.OffsetClauses.ForEngine(EngineCodes.SqlServer).ToList(); + Assert.Single(offsets); + Assert.Equal(10, offsets.Single().Offset); } [Fact] @@ -310,7 +343,7 @@ public void CompilerSpecificOffset() var engines = new[] { EngineCodes.SqlServer, EngineCodes.MySql, EngineCodes.PostgreSql }; var c = Compilers.Compile(engines, query); - Assert.Equal(2, query.GetComponents("offset").Count); + Assert.Equal(2, query.OffsetClauses.Count); Assert.Equal("SELECT * FROM `mytable` LIMIT 18446744073709551615 OFFSET 5", c[EngineCodes.MySql].ToString()); Assert.Equal("SELECT * FROM \"mytable\" OFFSET 10", c[EngineCodes.PostgreSql].ToString()); Assert.Equal("SELECT * FROM [mytable]", c[EngineCodes.SqlServer].ToString()); diff --git a/QueryBuilder/Compilers/Compiler.Conditions.cs b/QueryBuilder/Compilers/Compiler.Conditions.cs index cbb49b8e..e252b1b1 100644 --- a/QueryBuilder/Compilers/Compiler.Conditions.cs +++ b/QueryBuilder/Compilers/Compiler.Conditions.cs @@ -252,7 +252,8 @@ protected virtual string CompileExistsCondition(SqlResult ctx, ExistsCondition i if (OmitSelectInsideExists) { - query.ClearComponent("select").SelectRaw("1"); + query.SelectClauses.Clear(); + query.SelectRaw("1"); } var subCtx = CompileSubQuery(ctx, query); diff --git a/QueryBuilder/Compilers/Compiler.cs b/QueryBuilder/Compilers/Compiler.cs index e051e256..409758ea 100644 --- a/QueryBuilder/Compilers/Compiler.cs +++ b/QueryBuilder/Compilers/Compiler.cs @@ -77,14 +77,14 @@ protected SqlResult PrepareResult(SqlResult ctx) private Query TransformAggregateQuery(Query query) { - var clause = query.GetOneComponent("aggregate", EngineCode); + var clause = query.AggregateClauses.ForEngineOrDefault(EngineCode); if (clause.Columns.Count == 1 && !query.IsDistinct) return query; if (query.IsDistinct) { - query.ClearComponent("aggregate", EngineCode); - query.ClearComponent("select", EngineCode); + query.AggregateClauses.ClearForEngine(EngineCode); + query.SelectClauses.ClearForEngine(EngineCode); query.Select(clause.Columns.ToArray()); } else @@ -98,12 +98,14 @@ private Query TransformAggregateQuery(Query query) var outerClause = new AggregateClause() { Columns = new List { "*" }, - Type = clause.Type + Type = clause.Type, + Engine = EngineCode }; - return new Query() - .AddComponent("aggregate", outerClause) + var outerQuery = new Query() .From(query, $"{clause.Type}Query"); + outerQuery.AggregateClauses.Add(outerClause); + return outerQuery; } protected virtual SqlResult CompileRaw(Query query) @@ -129,9 +131,9 @@ protected virtual SqlResult CompileRaw(Query query) { if (query.Method == "aggregate") { - query.ClearComponent("limit") - .ClearComponent("order") - .ClearComponent("group"); + query.LimitClauses.Clear(); + query.OrderByClauses.Clear(); + query.GroupByClauses.Clear(); query = TransformAggregateQuery(query); } @@ -140,7 +142,7 @@ protected virtual SqlResult CompileRaw(Query query) } // handle CTEs - if (query.HasComponent("cte", EngineCode)) + if (query.CteClauses.ForEngine(EngineCode).Any()) { ctx = CompileCteQuery(ctx, query); } @@ -295,13 +297,12 @@ protected virtual SqlResult CompileDeleteQuery(Query query) Query = query }; - if (!ctx.Query.HasComponent("from", EngineCode)) + var fromClause = ctx.Query.FromClauses.ForEngineOrDefault(EngineCode); + if (fromClause is null) { throw new InvalidOperationException("No table set to delete"); } - var fromClause = ctx.Query.GetOneComponent("from", EngineCode); - string table = null; if (fromClause is FromClause fromClauseCast) @@ -357,13 +358,12 @@ protected virtual SqlResult CompileUpdateQuery(Query query) Query = query }; - if (!ctx.Query.HasComponent("from", EngineCode)) + var fromClause = ctx.Query.FromClauses.ForEngineOrDefault(EngineCode); + if (fromClause is null) { throw new InvalidOperationException("No table set to update"); } - var fromClause = ctx.Query.GetOneComponent("from", EngineCode); - string table = null; if (fromClause is FromClause fromClauseCast) @@ -383,7 +383,7 @@ protected virtual SqlResult CompileUpdateQuery(Query query) } // check for increment statements - var clause = ctx.Query.GetOneComponent("update", EngineCode); + var clause = ctx.Query.UpdateClauses.ForEngineOrDefault(EngineCode); string wheres; @@ -406,7 +406,7 @@ protected virtual SqlResult CompileUpdateQuery(Query query) } - var toUpdate = ctx.Query.GetOneComponent("update", EngineCode); + var toUpdate = clause as InsertClause; var parts = new List(); for (var i = 0; i < toUpdate.Columns.Count; i++) @@ -435,12 +435,9 @@ protected virtual SqlResult CompileInsertQuery(Query query) Query = query }; - if (!ctx.Query.HasComponent("from", EngineCode)) - throw new InvalidOperationException("No table set to insert"); - - var fromClause = ctx.Query.GetOneComponent("from", EngineCode); + var fromClause = ctx.Query.FromClauses.ForEngineOrDefault(EngineCode); if (fromClause is null) - throw new InvalidOperationException("Invalid table expression"); + throw new InvalidOperationException("No table set to insert"); string table = null; if (fromClause is FromClause fromClauseCast) @@ -454,7 +451,7 @@ protected virtual SqlResult CompileInsertQuery(Query query) if (table is null) throw new InvalidOperationException("Invalid table expression"); - var inserts = ctx.Query.GetComponents("insert", EngineCode); + var inserts = ctx.Query.InsertClauses.ForEngine(EngineCode).ToList(); if (inserts[0] is InsertQueryClause insertQueryClause) return CompileInsertQueryClause(ctx, table, insertQueryClause); else @@ -606,7 +603,10 @@ protected virtual string CompileFilterConditions(SqlResult ctx, AggregatedColumn return null; } - var wheres = aggregatedColumn.Filter.GetComponents("where"); + var wheres = aggregatedColumn.Filter.Wheres.Clauses + .ForEngine(EngineCode) + .Cast() + .ToList(); return CompileConditions(ctx, wheres); } @@ -650,10 +650,9 @@ protected virtual SqlResult OnBeforeSelect(SqlResult ctx) protected virtual string CompileColumns(SqlResult ctx) { - if (ctx.Query.HasComponent("aggregate", EngineCode)) + var aggregate = ctx.Query.AggregateClauses.ForEngineOrDefault(EngineCode); + if (aggregate != null) { - var aggregate = ctx.Query.GetOneComponent("aggregate", EngineCode); - var aggregateColumns = aggregate.Columns .Select(x => CompileColumn(ctx, new Column { Name = x })) .ToList(); @@ -675,8 +674,8 @@ protected virtual string CompileColumns(SqlResult ctx) return "SELECT 1"; } - var columns = ctx.Query - .GetComponents("select", EngineCode) + var columns = ctx.Query.SelectClauses + .ForEngine(EngineCode) .Select(x => CompileColumn(ctx, x)) .ToList(); @@ -692,15 +691,14 @@ public virtual string CompileUnion(SqlResult ctx) { // Handle UNION, EXCEPT and INTERSECT - if (!ctx.Query.GetComponents("combine", EngineCode).Any()) + var clauses = ctx.Query.CombineClauses.ForEngine(EngineCode).ToList(); + if (!clauses.Any()) { return null; } var combinedQueries = new List(); - var clauses = ctx.Query.GetComponents("combine", EngineCode); - foreach (var clause in clauses) { if (clause is Combine combineClause) @@ -759,10 +757,9 @@ public virtual string CompileTableExpression(SqlResult ctx, AbstractFrom from) public virtual string CompileFrom(SqlResult ctx) { - if (ctx.Query.HasComponent("from", EngineCode)) + var from = ctx.Query.FromClauses.ForEngineOrDefault(EngineCode); + if (from != null) { - var from = ctx.Query.GetOneComponent("from", EngineCode); - return "FROM " + CompileTableExpression(ctx, from); } @@ -771,24 +768,23 @@ public virtual string CompileFrom(SqlResult ctx) public virtual string CompileJoins(SqlResult ctx) { - if (!ctx.Query.HasComponent("join", EngineCode)) + var joins = ctx.Query.Joins + .ForEngine(EngineCode) + .Select(x => CompileJoin(ctx, x.Join)) + .ToList(); + + if (!joins.Any()) { return null; } - var joins = ctx.Query - .GetComponents("join", EngineCode) - .Select(x => CompileJoin(ctx, x.Join)); - return "\n" + string.Join("\n", joins); } public virtual string CompileJoin(SqlResult ctx, Join join, bool isNested = false) { -#pragma warning disable CS0618 - var from = join.FromClause ?? join.GetOneComponent("from", EngineCode); -#pragma warning restore CS0618 + var from = join.FromClause; var conditions = join.Conditions.GetConditions(EngineCode); var joinTable = CompileTableExpression(ctx, from); @@ -801,12 +797,15 @@ public virtual string CompileJoin(SqlResult ctx, Join join, bool isNested = fals public virtual string CompileWheres(SqlResult ctx) { - if (!ctx.Query.HasComponent("where", EngineCode)) + if (!ctx.Query.Wheres.HasConditions(EngineCode)) { return null; } - var conditions = ctx.Query.GetComponents("where", EngineCode); + var conditions = ctx.Query.Wheres.Clauses + .ForEngine(EngineCode) + .Cast() + .ToList(); var sql = CompileConditions(ctx, conditions).Trim(); return string.IsNullOrEmpty(sql) ? null : $"WHERE {sql}"; @@ -814,27 +813,23 @@ public virtual string CompileWheres(SqlResult ctx) public virtual string CompileGroups(SqlResult ctx) { - if (!ctx.Query.HasComponent("group", EngineCode)) + var columns = ctx.Query.GroupByClauses + .ForEngine(EngineCode) + .Select(x => CompileColumn(ctx, x)) + .ToList(); + + if (!columns.Any()) { return null; } - var columns = ctx.Query - .GetComponents("group", EngineCode) - .Select(x => CompileColumn(ctx, x)); - return "GROUP BY " + string.Join(", ", columns); } public virtual string CompileOrders(SqlResult ctx) { - if (!ctx.Query.HasComponent("order", EngineCode)) - { - return null; - } - - var columns = ctx.Query - .GetComponents("order", EngineCode) + var columns = ctx.Query.OrderByClauses + .ForEngine(EngineCode) .Select(x => { @@ -847,14 +842,19 @@ public virtual string CompileOrders(SqlResult ctx) 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) { - if (!ctx.Query.HasComponent("having", EngineCode)) + if (!ctx.Query.Havings.HasConditions(EngineCode)) { return null; } @@ -862,7 +862,8 @@ public virtual string CompileHaving(SqlResult ctx) var sql = new List(); string boolOperator; - var having = ctx.Query.GetComponents("having", EngineCode) + var having = ctx.Query.Havings.Clauses + .ForEngine(EngineCode) .Cast() .ToList(); diff --git a/QueryBuilder/Compilers/CteFinder.cs b/QueryBuilder/Compilers/CteFinder.cs index fbb0800a..c0564b94 100644 --- a/QueryBuilder/Compilers/CteFinder.cs +++ b/QueryBuilder/Compilers/CteFinder.cs @@ -1,4 +1,5 @@ using System.Collections.Generic; +using System.Linq; namespace SqlKata.Compilers { @@ -32,7 +33,7 @@ public List Find() private List findInternal(Query queryToSearch) { - var cteList = queryToSearch.GetComponents("cte", engineCode); + var cteList = queryToSearch.CteClauses.ForEngine(engineCode).ToList(); var resultList = new List(); diff --git a/QueryBuilder/Compilers/FirebirdCompiler.cs b/QueryBuilder/Compilers/FirebirdCompiler.cs index 61ab9547..39de54ca 100644 --- a/QueryBuilder/Compilers/FirebirdCompiler.cs +++ b/QueryBuilder/Compilers/FirebirdCompiler.cs @@ -17,7 +17,7 @@ protected override SqlResult CompileInsertQuery(Query query) { var ctx = base.CompileInsertQuery(query); - var inserts = ctx.Query.GetComponents("insert", EngineCode); + var inserts = ctx.Query.InsertClauses.ForEngine(EngineCode).ToList(); if (inserts.Count > 1) { @@ -57,7 +57,7 @@ protected override string CompileColumns(SqlResult ctx) { ctx.Bindings.Insert(0, limit); - ctx.Query.ClearComponent("limit"); + ctx.Query.LimitClauses.Clear(); return $"SELECT FIRST {parameterPlaceholder}" + compiled.Substring(6); } @@ -65,7 +65,7 @@ protected override string CompileColumns(SqlResult ctx) { ctx.Bindings.Insert(0, offset); - ctx.Query.ClearComponent("offset"); + ctx.Query.OffsetClauses.Clear(); return $"SELECT SKIP {parameterPlaceholder}" + compiled.Substring(6); } diff --git a/QueryBuilder/Compilers/OracleCompiler.cs b/QueryBuilder/Compilers/OracleCompiler.cs index 610ec20d..8bca3edf 100644 --- a/QueryBuilder/Compilers/OracleCompiler.cs +++ b/QueryBuilder/Compilers/OracleCompiler.cs @@ -52,7 +52,7 @@ public override string CompileLimit(SqlResult ctx) var safeOrder = ""; - if (!ctx.Query.HasComponent("order")) + if (!ctx.Query.OrderByClauses.Any()) { safeOrder = "ORDER BY (SELECT 0 FROM DUAL) "; } diff --git a/QueryBuilder/Compilers/SqlServerCompiler.cs b/QueryBuilder/Compilers/SqlServerCompiler.cs index 98b488bf..c19ad7d8 100644 --- a/QueryBuilder/Compilers/SqlServerCompiler.cs +++ b/QueryBuilder/Compilers/SqlServerCompiler.cs @@ -32,7 +32,7 @@ protected override SqlResult CompileSelectQuery(Query query) var offset = query.GetOffset(EngineCode); - if (!query.HasComponent("select")) + if (!query.SelectClauses.Any()) { query.Select("*"); } @@ -41,7 +41,7 @@ protected override SqlResult CompileSelectQuery(Query query) query.SelectRaw($"ROW_NUMBER() OVER ({order}) AS [row_num]", ctx.Bindings.ToArray()); - query.ClearComponent("order"); + query.OrderByClauses.Clear(); var result = base.CompileSelectQuery(query); @@ -81,7 +81,7 @@ protected override string CompileColumns(SqlResult ctx) // top bindings should be inserted first ctx.Bindings.Insert(0, limit); - ctx.Query.ClearComponent("limit"); + ctx.Query.LimitClauses.Clear(); // handle distinct if (compiled.IndexOf("SELECT DISTINCT") == 0) @@ -113,7 +113,7 @@ public override string CompileLimit(SqlResult ctx) } var safeOrder = ""; - if (!ctx.Query.HasComponent("order")) + if (!ctx.Query.OrderByClauses.Any()) { safeOrder = "ORDER BY (SELECT 0) "; } diff --git a/QueryBuilder/Extensions/ClauseExtensions.cs b/QueryBuilder/Extensions/ClauseExtensions.cs new file mode 100644 index 00000000..61d08d5c --- /dev/null +++ b/QueryBuilder/Extensions/ClauseExtensions.cs @@ -0,0 +1,42 @@ +using System.Collections.Generic; +using System.Linq; + +namespace SqlKata +{ + public static class ClauseExtensions + { + public static IEnumerable ForEngine(this IEnumerable clauses, string engineCode) where T : AbstractClause + { + if (clauses == null) + { + return Enumerable.Empty(); + } + + return clauses.Where(x => engineCode == null || x.Engine == null || engineCode == x.Engine); + } + + public static T ForEngineOrDefault(this IEnumerable clauses, string engineCode) where T : AbstractClause + { + if (clauses == null) + { + return null; + } + + var list = clauses as IList ?? clauses.ToList(); + return list.FirstOrDefault(c => c.Engine == engineCode) ?? list.FirstOrDefault(c => c.Engine == null); + } + + public static void ClearForEngine(this List clauses, string engineCode = null) where T : AbstractClause + { + if (clauses == null) return; + if (engineCode == null) + { + clauses.Clear(); + } + else + { + clauses.RemoveAll(x => x.Engine == null || x.Engine == engineCode); + } + } + } +} diff --git a/QueryBuilder/Extensions/ConditionExtensions.cs b/QueryBuilder/Extensions/ConditionExtensions.cs index 6855bad7..3e1b45d1 100644 --- a/QueryBuilder/Extensions/ConditionExtensions.cs +++ b/QueryBuilder/Extensions/ConditionExtensions.cs @@ -561,7 +561,7 @@ public static T OrWhere(this T target, string column, string op, Func(this T target, Query query) where T : IConditional { - if (!query.HasComponent("from")) + if (!query.FromClauses.Any()) { throw new ArgumentException($"'{nameof(FromClause)}' cannot be empty if used inside a '{nameof(WhereExists)}' condition"); } diff --git a/QueryBuilder/Query.Aggregate.cs b/QueryBuilder/Query.Aggregate.cs index 174b9106..2c51261a 100644 --- a/QueryBuilder/Query.Aggregate.cs +++ b/QueryBuilder/Query.Aggregate.cs @@ -10,11 +10,12 @@ public Query AsAggregate(string type, string[] columns = null) Method = "aggregate"; - this.ClearComponent("aggregate") - .AddComponent("aggregate", new AggregateClause + AggregateClauses.Clear(); + AggregateClauses.Add(new AggregateClause { Type = type, Columns = columns?.ToList() ?? [], + Engine = EngineScope }); return this; diff --git a/QueryBuilder/Query.Combine.cs b/QueryBuilder/Query.Combine.cs index b7238264..baf9bc43 100644 --- a/QueryBuilder/Query.Combine.cs +++ b/QueryBuilder/Query.Combine.cs @@ -13,12 +13,15 @@ public Query Combine(string operation, bool all, Query query) throw new InvalidOperationException("Only select queries can be combined."); } - return AddComponent("combine", new Combine + CombineClauses.Add(new Combine { Query = query, Operation = operation, All = all, + Engine = EngineScope }); + + return this; } public Query CombineRaw(string sql, params object[] bindings) @@ -28,11 +31,14 @@ public Query CombineRaw(string sql, params object[] bindings) throw new InvalidOperationException("Only select queries can be combined."); } - return AddComponent("combine", new RawCombine + CombineClauses.Add(new RawCombine { Expression = sql, Bindings = bindings, + Engine = EngineScope }); + + return this; } public Query Union(Query query, bool all = false) diff --git a/QueryBuilder/Query.From.cs b/QueryBuilder/Query.From.cs index 85f0228c..574d8e34 100644 --- a/QueryBuilder/Query.From.cs +++ b/QueryBuilder/Query.From.cs @@ -11,7 +11,7 @@ public partial class Query /// public Query From(string table) { - return AddOrReplaceComponent("from", new FromClause + return AddOrReplace(FromClauses, new FromClause { Table = table, }); @@ -26,7 +26,7 @@ public Query From(Query query, string alias = null) query.As(alias); } - return AddOrReplaceComponent("from", new QueryFromClause + return AddOrReplace(FromClauses, new QueryFromClause { Query = query }); @@ -34,7 +34,7 @@ public Query From(Query query, string alias = null) public Query FromRaw(string sql, params object[] bindings) { - return AddOrReplaceComponent("from", new RawFromClause + return AddOrReplace(FromClauses, new RawFromClause { Expression = sql, Bindings = bindings, diff --git a/QueryBuilder/Query.Having.cs b/QueryBuilder/Query.Having.cs index 78a6cca5..27c27b78 100644 --- a/QueryBuilder/Query.Having.cs +++ b/QueryBuilder/Query.Having.cs @@ -64,11 +64,13 @@ public Query Having(Func callback) if (query.Havings.HasConditions(EngineScope)) { - Havings.AddComponent("having", new NestedCondition + Havings.Clauses.Add(new NestedCondition { Conditions = query.Havings, IsNot = Havings.GetNot(), IsOr = Havings.GetOr(), + Engine = EngineScope, + Component = "having" }); } @@ -79,11 +81,13 @@ public Query Having(ConditionGroup group) { if (group.HasConditions(EngineScope)) { - Havings.AddComponent("having", new NestedCondition + Havings.Clauses.Add(new NestedCondition { Conditions = group, IsNot = Havings.GetNot(), IsOr = Havings.GetOr(), + Engine = EngineScope, + Component = "having" }); } diff --git a/QueryBuilder/Query.Insert.cs b/QueryBuilder/Query.Insert.cs index dbec60af..c52eb697 100644 --- a/QueryBuilder/Query.Insert.cs +++ b/QueryBuilder/Query.Insert.cs @@ -31,10 +31,12 @@ public Query AsInsert(IEnumerable columns, IEnumerable values) Method = "insert"; - ClearComponent("insert").AddComponent("insert", new InsertClause + InsertClauses.Clear(); + InsertClauses.Add(new InsertClause { Columns = columnsList, - Values = valuesList + Values = valuesList, + Engine = EngineScope }); return this; @@ -49,11 +51,13 @@ public Query AsInsert(IEnumerable> values, bool ret Method = "insert"; - ClearComponent("insert").AddComponent("insert", new InsertClause + InsertClauses.Clear(); + InsertClauses.Add(new InsertClause { Columns = values.Select(x => x.Key).ToList(), Values = values.Select(x => x.Value).ToList(), ReturnId = returnId, + Engine = EngineScope }); return this; @@ -77,7 +81,7 @@ public Query AsInsert(IEnumerable columns, IEnumerable columns, IEnumerable columns, Query query) { Method = "insert"; - ClearComponent("insert").AddComponent("insert", new InsertQueryClause + InsertClauses.Clear(); + InsertClauses.Add(new InsertQueryClause { Columns = columns.ToList(), Query = query.Clone(), + Engine = EngineScope }); return this; diff --git a/QueryBuilder/Query.Select.cs b/QueryBuilder/Query.Select.cs index 58908354..3f85e763 100644 --- a/QueryBuilder/Query.Select.cs +++ b/QueryBuilder/Query.Select.cs @@ -24,9 +24,10 @@ public Query Select(IEnumerable columns) foreach (var column in columns) { - AddComponent("select", new Column + SelectClauses.Add(new Column { - Name = column + Name = column, + Engine = EngineScope }); } @@ -41,10 +42,11 @@ public Query SelectRaw(string sql, params object[] bindings) { Method = "select"; - AddComponent("select", new RawColumn + SelectClauses.Add(new RawColumn { Expression = sql, Bindings = bindings, + Engine = EngineScope }); return this; @@ -56,9 +58,10 @@ public Query Select(Query query, string alias) query = query.Clone(); - AddComponent("select", new QueryColumn + SelectClauses.Add(new QueryColumn { Query = query.As(alias), + Engine = EngineScope }); return this; @@ -73,11 +76,12 @@ public Query SelectAggregate(string aggregate, string column, Query filter = nul { Method = "select"; - AddComponent("select", new AggregatedColumn + SelectClauses.Add(new AggregatedColumn { Column = new Column { Name = column }, Aggregate = aggregate, Filter = filter, + Engine = EngineScope }); return this; diff --git a/QueryBuilder/Query.Update.cs b/QueryBuilder/Query.Update.cs index d88aeb00..8cf7c037 100644 --- a/QueryBuilder/Query.Update.cs +++ b/QueryBuilder/Query.Update.cs @@ -28,10 +28,12 @@ public Query AsUpdate(IEnumerable columns, IEnumerable values) Method = "update"; - ClearComponent("update").AddComponent("update", new InsertClause + UpdateClauses.Clear(); + UpdateClauses.Add(new InsertClause { Columns = columns.ToList(), - Values = values.ToList() + Values = values.ToList(), + Engine = EngineScope }); return this; @@ -46,10 +48,12 @@ public Query AsUpdate(IEnumerable> values) Method = "update"; - ClearComponent("update").AddComponent("update", new InsertClause + UpdateClauses.Clear(); + UpdateClauses.Add(new InsertClause { Columns = values.Select(x => x.Key).ToList(), Values = values.Select(x => x.Value).ToList(), + Engine = EngineScope }); return this; @@ -58,13 +62,11 @@ public Query AsUpdate(IEnumerable> values) public Query AsIncrement(string column, int value = 1) { Method = "update"; - AddOrReplaceComponent("update", new IncrementClause + return AddOrReplace(UpdateClauses, new IncrementClause { Column = column, Value = value }); - - return this; } public Query AsDecrement(string column, int value = 1) diff --git a/QueryBuilder/Query.Where.cs b/QueryBuilder/Query.Where.cs index 361bcfb7..ba24e7f9 100644 --- a/QueryBuilder/Query.Where.cs +++ b/QueryBuilder/Query.Where.cs @@ -64,11 +64,13 @@ public Query Where(Func callback) if (query.Wheres.HasConditions(EngineScope)) { - Wheres.AddComponent("where", new NestedCondition + Wheres.Clauses.Add(new NestedCondition { Conditions = query.Wheres, IsNot = Wheres.GetNot(), IsOr = Wheres.GetOr(), + Engine = EngineScope, + Component = "where" }); } @@ -79,11 +81,13 @@ public Query Where(ConditionGroup group) { if (group.HasConditions(EngineScope)) { - Wheres.AddComponent("where", new NestedCondition + Wheres.Clauses.Add(new NestedCondition { Conditions = group, IsNot = Wheres.GetNot(), IsOr = Wheres.GetOr(), + Engine = EngineScope, + Component = "where" }); } diff --git a/QueryBuilder/Query.cs b/QueryBuilder/Query.cs index 2a0033fd..bb3600a0 100755 --- a/QueryBuilder/Query.cs +++ b/QueryBuilder/Query.cs @@ -8,8 +8,6 @@ namespace SqlKata { public partial class Query { - protected List _clauses = new List(); - private ConditionGroup _wheres; public ConditionGroup Wheres { @@ -19,7 +17,6 @@ public ConditionGroup Wheres _wheres = value; if (_wheres != null) { - _wheres.EngineScope = this.EngineScope; _wheres.OnGetOr = () => Havings?.SetOr(false); _wheres.OnGetNot = () => Havings?.SetNot(false); } @@ -35,7 +32,6 @@ public ConditionGroup Havings _havings = value; if (_havings != null) { - _havings.EngineScope = this.EngineScope; _havings.OnGetOr = () => Wheres?.SetOr(false); _havings.OnGetNot = () => Wheres?.SetNot(false); } @@ -45,57 +41,102 @@ public ConditionGroup Havings public List Joins { get; set; } = new List(); public List FromClauses { get; set; } = new List(); public List SelectClauses { get; set; } = new List(); - - - - [Obsolete("Use dedicated properties (Wheres, Havings, Joins, FromClauses, SelectClauses) instead.")] + public List CteClauses { get; set; } = new List(); + public List OrderByClauses { get; set; } = new List(); + public List GroupByClauses { get; set; } = new List(); + public List LimitClauses { get; set; } = new List(); + public List OffsetClauses { get; set; } = new List(); + public List AggregateClauses { get; set; } = new List(); + public List CombineClauses { get; set; } = new List(); + public List InsertClauses { get; set; } = new List(); + public List UpdateClauses { get; set; } = new List(); + + [Obsolete("Use dedicated properties (Wheres, Havings, Joins, FromClauses, SelectClauses, etc.) instead.")] public List Clauses { get { - var all = new List(_clauses); - all.AddRange(FromClauses); + var all = new List(); + all.AddRange(CteClauses); all.AddRange(SelectClauses); + all.AddRange(FromClauses); all.AddRange(Joins); all.AddRange(Wheres.Clauses); + all.AddRange(GroupByClauses); all.AddRange(Havings.Clauses); + all.AddRange(OrderByClauses); + all.AddRange(LimitClauses); + all.AddRange(OffsetClauses); + all.AddRange(AggregateClauses); + all.AddRange(CombineClauses); + all.AddRange(InsertClauses); + all.AddRange(UpdateClauses); return all; } set { - _clauses.Clear(); + CteClauses.Clear(); + SelectClauses.Clear(); + FromClauses.Clear(); + Joins.Clear(); Wheres.Clauses.Clear(); + GroupByClauses.Clear(); Havings.Clauses.Clear(); - Joins.Clear(); - FromClauses.Clear(); - SelectClauses.Clear(); + OrderByClauses.Clear(); + LimitClauses.Clear(); + OffsetClauses.Clear(); + AggregateClauses.Clear(); + CombineClauses.Clear(); + InsertClauses.Clear(); + UpdateClauses.Clear(); if (value != null) { foreach (var clause in value) { - if (clause.Component == "where") + switch (clause.Component) { - Wheres.Clauses.Add(clause); - } - else if (clause.Component == "having") - { - Havings.Clauses.Add(clause); - } - else if (clause is BaseJoin bj) - { - Joins.Add(bj); - } - else if (clause is AbstractFrom from) - { - FromClauses.Add(from); - } - else if (clause is AbstractColumn col) - { - SelectClauses.Add(col); - } - else - { - _clauses.Add(clause); + case "where": + Wheres.Clauses.Add(clause); + break; + case "having": + Havings.Clauses.Add(clause); + break; + case "join" when clause is BaseJoin bj: + Joins.Add(bj); + break; + case "from" when clause is AbstractFrom from: + FromClauses.Add(from); + break; + case "select" when clause is AbstractColumn col: + SelectClauses.Add(col); + break; + case "cte" when clause is AbstractFrom cte: + CteClauses.Add(cte); + break; + case "order" when clause is AbstractOrderBy order: + OrderByClauses.Add(order); + break; + case "group" when clause is AbstractColumn col: + GroupByClauses.Add(col); + break; + case "limit" when clause is LimitClause limit: + LimitClauses.Add(limit); + break; + case "offset" when clause is OffsetClause offset: + OffsetClauses.Add(offset); + break; + case "aggregate" when clause is AggregateClause agg: + AggregateClauses.Add(agg); + break; + case "combine" when clause is AbstractCombine combine: + CombineClauses.Add(combine); + break; + case "insert" when clause is AbstractInsertClause insert: + InsertClauses.Add(insert); + break; + case "update": + UpdateClauses.Add(clause); + break; } } } @@ -146,12 +187,20 @@ public Query(string table, string comment = null) : this() /// public Query CopyClausesFrom(Query source) { - this._clauses = source._clauses.Select(x => x.Clone()).ToList(); this.Wheres = new ConditionGroup { Clauses = source.Wheres.Clauses.Select(x => x.Clone()).ToList() }; this.Havings = new ConditionGroup { Clauses = source.Havings.Clauses.Select(x => x.Clone()).ToList() }; this.Joins = source.Joins.Select(x => (BaseJoin)x.Clone()).ToList(); this.FromClauses = source.FromClauses.Select(x => (AbstractFrom)x.Clone()).ToList(); this.SelectClauses = source.SelectClauses.Select(x => (AbstractColumn)x.Clone()).ToList(); + this.CteClauses = source.CteClauses.Select(x => (AbstractFrom)x.Clone()).ToList(); + this.OrderByClauses = source.OrderByClauses.Select(x => (AbstractOrderBy)x.Clone()).ToList(); + this.GroupByClauses = source.GroupByClauses.Select(x => (AbstractColumn)x.Clone()).ToList(); + this.LimitClauses = source.LimitClauses.Select(x => (LimitClause)x.Clone()).ToList(); + this.OffsetClauses = source.OffsetClauses.Select(x => (OffsetClause)x.Clone()).ToList(); + this.AggregateClauses = source.AggregateClauses.Select(x => (AggregateClause)x.Clone()).ToList(); + this.CombineClauses = source.CombineClauses.Select(x => (AbstractCombine)x.Clone()).ToList(); + this.InsertClauses = source.InsertClauses.Select(x => (AbstractInsertClause)x.Clone()).ToList(); + this.UpdateClauses = source.UpdateClauses.Select(x => x.Clone()).ToList(); return this; } @@ -168,233 +217,14 @@ public virtual Query Clone() return clone; } - - /// - /// Add a component clause to the query. - /// - /// - /// - /// - /// - public Query AddComponent(string component, AbstractClause clause, string engineCode = null) - { - if (engineCode == null) - { - engineCode = EngineScope; - } - - clause.Engine = engineCode; - clause.Component = component; - - switch (component) - { - case "where": - Wheres.AddComponent("where", clause, engineCode); - break; - case "having": - Havings.AddComponent("having", clause, engineCode); - break; - case "join" when clause is BaseJoin bj: - Joins.Add(bj); - break; - case "from" when clause is AbstractFrom from: - FromClauses.Add(from); - break; - case "select" when clause is AbstractColumn col: - SelectClauses.Add(col); - break; - default: - _clauses.Add(clause); - break; - } - - return this; - } - - /// - /// If the query already contains a clause for the given component - /// and engine, replace it with the specified clause. Otherwise, just - /// add the clause. - /// - /// - /// - /// - /// - public Query AddOrReplaceComponent(string component, AbstractClause clause, string engineCode = null) - { - engineCode = engineCode ?? EngineScope; - - clause.Engine = engineCode; - clause.Component = component; - - switch (component) - { - case "where": - var currentWhere = Wheres.Clauses.SingleOrDefault(c => c.Engine == engineCode); - if (currentWhere != null) - Wheres.Clauses.Remove(currentWhere); - Wheres.AddComponent("where", clause, engineCode); - break; - case "having": - var currentHaving = Havings.Clauses.SingleOrDefault(c => c.Engine == engineCode); - if (currentHaving != null) - Havings.Clauses.Remove(currentHaving); - Havings.AddComponent("having", clause, engineCode); - break; - case "join" when clause is BaseJoin bj: - var currentJoin = Joins.SingleOrDefault(c => c.Engine == engineCode); - if (currentJoin != null) - Joins.Remove(currentJoin); - Joins.Add(bj); - break; - case "from" when clause is AbstractFrom from: - var currentFrom = FromClauses.SingleOrDefault(c => c.Engine == engineCode); - if (currentFrom != null) - FromClauses.Remove(currentFrom); - FromClauses.Add(from); - break; - case "select" when clause is AbstractColumn col: - var currentSelect = SelectClauses.SingleOrDefault(c => c.Engine == engineCode); - if (currentSelect != null) - SelectClauses.Remove(currentSelect); - SelectClauses.Add(col); - break; - default: - var current = _clauses.Where(x => x.Component == component).SingleOrDefault(c => c.Engine == engineCode); - if (current != null) - _clauses.Remove(current); - _clauses.Add(clause); - break; - } - - return this; - } - - /// - /// Get the list of clauses for a component. - /// - /// - public List GetComponents(string component, string engineCode = null) where C : AbstractClause - { - if (engineCode == null) - { - engineCode = EngineScope; - } - - return component switch - { - "where" => Wheres.GetComponents(null, engineCode), - "having" => Havings.GetComponents(null, engineCode), - "join" => Joins - .Where(x => engineCode == null || x.Engine == null || engineCode == x.Engine) - .Cast() - .ToList(), - "from" => FromClauses - .Where(x => engineCode == null || x.Engine == null || engineCode == x.Engine) - .Cast() - .ToList(), - "select" => SelectClauses - .Where(x => engineCode == null || x.Engine == null || engineCode == x.Engine) - .Cast() - .ToList(), - _ => _clauses - .Where(x => x.Component == component) - .Where(x => engineCode == null || x.Engine == null || engineCode == x.Engine) - .Cast() - .ToList() - }; - } - - /// - /// Get the list of clauses for a component. - /// - /// - /// - /// - public List GetComponents(string component, string engineCode = null) - { - return GetComponents(component, engineCode); - } - - /// - /// Get a single component clause from the query. - /// - /// - public C GetOneComponent(string component, string engineCode = null) where C : AbstractClause - { - engineCode = engineCode ?? EngineScope; - - var all = GetComponents(component, engineCode); - return all.FirstOrDefault(c => c.Engine == engineCode) ?? all.FirstOrDefault(c => c.Engine == null); - } - - /// - /// Get a single component clause from the query. - /// - /// - /// - /// - public AbstractClause GetOneComponent(string component, string engineCode = null) - { - return GetOneComponent(component, engineCode); - } - - /// - /// Return whether the query has clauses for a component. - /// - /// - /// - /// - public bool HasComponent(string component, string engineCode = null) - { - if (engineCode == null) - { - engineCode = EngineScope; - } - - return component switch - { - "where" => Wheres.HasConditions(engineCode), - "having" => Havings.HasConditions(engineCode), - "join" => Joins.Any(x => engineCode == null || x.Engine == null || engineCode == x.Engine), - "from" => FromClauses.Any(x => engineCode == null || x.Engine == null || engineCode == x.Engine), - "select" => SelectClauses.Any(x => engineCode == null || x.Engine == null || engineCode == x.Engine), - _ => _clauses.Any(x => x.Component == component && (engineCode == null || x.Engine == null || engineCode == x.Engine)) - }; - } - - /// - /// Remove all clauses for a component. - /// - /// - /// - /// - public Query ClearComponent(string component, string engineCode = null) + internal Query AddOrReplace(List list, T clause, string engineCode = null) where T : AbstractClause { engineCode ??= EngineScope; - - switch (component) - { - case "where": - Wheres.Clear(engineCode); - break; - case "having": - Havings.Clear(engineCode); - break; - case "join": - Joins.RemoveAll(x => engineCode == null || x.Engine == null || engineCode == x.Engine); - break; - case "from": - FromClauses.RemoveAll(x => engineCode == null || x.Engine == null || engineCode == x.Engine); - break; - case "select": - SelectClauses.RemoveAll(x => engineCode == null || x.Engine == null || engineCode == x.Engine); - break; - default: - _clauses.RemoveAll(x => x.Component == component && (engineCode == null || x.Engine == null || engineCode == x.Engine)); - break; - } - + clause.Engine = engineCode; + var current = list.SingleOrDefault(c => c.Engine == engineCode); + if (current != null) + list.Remove(current); + list.Add(clause); return this; } @@ -421,7 +251,7 @@ public Query Not(bool flag = true) internal long GetOffset(string engineCode = null) { engineCode = engineCode ?? EngineScope; - var offset = this.GetOneComponent("offset", engineCode); + var offset = OffsetClauses.ForEngineOrDefault(engineCode); return offset?.Offset ?? 0; } @@ -429,7 +259,7 @@ internal long GetOffset(string engineCode = null) internal int GetLimit(string engineCode = null) { engineCode = engineCode ?? EngineScope; - var limit = this.GetOneComponent("limit", engineCode); + var limit = LimitClauses.ForEngineOrDefault(engineCode); return limit?.Limit ?? 0; } @@ -478,11 +308,13 @@ public Query With(Query query) // clear the query alias query.QueryAlias = null; - return AddComponent("cte", new QueryFromClause + CteClauses.Add(new QueryFromClause { Query = query, Alias = alias, + Engine = EngineScope }); + return this; } public Query With(Func fn) @@ -518,6 +350,7 @@ public Query With(string alias, IEnumerable columns, IEnumerable(), + Engine = EngineScope }; foreach (var values in valuesCollectionList) @@ -531,37 +364,36 @@ public Query With(string alias, IEnumerable columns, IEnumerable>> GetAsync( public bool Exists(Query query, IDbTransaction transaction = null, int? timeout = null) { - var clone = query.Clone() - .ClearComponent("select") - .SelectRaw("1 as [Exists]") + var clone = query.Clone(); + clone.SelectClauses.Clear(); + clone.SelectRaw("1 as [Exists]") .Limit(1); var rows = Get(clone, transaction, timeout); @@ -361,9 +362,9 @@ public bool Exists(Query query, IDbTransaction transaction = null, int? timeout public async Task ExistsAsync(Query query, IDbTransaction transaction = null, int? timeout = null, CancellationToken cancellationToken = default) { - var clone = query.Clone() - .ClearComponent("select") - .SelectRaw("1 as [Exists]") + var clone = query.Clone(); + clone.SelectClauses.Clear(); + clone.SelectRaw("1 as [Exists]") .Limit(1); var rows = await GetAsync(clone, transaction, timeout, cancellationToken); @@ -684,7 +685,7 @@ private static IEnumerable handleIncludes(Query query, IEnumerable resu // I will try to fetch the table name if provided and appending the Id as a convention // Here am using Humanizer package to help getting the singular form of the table - var fromTable = query.GetOneComponent("from") as FromClause; + var fromTable = query.FromClauses.ForEngineOrDefault(query.EngineScope) as FromClause; if (fromTable == null) { @@ -785,7 +786,7 @@ private static async Task> handleIncludesAsync(Query query, IE // I will try to fetch the table name if provided and appending the Id as a convention // Here am using Humanizer package to help getting the singular form of the table - var fromTable = query.GetOneComponent("from") as FromClause; + var fromTable = query.FromClauses.ForEngineOrDefault(query.EngineScope) as FromClause; if (fromTable == null) { From 276bfa4c381ff6f89d3977588ab63dee75740d7f Mon Sep 17 00:00:00 2001 From: Ahmad Date: Wed, 14 Oct 2026 12:09:48 +0300 Subject: [PATCH 8/8] refactor(compiler): avoid query mutation --- QueryBuilder.Tests/GeneralTests.cs | 74 ++++++++++++ QueryBuilder/Compilers/Compiler.cs | 53 +++++++-- QueryBuilder/Compilers/FirebirdCompiler.cs | 9 +- QueryBuilder/Compilers/SqlServerCompiler.cs | 4 +- QueryBuilder/Query.Insert.cs | 8 +- QueryBuilder/Query.Update.cs | 4 +- QueryBuilder/Query.cs | 124 +++----------------- 7 files changed, 140 insertions(+), 136 deletions(-) diff --git a/QueryBuilder.Tests/GeneralTests.cs b/QueryBuilder.Tests/GeneralTests.cs index e9a84c81..5496f063 100644 --- a/QueryBuilder.Tests/GeneralTests.cs +++ b/QueryBuilder.Tests/GeneralTests.cs @@ -680,5 +680,79 @@ public void Clone_ShouldPreserveAllProperties() Assert.Equal(query.Includes[0].Name, clone.Includes[0].Name); Assert.Equal(query.Variables["myvar"], clone.Variables["myvar"]); } + + [Fact] + public void Compile_Aggregate_DoesNotMutateOriginalQuery() + { + var query = new Query("users") + .OrderBy("id") + .Limit(10) + .GroupBy("category") + .AsCount(); + + var compiler = new PostgresCompiler(); + var res1 = compiler.Compile(query); + var res2 = compiler.Compile(query); + + Assert.Single(query.OrderByClauses); + Assert.Single(query.LimitClauses); + Assert.Single(query.GroupByClauses); + Assert.Equal(res1.RawSql, res2.RawSql); + } + + [Fact] + public void Compile_DistinctAggregate_DoesNotMutateOriginalQuery() + { + var query = new Query("users") + .Distinct() + .AsAvg("score"); + + var compiler = new PostgresCompiler(); + var res1 = compiler.Compile(query); + var res2 = compiler.Compile(query); + + Assert.True(query.IsDistinct); + Assert.Single(query.AggregateClauses); + Assert.Empty(query.SelectClauses); + Assert.Equal(res1.RawSql, res2.RawSql); + } + + [Fact] + public void Compile_SqlServer_Top_DoesNotMutateOriginalQuery() + { + var query = new Query("users").Limit(5); + var compiler = new SqlServerCompiler { UseLegacyPagination = true }; + + var res1 = compiler.Compile(query); + var res2 = compiler.Compile(query); + + Assert.Single(query.LimitClauses); + Assert.Equal(res1.RawSql, res2.RawSql); + } + + [Fact] + public void Compile_Firebird_Limit_DoesNotMutateOriginalQuery() + { + var query = new Query("users").Limit(5); + var compiler = new FirebirdCompiler(); + + var res1 = compiler.Compile(query); + var res2 = compiler.Compile(query); + + Assert.Single(query.LimitClauses); + Assert.Equal(res1.RawSql, res2.RawSql); + } + + [Fact] + public void Compile_WhereExists_DoesNotMutateSubquery() + { + var inner = new Query("orders").Select("id", "total"); + var outer = new Query("users").WhereExists(inner); + + var compiler = new PostgresCompiler(); + compiler.Compile(outer); + + Assert.Equal(2, inner.SelectClauses.Count); + } } } diff --git a/QueryBuilder/Compilers/Compiler.cs b/QueryBuilder/Compilers/Compiler.cs index 409758ea..a09d8124 100644 --- a/QueryBuilder/Compilers/Compiler.cs +++ b/QueryBuilder/Compilers/Compiler.cs @@ -79,19 +79,50 @@ private Query TransformAggregateQuery(Query query) { var clause = query.AggregateClauses.ForEngineOrDefault(EngineCode); - if (clause.Columns.Count == 1 && !query.IsDistinct) return query; + // 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(); + single.LimitClauses.Clear(); + single.OrderByClauses.Clear(); + single.GroupByClauses.Clear(); + return single; + } - if (query.IsDistinct) - { - query.AggregateClauses.ClearForEngine(EngineCode); - query.SelectClauses.ClearForEngine(EngineCode); - query.Select(clause.Columns.ToArray()); + // 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(); + transformed.GroupByClauses.Clear(); + + 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) { - query.WhereNotNull(column); + transformed.WhereNotNull(column); } } @@ -103,7 +134,7 @@ private Query TransformAggregateQuery(Query query) }; var outerQuery = new Query() - .From(query, $"{clause.Type}Query"); + .From(transformed, $"{clause.Type}Query"); outerQuery.AggregateClauses.Add(outerClause); return outerQuery; } @@ -131,10 +162,6 @@ protected virtual SqlResult CompileRaw(Query query) { if (query.Method == "aggregate") { - query.LimitClauses.Clear(); - query.OrderByClauses.Clear(); - query.GroupByClauses.Clear(); - query = TransformAggregateQuery(query); } @@ -244,7 +271,7 @@ protected virtual SqlResult CompileSelectQuery(Query query) { var ctx = new SqlResult(parameterPlaceholder, EscapeCharacter) { - Query = query.Clone(), + Query = query, }; var results = new[] { diff --git a/QueryBuilder/Compilers/FirebirdCompiler.cs b/QueryBuilder/Compilers/FirebirdCompiler.cs index 39de54ca..74637c63 100644 --- a/QueryBuilder/Compilers/FirebirdCompiler.cs +++ b/QueryBuilder/Compilers/FirebirdCompiler.cs @@ -1,3 +1,4 @@ +using System; using System.Collections.Generic; using System.Linq; using System.Text.RegularExpressions; @@ -57,17 +58,13 @@ protected override string CompileColumns(SqlResult ctx) { ctx.Bindings.Insert(0, limit); - ctx.Query.LimitClauses.Clear(); - - return $"SELECT FIRST {parameterPlaceholder}" + compiled.Substring(6); + return string.Concat($"SELECT FIRST {parameterPlaceholder}", compiled.AsSpan(6)); } else if (limit == 0 && offset > 0) { ctx.Bindings.Insert(0, offset); - ctx.Query.OffsetClauses.Clear(); - - return $"SELECT SKIP {parameterPlaceholder}" + compiled.Substring(6); + return string.Concat($"SELECT SKIP {parameterPlaceholder}", compiled.AsSpan(6)); } return compiled; diff --git a/QueryBuilder/Compilers/SqlServerCompiler.cs b/QueryBuilder/Compilers/SqlServerCompiler.cs index c19ad7d8..d48141e3 100644 --- a/QueryBuilder/Compilers/SqlServerCompiler.cs +++ b/QueryBuilder/Compilers/SqlServerCompiler.cs @@ -81,10 +81,8 @@ protected override string CompileColumns(SqlResult ctx) // top bindings should be inserted first ctx.Bindings.Insert(0, limit); - ctx.Query.LimitClauses.Clear(); - // handle distinct - if (compiled.IndexOf("SELECT DISTINCT") == 0) + if (compiled.StartsWith("SELECT DISTINCT")) { return $"SELECT DISTINCT TOP ({parameterPlaceholder}){compiled.Substring(15)}"; } diff --git a/QueryBuilder/Query.Insert.cs b/QueryBuilder/Query.Insert.cs index c52eb697..04fcf4e0 100644 --- a/QueryBuilder/Query.Insert.cs +++ b/QueryBuilder/Query.Insert.cs @@ -31,7 +31,7 @@ public Query AsInsert(IEnumerable columns, IEnumerable values) Method = "insert"; - InsertClauses.Clear(); + InsertClauses.ClearForEngine(EngineScope); InsertClauses.Add(new InsertClause { Columns = columnsList, @@ -51,7 +51,7 @@ public Query AsInsert(IEnumerable> values, bool ret Method = "insert"; - InsertClauses.Clear(); + InsertClauses.ClearForEngine(EngineScope); InsertClauses.Add(new InsertClause { Columns = values.Select(x => x.Key).ToList(), @@ -81,7 +81,7 @@ public Query AsInsert(IEnumerable columns, IEnumerable columns, Query query) { Method = "insert"; - InsertClauses.Clear(); + InsertClauses.ClearForEngine(EngineScope); InsertClauses.Add(new InsertQueryClause { Columns = columns.ToList(), diff --git a/QueryBuilder/Query.Update.cs b/QueryBuilder/Query.Update.cs index 8cf7c037..9d596c8a 100644 --- a/QueryBuilder/Query.Update.cs +++ b/QueryBuilder/Query.Update.cs @@ -28,7 +28,7 @@ public Query AsUpdate(IEnumerable columns, IEnumerable values) Method = "update"; - UpdateClauses.Clear(); + UpdateClauses.ClearForEngine(EngineScope); UpdateClauses.Add(new InsertClause { Columns = columns.ToList(), @@ -48,7 +48,7 @@ public Query AsUpdate(IEnumerable> values) Method = "update"; - UpdateClauses.Clear(); + UpdateClauses.ClearForEngine(EngineScope); UpdateClauses.Add(new InsertClause { Columns = values.Select(x => x.Key).ToList(), diff --git a/QueryBuilder/Query.cs b/QueryBuilder/Query.cs index bb3600a0..d1ff0671 100755 --- a/QueryBuilder/Query.cs +++ b/QueryBuilder/Query.cs @@ -38,110 +38,18 @@ public ConditionGroup Havings } } - public List Joins { get; set; } = new List(); - public List FromClauses { get; set; } = new List(); - public List SelectClauses { get; set; } = new List(); - public List CteClauses { get; set; } = new List(); - public List OrderByClauses { get; set; } = new List(); - public List GroupByClauses { get; set; } = new List(); - public List LimitClauses { get; set; } = new List(); - public List OffsetClauses { get; set; } = new List(); - public List AggregateClauses { get; set; } = new List(); - public List CombineClauses { get; set; } = new List(); - public List InsertClauses { get; set; } = new List(); - public List UpdateClauses { get; set; } = new List(); - - [Obsolete("Use dedicated properties (Wheres, Havings, Joins, FromClauses, SelectClauses, etc.) instead.")] - public List Clauses - { - get - { - var all = new List(); - all.AddRange(CteClauses); - all.AddRange(SelectClauses); - all.AddRange(FromClauses); - all.AddRange(Joins); - all.AddRange(Wheres.Clauses); - all.AddRange(GroupByClauses); - all.AddRange(Havings.Clauses); - all.AddRange(OrderByClauses); - all.AddRange(LimitClauses); - all.AddRange(OffsetClauses); - all.AddRange(AggregateClauses); - all.AddRange(CombineClauses); - all.AddRange(InsertClauses); - all.AddRange(UpdateClauses); - return all; - } - set - { - CteClauses.Clear(); - SelectClauses.Clear(); - FromClauses.Clear(); - Joins.Clear(); - Wheres.Clauses.Clear(); - GroupByClauses.Clear(); - Havings.Clauses.Clear(); - OrderByClauses.Clear(); - LimitClauses.Clear(); - OffsetClauses.Clear(); - AggregateClauses.Clear(); - CombineClauses.Clear(); - InsertClauses.Clear(); - UpdateClauses.Clear(); - if (value != null) - { - foreach (var clause in value) - { - switch (clause.Component) - { - case "where": - Wheres.Clauses.Add(clause); - break; - case "having": - Havings.Clauses.Add(clause); - break; - case "join" when clause is BaseJoin bj: - Joins.Add(bj); - break; - case "from" when clause is AbstractFrom from: - FromClauses.Add(from); - break; - case "select" when clause is AbstractColumn col: - SelectClauses.Add(col); - break; - case "cte" when clause is AbstractFrom cte: - CteClauses.Add(cte); - break; - case "order" when clause is AbstractOrderBy order: - OrderByClauses.Add(order); - break; - case "group" when clause is AbstractColumn col: - GroupByClauses.Add(col); - break; - case "limit" when clause is LimitClause limit: - LimitClauses.Add(limit); - break; - case "offset" when clause is OffsetClause offset: - OffsetClauses.Add(offset); - break; - case "aggregate" when clause is AggregateClause agg: - AggregateClauses.Add(agg); - break; - case "combine" when clause is AbstractCombine combine: - CombineClauses.Add(combine); - break; - case "insert" when clause is AbstractInsertClause insert: - InsertClauses.Add(insert); - break; - case "update": - UpdateClauses.Add(clause); - break; - } - } - } - } - } + public List Joins { get; set; } = []; + public List FromClauses { get; set; } = []; + public List SelectClauses { get; set; } = []; + public List CteClauses { get; set; } = []; + public List OrderByClauses { get; set; } = []; + public List GroupByClauses { get; set; } = []; + public List LimitClauses { get; set; } = []; + public List OffsetClauses { get; set; } = []; + public List AggregateClauses { get; set; } = []; + public List CombineClauses { get; set; } = []; + public List InsertClauses { get; set; } = []; + public List UpdateClauses { get; set; } = []; private string _engineScope = null; public string EngineScope @@ -167,8 +75,8 @@ public Query SetEngineScope(string engine) public bool IsDistinct { get; set; } = false; public string QueryAlias { get; set; } public string Method { get; set; } = "select"; - public List Includes = new List(); - public Dictionary Variables = new Dictionary(); + public List Includes = []; + public Dictionary Variables = []; public Query() { @@ -250,7 +158,7 @@ public Query Not(bool flag = true) internal long GetOffset(string engineCode = null) { - engineCode = engineCode ?? EngineScope; + engineCode ??= EngineScope; var offset = OffsetClauses.ForEngineOrDefault(engineCode); return offset?.Offset ?? 0; @@ -258,7 +166,7 @@ internal long GetOffset(string engineCode = null) internal int GetLimit(string engineCode = null) { - engineCode = engineCode ?? EngineScope; + engineCode ??= EngineScope; var limit = LimitClauses.ForEngineOrDefault(engineCode); return limit?.Limit ?? 0;