diff --git a/QueryBuilder.Tests/DefineTest.cs b/QueryBuilder.Tests/DefineTest.cs index 0b5ff292..5da783f7 100644 --- a/QueryBuilder.Tests/DefineTest.cs +++ b/QueryBuilder.Tests/DefineTest.cs @@ -47,77 +47,42 @@ public void Test_Define_SubQuery() [Fact] public void Test_Define_WhereEnds() { - var query1 = new Query("Products") .Select("ProductId") .Define("@product", "Coffee") .WhereEnds("ProductName", Variable("@product")); - - var query2 = new Query("Products") - .Select("ProductId", "ProductName") - .Define("@product", "Coffee") - .WhereEnds("ProductName", Variable("@product"), true); - var c1 = Compile(query1); - var c2 = Compile(query2); - - Assert.Equal("SELECT [ProductId] FROM [Products] WHERE LOWER([ProductName]) like '%coffee'", c1[EngineCodes.SqlServer]); - - Assert.Equal("SELECT [ProductId], [ProductName] FROM [Products] WHERE [ProductName] like '%Coffee'", c2[EngineCodes.SqlServer]); + Assert.Equal("SELECT [ProductId] FROM [Products] WHERE [ProductName] like '%Coffee'", c1[EngineCodes.SqlServer]); } - - [Fact] public void Test_Define_WhereStarts() { - - var query1 = new Query("Products") .Select("ProductId", "QuantityPerUnit") .Define("@perUnit", "12") .WhereStarts("QuantityPerUnit", Variable("@perUnit")); - - var query2 = new Query("Products") - .Select("ProductId", "QuantityPerUnit") - .Define("@perUnit", "12") - .WhereStarts("QuantityPerUnit", Variable("@perUnit"), true); - var c1 = Compile(query1); - var c2 = Compile(query2); - Assert.Equal("SELECT [ProductId], [QuantityPerUnit] FROM [Products] WHERE LOWER([QuantityPerUnit]) like '12%'", c1[EngineCodes.SqlServer]); - Assert.Equal("SELECT [ProductId], [QuantityPerUnit] FROM [Products] WHERE [QuantityPerUnit] like '12%'", c2[EngineCodes.SqlServer]); + Assert.Equal("SELECT [ProductId], [QuantityPerUnit] FROM [Products] WHERE [QuantityPerUnit] like '12%'", c1[EngineCodes.SqlServer]); } - [Fact] public void Test_Define_WhereContains() { - var query1 = new Query("Products") .Define("@perUnit", "500") .Select("ProductId", "QuantityPerUnit") .WhereContains("QuantityPerUnit", Variable("@perUnit")); - - var query2 = new Query("Products") - .Define("@perUnit", "500") - .Select("ProductId", "QuantityPerUnit") - .WhereContains("QuantityPerUnit", Variable("@perUnit"), true); - var c1 = Compile(query1); - var c2 = Compile(query2); - - Assert.Equal("SELECT [ProductId], [QuantityPerUnit] FROM [Products] WHERE LOWER([QuantityPerUnit]) like '%500%'", c1[EngineCodes.SqlServer]); - Assert.Equal("SELECT [ProductId], [QuantityPerUnit] FROM [Products] WHERE [QuantityPerUnit] like '%500%'", c2[EngineCodes.SqlServer]); + Assert.Equal("SELECT [ProductId], [QuantityPerUnit] FROM [Products] WHERE [QuantityPerUnit] like '%500%'", c1[EngineCodes.SqlServer]); } - [Fact] public void Test_Define_WhereLike() { @@ -126,18 +91,9 @@ public void Test_Define_WhereLike() .Define("@id", "20") .WhereLike("SupplierID", Variable("@id")); - - var query2 = new Query("Products") - .Select("ProductId", "ProductName", "SupplierID") - .Define("@id", "20") - .WhereLike("SupplierID", Variable("@id"), true); - var c1 = Compile(query1); - var c2 = Compile(query2); - - Assert.Equal("SELECT [ProductId], [ProductName], [SupplierID] FROM [Products] WHERE LOWER([SupplierID]) like '20'", c1[EngineCodes.SqlServer]); - Assert.Equal("SELECT [ProductId], [ProductName], [SupplierID] FROM [Products] WHERE [SupplierID] like '20'", c2[EngineCodes.SqlServer]); + Assert.Equal("SELECT [ProductId], [ProductName], [SupplierID] FROM [Products] WHERE [SupplierID] like '20'", c1[EngineCodes.SqlServer]); } @@ -202,7 +158,7 @@ public void Test_Define_HavingStarts() var c = Compile(query); - Assert.Equal("SELECT COUNT(CustomerID), [Country] FROM [Customers] GROUP BY [Country] HAVING LOWER([Country]) like 'u%'", c[EngineCodes.SqlServer]); + Assert.Equal("SELECT COUNT(CustomerID), [Country] FROM [Customers] GROUP BY [Country] HAVING [Country] like 'U%'", c[EngineCodes.SqlServer]); } @@ -220,7 +176,7 @@ public void Test_Define_Having_Ends() var c = Compile(query); - Assert.Equal("SELECT COUNT(CustomerID), [Country] FROM [Customers] GROUP BY [Country] HAVING LOWER([Country]) like '%d'", c[EngineCodes.SqlServer]); + Assert.Equal("SELECT COUNT(CustomerID), [Country] FROM [Customers] GROUP BY [Country] HAVING [Country] like '%d'", c[EngineCodes.SqlServer]); } @@ -238,7 +194,7 @@ public void Test_Define_Having_Contains() var c = Compile(query); - Assert.Equal("SELECT COUNT(CustomerID), [Country] FROM [Customers] GROUP BY [Country] HAVING LOWER([Country]) like '%d%'", c[EngineCodes.SqlServer]); + Assert.Equal("SELECT COUNT(CustomerID), [Country] FROM [Customers] GROUP BY [Country] HAVING [Country] like '%d%'", c[EngineCodes.SqlServer]); } @@ -367,7 +323,7 @@ public void Test_Define_Union() .WhereNotLike("City", Variable("@city")); var c = Compile(q2); - Assert.Equal("SELECT [City] FROM [Customers] WHERE NOT (LOWER([City]) like 'z') UNION SELECT [City] FROM [Suppliers] WHERE [City] = 'Beirut'", c[EngineCodes.SqlServer]); + Assert.Equal("SELECT [City] FROM [Customers] WHERE NOT ([City] like 'Z') UNION SELECT [City] FROM [Suppliers] WHERE [City] = 'Beirut'", c[EngineCodes.SqlServer]); } @@ -386,7 +342,7 @@ public void Test_Define_Except() .WhereNotLike("City", Variable("@city")); var c = Compile(q2); - Assert.Equal("SELECT [City] FROM [Customers] WHERE NOT (LOWER([City]) like 'z') EXCEPT SELECT [City] FROM [Suppliers] WHERE [City] = 'Beirut'", c[EngineCodes.SqlServer]); + Assert.Equal("SELECT [City] FROM [Customers] WHERE NOT ([City] like 'Z') EXCEPT SELECT [City] FROM [Suppliers] WHERE [City] = 'Beirut'", c[EngineCodes.SqlServer]); } [Fact] @@ -404,7 +360,7 @@ public void Test_Define_Intersect() .WhereNotLike("City", Variable("@city")); var c = Compile(q2); - Assert.Equal("SELECT [City] FROM [Customers] WHERE NOT (LOWER([City]) like 'z') INTERSECT SELECT [City] FROM [Suppliers] WHERE [City] = 'Beirut'", c[EngineCodes.SqlServer]); + Assert.Equal("SELECT [City] FROM [Customers] WHERE NOT ([City] like 'Z') INTERSECT SELECT [City] FROM [Suppliers] WHERE [City] = 'Beirut'", c[EngineCodes.SqlServer]); } /* diff --git a/QueryBuilder.Tests/DeleteTests.cs b/QueryBuilder.Tests/DeleteTests.cs index 14fd4043..93a768ea 100644 --- a/QueryBuilder.Tests/DeleteTests.cs +++ b/QueryBuilder.Tests/DeleteTests.cs @@ -27,10 +27,13 @@ public void DeleteWithJoin() .Where("Authors.Id", 5) .AsDelete(); - var c = Compile(q); + Assert.Equal("DELETE [Posts] FROM [Posts] \nINNER JOIN [Authors] ON [Authors].[Id] = [Posts].[AuthorId] WHERE [Authors].[Id] = 5", Compilers.CompileFor(EngineCodes.SqlServer, q).ToString()); + Assert.Equal("DELETE `Posts` FROM `Posts` \nINNER JOIN `Authors` ON `Authors`.`Id` = `Posts`.`AuthorId` WHERE `Authors`.`Id` = 5", Compilers.CompileFor(EngineCodes.MySql, q).ToString()); - Assert.Equal("DELETE [Posts] FROM [Posts] \nINNER JOIN [Authors] ON [Authors].[Id] = [Posts].[AuthorId] WHERE [Authors].[Id] = 5", c[EngineCodes.SqlServer]); - Assert.Equal("DELETE `Posts` FROM `Posts` \nINNER JOIN `Authors` ON `Authors`.`Id` = `Posts`.`AuthorId` WHERE `Authors`.`Id` = 5", c[EngineCodes.MySql]); + Assert.Throws(() => Compilers.CompileFor(EngineCodes.PostgreSql, q)); + Assert.Throws(() => Compilers.CompileFor(EngineCodes.Sqlite, q)); + Assert.Throws(() => Compilers.CompileFor(EngineCodes.Oracle, q)); + Assert.Throws(() => Compilers.CompileFor(EngineCodes.Firebird, q)); } [Fact] @@ -41,9 +44,19 @@ public void DeleteWithJoinAndAlias() .Where("Authors.Id", 5) .AsDelete(); - var c = Compile(q); + Assert.Equal("DELETE [P] FROM [Posts] AS [P] \nINNER JOIN [Authors] ON [Authors].[Id] = [P].[AuthorId] WHERE [Authors].[Id] = 5", Compilers.CompileFor(EngineCodes.SqlServer, q).ToString()); + Assert.Equal("DELETE `P` FROM `Posts` AS `P` \nINNER JOIN `Authors` ON `Authors`.`Id` = `P`.`AuthorId` WHERE `Authors`.`Id` = 5", Compilers.CompileFor(EngineCodes.MySql, q).ToString()); + } + + [Fact] + public void DeleteWithJoinAndSchema() + { + var q = new Query("Audit.Posts") + .Join("Authors", "Authors.Id", "Posts.AuthorId") + .AsDelete(); - Assert.Equal("DELETE [P] FROM [Posts] AS [P] \nINNER JOIN [Authors] ON [Authors].[Id] = [P].[AuthorId] WHERE [Authors].[Id] = 5", c[EngineCodes.SqlServer]); + Assert.Equal("DELETE [Posts] FROM [Audit].[Posts] \nINNER JOIN [Authors] ON [Authors].[Id] = [Posts].[AuthorId]", Compilers.CompileFor(EngineCodes.SqlServer, q).ToString()); + Assert.Equal("DELETE `Posts` FROM `Audit`.`Posts` \nINNER JOIN `Authors` ON `Authors`.`Id` = `Posts`.`AuthorId`", Compilers.CompileFor(EngineCodes.MySql, q).ToString()); } } } diff --git a/QueryBuilder.Tests/Firebird/FirebirdLimitTests.cs b/QueryBuilder.Tests/Firebird/FirebirdLimitTests.cs index b449bae8..ef058628 100644 --- a/QueryBuilder.Tests/Firebird/FirebirdLimitTests.cs +++ b/QueryBuilder.Tests/Firebird/FirebirdLimitTests.cs @@ -17,7 +17,7 @@ public FirebirdLimitTests() public void NoLimitNorOffset() { var query = new Query("Table"); - var ctx = new SqlResult("?", "\\") {Query = query}; + var ctx = new CompileContext(query); Assert.Null(compiler.CompileLimit(ctx)); } @@ -26,7 +26,7 @@ public void NoLimitNorOffset() public void LimitOnly() { var query = new Query("Table").Limit(10); - var ctx = new SqlResult("?", "\\") {Query = query}; + var ctx = new CompileContext(query); Assert.Null(compiler.CompileLimit(ctx)); } @@ -35,7 +35,7 @@ public void LimitOnly() public void OffsetOnly() { var query = new Query("Table").Offset(20); - var ctx = new SqlResult("?", "\\") {Query = query}; + var ctx = new CompileContext(query); Assert.Null(compiler.CompileLimit(ctx)); } @@ -44,7 +44,7 @@ public void OffsetOnly() public void LimitAndOffset() { var query = new Query("Table").Limit(5).Offset(20); - var ctx = new SqlResult("?", "\\") {Query = query}; + var ctx = new CompileContext(query); Assert.Equal("ROWS ? TO ?", compiler.CompileLimit(ctx)); Assert.Equal(21L, ctx.Bindings[0]); diff --git a/QueryBuilder.Tests/GeneralTests.cs b/QueryBuilder.Tests/GeneralTests.cs index 5496f063..966919c2 100644 --- a/QueryBuilder.Tests/GeneralTests.cs +++ b/QueryBuilder.Tests/GeneralTests.cs @@ -1,8 +1,8 @@ +using System; +using System.Linq; using SqlKata.Compilers; using SqlKata.Extensions; using SqlKata.Tests.Infrastructure; -using System; -using System.Linq; using Xunit; namespace SqlKata.Tests @@ -754,5 +754,96 @@ public void Compile_WhereExists_DoesNotMutateSubquery() Assert.Equal(2, inner.SelectClauses.Count); } + + [Fact] + public void SqlServer_SelectPrefix_TopWithColumnBindings_PreservesParameterOrder() + { + var query = new Query("users").SelectRaw("? AS [bonus]", 100).Limit(5); + var compiler = new SqlServerCompiler { UseLegacyPagination = true }; + + var result = compiler.Compile(query); + + Assert.Equal("SELECT TOP (@p0) @p1 AS [bonus] FROM [users]", result.Sql); + Assert.Equal(5, result.NamedBindings["@p0"]); + Assert.Equal(100, result.NamedBindings["@p1"]); + } + + [Fact] + public void Firebird_SelectPrefix_FirstWithDistinct() + { + var query = new Query("users").Select("name").Distinct().Limit(10); + var compiler = new FirebirdCompiler(); + + var result = compiler.Compile(query); + + Assert.Equal("SELECT FIRST 10 DISTINCT \"NAME\" FROM \"USERS\"", result.ToString()); + } + + [Fact] + public void OrderByRandom_Compiles_EmitsRandomExpression() + { + var query = new Query("Users").OrderByRandom(); + + var sqlServer = Compilers.CompileFor(EngineCodes.SqlServer, query).ToString(); + Assert.Equal("SELECT * FROM [Users] ORDER BY NEWID()", sqlServer); + + var postgres = Compilers.CompileFor(EngineCodes.PostgreSql, query).ToString(); + Assert.Equal("SELECT * FROM \"Users\" ORDER BY RANDOM()", postgres); + + var sqlite = Compilers.CompileFor(EngineCodes.Sqlite, query).ToString(); + Assert.Equal("SELECT * FROM \"Users\" ORDER BY RANDOM()", sqlite); + + var mysql = Compilers.CompileFor(EngineCodes.MySql, query).ToString(); + Assert.Equal("SELECT * FROM `Users` ORDER BY RAND()", mysql); + + var firebird = Compilers.CompileFor(EngineCodes.Firebird, query).ToString(); + Assert.Equal("SELECT * FROM \"USERS\" ORDER BY RAND()", firebird); + + var oracle = Compilers.CompileFor(EngineCodes.Oracle, query).ToString(); + Assert.Equal("SELECT * FROM \"Users\" ORDER BY DBMS_RANDOM.VALUE", oracle); + } + + private class SeedCompiler : Compiler + { + public override string EngineCode => "seed_engine"; + public override string CompileRandom(int? seed) => $"RANDOM_WITH_SEED({seed})"; + } + + [Fact] + public void OrderByRandom_RetainsAndForwardsSeed_AndClonesCorrectly() + { + var query = new Query("Users").OrderByRandom(1234); + var cloned = query.Clone(); + + var clause = Assert.IsType(Assert.Single(cloned.OrderByClauses)); + Assert.Equal(1234, clause.Seed); + + var compiler = new SeedCompiler(); + var result = compiler.Compile(query).ToString(); + Assert.Equal("SELECT * FROM \"Users\" ORDER BY RANDOM_WITH_SEED(1234)", result); + } + + [Fact] + public void OrderByRandom_RetainsAndForwardsSeed_InMySQL() + { + var query = new Query("Users").OrderByRandom(1234); + var cloned = query.Clone(); + + var clause = Assert.IsType(Assert.Single(cloned.OrderByClauses)); + Assert.Equal(1234, clause.Seed); + + var compiler = new MySqlCompiler(); + var result = compiler.Compile(query).ToString(); + Assert.Equal("SELECT * FROM `Users` ORDER BY RAND(1234)", result); + } + + [Fact] + public void OrderByRandom_CombinedWithOtherOrderAndLimit_DoesNotProduceDanglingOrderBy() + { + var query = new Query("Users").OrderBy("Id").OrderByRandom().Limit(10); + + var postgres = Compilers.CompileFor(EngineCodes.PostgreSql, query).ToString(); + Assert.Equal("SELECT * FROM \"Users\" ORDER BY \"Id\", RANDOM() LIMIT 10", postgres); + } } } diff --git a/QueryBuilder.Tests/MySql/MySqlLimitTests.cs b/QueryBuilder.Tests/MySql/MySqlLimitTests.cs index 3e22c19a..6360ca40 100644 --- a/QueryBuilder.Tests/MySql/MySqlLimitTests.cs +++ b/QueryBuilder.Tests/MySql/MySqlLimitTests.cs @@ -17,7 +17,7 @@ public MySqlLimitTests() public void WithNoLimitNorOffset() { var query = new Query("Table"); - var ctx = new SqlResult("?", "\\") {Query = query}; + var ctx = new CompileContext(query); Assert.Null(compiler.CompileLimit(ctx)); } @@ -26,7 +26,7 @@ public void WithNoLimitNorOffset() public void WithNoOffset() { var query = new Query("Table").Limit(10); - var ctx = new SqlResult("?", "\\") {Query = query}; + var ctx = new CompileContext(query); Assert.Equal("LIMIT ?", compiler.CompileLimit(ctx)); Assert.Equal(10, ctx.Bindings[0]); @@ -36,7 +36,7 @@ public void WithNoOffset() public void WithNoLimit() { var query = new Query("Table").Offset(20); - var ctx = new SqlResult("?", "\\") {Query = query}; + var ctx = new CompileContext(query); Assert.Equal("LIMIT 18446744073709551615 OFFSET ?", compiler.CompileLimit(ctx)); Assert.Equal(20L, ctx.Bindings[0]); @@ -47,7 +47,7 @@ public void WithNoLimit() public void WithLimitAndOffset() { var query = new Query("Table").Limit(5).Offset(20); - var ctx = new SqlResult("?", "\\") {Query = query}; + var ctx = new CompileContext(query); Assert.Equal("LIMIT ? OFFSET ?", compiler.CompileLimit(ctx)); Assert.Equal(5, ctx.Bindings[0]); diff --git a/QueryBuilder.Tests/Oracle/OracleLegacyLimitTests.cs b/QueryBuilder.Tests/Oracle/OracleLegacyLimitTests.cs index 47a233dd..3540fba1 100644 --- a/QueryBuilder.Tests/Oracle/OracleLegacyLimitTests.cs +++ b/QueryBuilder.Tests/Oracle/OracleLegacyLimitTests.cs @@ -21,7 +21,7 @@ public void WithNoLimitNorOffset() { // Arrange: var query = new Query(TableName); - var ctx = new SqlResult("?", "\\") { Query = query, RawSql = SqlPlaceholder }; + var ctx = new CompileContext(query, "?", "\\") { Query = query, RawSql = SqlPlaceholder }; // Act: compiler.ApplyLegacyLimit(ctx); @@ -35,7 +35,7 @@ public void WithNoOffset() { // Arrange: var query = new Query(TableName).Limit(10); - var ctx = new SqlResult("?", "\\") { Query = query, RawSql = SqlPlaceholder }; + var ctx = new CompileContext(query, "?", "\\") { Query = query, RawSql = SqlPlaceholder }; // Act: compiler.ApplyLegacyLimit(ctx); @@ -51,7 +51,7 @@ public void WithNoLimit() { // Arrange: var query = new Query(TableName).Offset(20); - var ctx = new SqlResult("?", "\\") { Query = query, RawSql = SqlPlaceholder }; + var ctx = new CompileContext(query, "?", "\\") { Query = query, RawSql = SqlPlaceholder }; // Act: compiler.ApplyLegacyLimit(ctx); @@ -67,7 +67,7 @@ public void WithLimitAndOffset() { // Arrange: var query = new Query(TableName).Limit(5).Offset(20); - var ctx = new SqlResult("?", "\\") { Query = query, RawSql = SqlPlaceholder }; + var ctx = new CompileContext(query, "?", "\\") { Query = query, RawSql = SqlPlaceholder }; // Act: compiler.ApplyLegacyLimit(ctx); diff --git a/QueryBuilder.Tests/Oracle/OracleLimitTests.cs b/QueryBuilder.Tests/Oracle/OracleLimitTests.cs index 9701705c..700f6e23 100644 --- a/QueryBuilder.Tests/Oracle/OracleLimitTests.cs +++ b/QueryBuilder.Tests/Oracle/OracleLimitTests.cs @@ -21,7 +21,7 @@ public void NoLimitNorOffset() { // Arrange: var query = new Query(TableName); - var ctx = new SqlResult("?", "\\") { Query = query, RawSql = SqlPlaceholder }; + var ctx = new CompileContext(query, "?", "\\") { RawSql = SqlPlaceholder }; // Act & Assert: Assert.Null(compiler.CompileLimit(ctx)); @@ -32,7 +32,7 @@ public void LimitOnly() { // Arrange: var query = new Query(TableName).Limit(10); - var ctx = new SqlResult("?", "\\") { Query = query, RawSql = SqlPlaceholder }; + var ctx = new CompileContext(query, "?", "\\") { RawSql = SqlPlaceholder }; // Act & Assert: Assert.EndsWith("OFFSET ? ROWS FETCH NEXT ? ROWS ONLY", compiler.CompileLimit(ctx)); @@ -46,7 +46,7 @@ public void OffsetOnly() { // Arrange: var query = new Query(TableName).Offset(20); - var ctx = new SqlResult("?", "\\") { Query = query, RawSql = SqlPlaceholder }; + var ctx = new CompileContext(query, "?", "\\") { RawSql = SqlPlaceholder }; // Act & Assert: Assert.EndsWith("OFFSET ? ROWS", compiler.CompileLimit(ctx)); @@ -60,7 +60,7 @@ public void LimitAndOffset() { // Arrange: var query = new Query(TableName).Limit(5).Offset(20); - var ctx = new SqlResult("?", "\\") { Query = query, RawSql = SqlPlaceholder }; + var ctx = new CompileContext(query, "?", "\\") { RawSql = SqlPlaceholder }; // Act & Assert: Assert.EndsWith("OFFSET ? ROWS FETCH NEXT ? ROWS ONLY", compiler.CompileLimit(ctx)); diff --git a/QueryBuilder.Tests/PostgreSql/PostgreSqlLimitTests.cs b/QueryBuilder.Tests/PostgreSql/PostgreSqlLimitTests.cs index 169fb99d..f7fcbfef 100644 --- a/QueryBuilder.Tests/PostgreSql/PostgreSqlLimitTests.cs +++ b/QueryBuilder.Tests/PostgreSql/PostgreSqlLimitTests.cs @@ -17,7 +17,7 @@ public PostgreSqlLimitTests() public void WithNoLimitNorOffset() { var query = new Query("Table"); - var ctx = new SqlResult("?", "\\") {Query = query}; + var ctx = new CompileContext(query); Assert.Null(compiler.CompileLimit(ctx)); } @@ -26,7 +26,7 @@ public void WithNoLimitNorOffset() public void WithNoOffset() { var query = new Query("Table").Limit(10); - var ctx = new SqlResult("?", "\\") {Query = query}; + var ctx = new CompileContext(query); Assert.Equal("LIMIT ?", compiler.CompileLimit(ctx)); Assert.Equal(10, ctx.Bindings[0]); @@ -36,7 +36,7 @@ public void WithNoOffset() public void WithNoLimit() { var query = new Query("Table").Offset(20); - var ctx = new SqlResult("?", "\\") {Query = query}; + var ctx = new CompileContext(query); Assert.Equal("OFFSET ?", compiler.CompileLimit(ctx)); Assert.Equal(20L, ctx.Bindings[0]); @@ -47,7 +47,7 @@ public void WithNoLimit() public void WithLimitAndOffset() { var query = new Query("Table").Limit(5).Offset(20); - var ctx = new SqlResult("?", "\\") {Query = query}; + var ctx = new CompileContext(query); Assert.Equal("LIMIT ? OFFSET ?", compiler.CompileLimit(ctx)); Assert.Equal(5, ctx.Bindings[0]); diff --git a/QueryBuilder.Tests/SelectTests.cs b/QueryBuilder.Tests/SelectTests.cs index 647187b9..1db3e90b 100644 --- a/QueryBuilder.Tests/SelectTests.cs +++ b/QueryBuilder.Tests/SelectTests.cs @@ -742,94 +742,94 @@ public void MultipleOrHaving() } [Fact] - public void ShouldUseILikeOnPostgresWhenNonCaseSensitive() + public void WhereLike_DefaultDatabaseHandling() { var q = new Query("Table1") - .WhereLike("Column1", "%Upper Word%", false); + .WhereLike("Column1", "%Upper Word%"); var c = Compile(q); - Assert.Equal(@"SELECT * FROM [Table1] WHERE LOWER([Column1]) like '%upper word%'", c[EngineCodes.SqlServer]); - Assert.Equal("SELECT * FROM \"Table1\" WHERE \"Column1\" ilike '%Upper Word%'", c[EngineCodes.PostgreSql]); + Assert.Equal(@"SELECT * FROM [Table1] WHERE [Column1] like '%Upper Word%'", c[EngineCodes.SqlServer]); + Assert.Equal("SELECT * FROM \"Table1\" WHERE \"Column1\" like '%Upper Word%'", c[EngineCodes.PostgreSql]); } [Fact] public void EscapedWhereLike() { var q = new Query("Table1") - .WhereLike("Column1", @"TestString\%", false, @"\"); + .WhereLike("Column1", @"TestString\%", @"\"); var c = Compile(q); - Assert.Equal(@"SELECT * FROM [Table1] WHERE LOWER([Column1]) like 'teststring\%' ESCAPE '\'", c[EngineCodes.SqlServer]); + Assert.Equal(@"SELECT * FROM [Table1] WHERE [Column1] like 'TestString\%' ESCAPE '\'", c[EngineCodes.SqlServer]); } [Fact] public void EscapedWhereStarts() { var q = new Query("Table1") - .WhereStarts("Column1", @"TestString\%", false, @"\"); + .WhereStarts("Column1", @"TestString\%", @"\"); var c = Compile(q); - Assert.Equal(@"SELECT * FROM [Table1] WHERE LOWER([Column1]) like 'teststring\%%' ESCAPE '\'", c[EngineCodes.SqlServer]); + Assert.Equal(@"SELECT * FROM [Table1] WHERE [Column1] like 'TestString\%%' ESCAPE '\'", c[EngineCodes.SqlServer]); } [Fact] public void EscapedWhereEnds() { var q = new Query("Table1") - .WhereEnds("Column1", @"TestString\%", false, @"\"); + .WhereEnds("Column1", @"TestString\%", @"\"); var c = Compile(q); - Assert.Equal(@"SELECT * FROM [Table1] WHERE LOWER([Column1]) like '%teststring\%' ESCAPE '\'", c[EngineCodes.SqlServer]); + Assert.Equal(@"SELECT * FROM [Table1] WHERE [Column1] like '%TestString\%' ESCAPE '\'", c[EngineCodes.SqlServer]); } [Fact] public void EscapedWhereContains() { var q = new Query("Table1") - .WhereContains("Column1", @"TestString\%", false, @"\"); + .WhereContains("Column1", @"TestString\%", @"\"); var c = Compile(q); - Assert.Equal(@"SELECT * FROM [Table1] WHERE LOWER([Column1]) like '%teststring\%%' ESCAPE '\'", c[EngineCodes.SqlServer]); + Assert.Equal(@"SELECT * FROM [Table1] WHERE [Column1] like '%TestString\%%' ESCAPE '\'", c[EngineCodes.SqlServer]); } [Fact] public void EscapedHavingLike() { var q = new Query("Table1") - .HavingLike("Column1", @"TestString\%", false, @"\"); + .HavingLike("Column1", @"TestString\%", @"\"); var c = Compile(q); - Assert.Equal(@"SELECT * FROM [Table1] HAVING LOWER([Column1]) like 'teststring\%' ESCAPE '\'", c[EngineCodes.SqlServer]); + Assert.Equal(@"SELECT * FROM [Table1] HAVING [Column1] like 'TestString\%' ESCAPE '\'", c[EngineCodes.SqlServer]); } [Fact] public void EscapedHavingStarts() { var q = new Query("Table1") - .HavingStarts("Column1", @"TestString\%", false, @"\"); + .HavingStarts("Column1", @"TestString\%", @"\"); var c = Compile(q); - Assert.Equal(@"SELECT * FROM [Table1] HAVING LOWER([Column1]) like 'teststring\%%' ESCAPE '\'", c[EngineCodes.SqlServer]); + Assert.Equal(@"SELECT * FROM [Table1] HAVING [Column1] like 'TestString\%%' ESCAPE '\'", c[EngineCodes.SqlServer]); } [Fact] public void EscapedHavingEnds() { var q = new Query("Table1") - .HavingEnds("Column1", @"TestString\%", false, @"\"); + .HavingEnds("Column1", @"TestString\%", @"\"); var c = Compile(q); - Assert.Equal(@"SELECT * FROM [Table1] HAVING LOWER([Column1]) like '%teststring\%' ESCAPE '\'", c[EngineCodes.SqlServer]); + Assert.Equal(@"SELECT * FROM [Table1] HAVING [Column1] like '%TestString\%' ESCAPE '\'", c[EngineCodes.SqlServer]); } [Fact] public void EscapedHavingContains() { var q = new Query("Table1") - .HavingContains("Column1", @"TestString\%", false, @"\"); + .HavingContains("Column1", @"TestString\%", @"\"); var c = Compile(q); - Assert.Equal(@"SELECT * FROM [Table1] HAVING LOWER([Column1]) like '%teststring\%%' ESCAPE '\'", c[EngineCodes.SqlServer]); + Assert.Equal(@"SELECT * FROM [Table1] HAVING [Column1] like '%TestString\%%' ESCAPE '\'", c[EngineCodes.SqlServer]); } [Fact] @@ -838,7 +838,7 @@ public void EscapeClauseThrowsForMultipleCharacters() Assert.ThrowsAny(() => { var q = new Query("Table1") - .HavingContains("Column1", @"TestString\%", false, @"\aa"); + .HavingContains("Column1", @"TestString\%", @"\aa"); }); } diff --git a/QueryBuilder.Tests/SqlServer/SqlServerLegacyLimitTests.cs b/QueryBuilder.Tests/SqlServer/SqlServerLegacyLimitTests.cs index 1cc58060..1119ceb1 100644 --- a/QueryBuilder.Tests/SqlServer/SqlServerLegacyLimitTests.cs +++ b/QueryBuilder.Tests/SqlServer/SqlServerLegacyLimitTests.cs @@ -18,7 +18,7 @@ public SqlServerLegacyLimitTests() public void NoLimitNorOffset() { var query = new Query("Table"); - var ctx = new SqlResult("?", "\\") {Query = query}; + var ctx = new CompileContext(query); Assert.Null(compiler.CompileLimit(ctx)); } @@ -27,7 +27,7 @@ public void NoLimitNorOffset() public void LimitOnly() { var query = new Query("Table").Limit(10); - var ctx = new SqlResult("?", "\\") {Query = query}; + var ctx = new CompileContext(query); Assert.Null(compiler.CompileLimit(ctx)); } @@ -36,7 +36,7 @@ public void LimitOnly() public void OffsetOnly() { var query = new Query("Table").Offset(20); - var ctx = new SqlResult("?", "\\") {Query = query}; + var ctx = new CompileContext(query); Assert.Null(compiler.CompileLimit(ctx)); } @@ -45,7 +45,7 @@ public void OffsetOnly() public void LimitAndOffset() { var query = new Query("Table").Limit(5).Offset(20); - var ctx = new SqlResult("?", "\\") {Query = query}; + var ctx = new CompileContext(query); Assert.Null(compiler.CompileLimit(ctx)); } diff --git a/QueryBuilder.Tests/SqlServer/SqlServerLimitTests.cs b/QueryBuilder.Tests/SqlServer/SqlServerLimitTests.cs index e6fa416b..e4b828ad 100644 --- a/QueryBuilder.Tests/SqlServer/SqlServerLimitTests.cs +++ b/QueryBuilder.Tests/SqlServer/SqlServerLimitTests.cs @@ -18,7 +18,7 @@ public SqlServerLimitTests() public void NoLimitNorOffset() { var query = new Query("Table"); - var ctx = new SqlResult("?", "\\") {Query = query}; + var ctx = new CompileContext(query); Assert.Null(compiler.CompileLimit(ctx)); } @@ -27,7 +27,7 @@ public void NoLimitNorOffset() public void LimitOnly() { var query = new Query("Table").Limit(10); - var ctx = new SqlResult("?", "\\") {Query = query}; + var ctx = new CompileContext(query); Assert.EndsWith("OFFSET ? ROWS FETCH NEXT ? ROWS ONLY", compiler.CompileLimit(ctx)); Assert.Equal(2, ctx.Bindings.Count); @@ -39,7 +39,7 @@ public void LimitOnly() public void OffsetOnly() { var query = new Query("Table").Offset(20); - var ctx = new SqlResult("?", "\\") {Query = query}; + var ctx = new CompileContext(query); Assert.EndsWith("OFFSET ? ROWS", compiler.CompileLimit(ctx)); @@ -51,7 +51,7 @@ public void OffsetOnly() public void LimitAndOffset() { var query = new Query("Table").Limit(5).Offset(20); - var ctx = new SqlResult("?", "\\") {Query = query}; + var ctx = new CompileContext(query); Assert.EndsWith("OFFSET ? ROWS FETCH NEXT ? ROWS ONLY", compiler.CompileLimit(ctx)); diff --git a/QueryBuilder.Tests/Sqlite/SqliteLimitTests.cs b/QueryBuilder.Tests/Sqlite/SqliteLimitTests.cs index 837a6654..8fb73f6c 100644 --- a/QueryBuilder.Tests/Sqlite/SqliteLimitTests.cs +++ b/QueryBuilder.Tests/Sqlite/SqliteLimitTests.cs @@ -17,7 +17,7 @@ public SqliteLimitTests() public void WithNoLimitNorOffset() { var query = new Query("Table"); - var ctx = new SqlResult("?", "\\") { Query = query }; + var ctx = new CompileContext(query); Assert.Null(compiler.CompileLimit(ctx)); } @@ -26,7 +26,7 @@ public void WithNoLimitNorOffset() public void WithNoOffset() { var query = new Query("Table").Limit(10); - var ctx = new SqlResult("?", "\\") { Query = query }; + var ctx = new CompileContext(query); Assert.Equal("LIMIT ?", compiler.CompileLimit(ctx)); Assert.Equal(10, ctx.Bindings[0]); @@ -36,7 +36,7 @@ public void WithNoOffset() public void WithNoLimit() { var query = new Query("Table").Offset(20); - var ctx = new SqlResult("?", "\\") { Query = query }; + var ctx = new CompileContext(query); Assert.Equal("LIMIT -1 OFFSET ?", compiler.CompileLimit(ctx)); Assert.Equal(20L, ctx.Bindings[0]); @@ -47,7 +47,7 @@ public void WithNoLimit() public void WithLimitAndOffset() { var query = new Query("Table").Limit(5).Offset(20); - var ctx = new SqlResult("?", "\\") { Query = query }; + var ctx = new CompileContext(query); Assert.Equal("LIMIT ? OFFSET ?", compiler.CompileLimit(ctx)); Assert.Equal(5, ctx.Bindings[0]); diff --git a/QueryBuilder.Tests/UpdateTests.cs b/QueryBuilder.Tests/UpdateTests.cs index bf3dd7d9..104bf0c3 100644 --- a/QueryBuilder.Tests/UpdateTests.cs +++ b/QueryBuilder.Tests/UpdateTests.cs @@ -315,5 +315,133 @@ public void DecrementUpdate() var c = Compile(query); Assert.Equal("UPDATE [Table] SET [Total] = [Total] - 2 WHERE [Name] = 'A'", c[EngineCodes.SqlServer]); } + + [Fact] + public void UpdateWithJoin() + { + var q = new Query("Posts") + .Join("Authors", "Authors.Id", "Posts.AuthorId") + .Where("Authors.Id", 5) + .AsUpdate(new[] { "Title" }, new[] { "Updated" }); + + Assert.Equal("UPDATE [Posts] SET [Title] = 'Updated' FROM [Posts] \nINNER JOIN [Authors] ON [Authors].[Id] = [Posts].[AuthorId] WHERE [Authors].[Id] = 5", Compilers.CompileFor(EngineCodes.SqlServer, q).ToString()); + Assert.Equal("UPDATE `Posts` \nINNER JOIN `Authors` ON `Authors`.`Id` = `Posts`.`AuthorId` SET `Title` = 'Updated' WHERE `Authors`.`Id` = 5", Compilers.CompileFor(EngineCodes.MySql, q).ToString()); + + Assert.Throws(() => Compilers.CompileFor(EngineCodes.PostgreSql, q)); + Assert.Throws(() => Compilers.CompileFor(EngineCodes.Sqlite, q)); + Assert.Throws(() => Compilers.CompileFor(EngineCodes.Oracle, q)); + Assert.Throws(() => Compilers.CompileFor(EngineCodes.Firebird, q)); + } + + [Fact] + public void UpdateWithJoinAndAlias() + { + var q = new Query("Posts as P") + .Join("Authors", "Authors.Id", "P.AuthorId") + .Where("Authors.Id", 5) + .AsUpdate(new[] { "P.Title" }, new[] { "Updated" }); + + Assert.Equal("UPDATE [P] SET [P].[Title] = 'Updated' FROM [Posts] AS [P] \nINNER JOIN [Authors] ON [Authors].[Id] = [P].[AuthorId] WHERE [Authors].[Id] = 5", Compilers.CompileFor(EngineCodes.SqlServer, q).ToString()); + Assert.Equal("UPDATE `Posts` AS `P` \nINNER JOIN `Authors` ON `Authors`.`Id` = `P`.`AuthorId` SET `P`.`Title` = 'Updated' WHERE `Authors`.`Id` = 5", Compilers.CompileFor(EngineCodes.MySql, q).ToString()); + } + + [Fact] + public void UpdateWithJoinAndSchema() + { + var q = new Query("Audit.Posts") + .Join("Authors", "Authors.Id", "Posts.AuthorId") + .AsUpdate(new[] { "Title" }, new[] { "Updated" }); + + Assert.Equal("UPDATE [Posts] SET [Title] = 'Updated' FROM [Audit].[Posts] \nINNER JOIN [Authors] ON [Authors].[Id] = [Posts].[AuthorId]", Compilers.CompileFor(EngineCodes.SqlServer, q).ToString()); + Assert.Equal("UPDATE `Audit`.`Posts` \nINNER JOIN `Authors` ON `Authors`.`Id` = `Posts`.`AuthorId` SET `Title` = 'Updated'", Compilers.CompileFor(EngineCodes.MySql, q).ToString()); + } + + [Fact] + public void UpdateIncrementWithJoin() + { + var q = new Query("Posts as P") + .Join("Authors", "Authors.Id", "P.AuthorId") + .Where("Authors.Id", 5) + .AsIncrement("P.Views", 1); + + Assert.Equal("UPDATE [P] SET [P].[Views] = [P].[Views] + 1 FROM [Posts] AS [P] \nINNER JOIN [Authors] ON [Authors].[Id] = [P].[AuthorId] WHERE [Authors].[Id] = 5", Compilers.CompileFor(EngineCodes.SqlServer, q).ToString()); + Assert.Equal("UPDATE `Posts` AS `P` \nINNER JOIN `Authors` ON `Authors`.`Id` = `P`.`AuthorId` SET `P`.`Views` = `P`.`Views` + 1 WHERE `Authors`.`Id` = 5", Compilers.CompileFor(EngineCodes.MySql, q).ToString()); + } + + [Fact] + public void UpdateWithJoinPredicate_BindsCorrectly() + { + var q = new Query("Posts") + .Join("Authors", j => j.On("Authors.Id", "Posts.AuthorId").Where("Authors.TenantId", 42)) + .Where("Authors.Status", "Active") + .AsUpdate(new[] { "Title" }, new[] { "Updated" }); + + var mysql = Compilers.CompileFor(EngineCodes.MySql, q); + Assert.Equal("UPDATE `Posts` \nINNER JOIN `Authors` ON (`Authors`.`Id` = `Posts`.`AuthorId` AND `Authors`.`TenantId` = 42) SET `Title` = 'Updated' WHERE `Authors`.`Status` = 'Active'", mysql.ToString()); + Assert.Equal(new object[] { 42, "Updated", "Active" }, mysql.Bindings.ToArray()); + + var sqlServer = Compilers.CompileFor(EngineCodes.SqlServer, q); + Assert.Equal("UPDATE [Posts] SET [Title] = 'Updated' FROM [Posts] \nINNER JOIN [Authors] ON ([Authors].[Id] = [Posts].[AuthorId] AND [Authors].[TenantId] = 42) WHERE [Authors].[Status] = 'Active'", sqlServer.ToString()); + Assert.Equal(new object[] { "Updated", 42, "Active" }, sqlServer.Bindings.ToArray()); + } + + [Fact] + public void UpdateIncrementWithJoinPredicate_BindsCorrectly() + { + var q = new Query("Posts") + .Join("Authors", j => j.On("Authors.Id", "Posts.AuthorId").Where("Authors.TenantId", 42)) + .Where("Authors.Status", "Active") + .AsIncrement("Views", 1); + + var mysql = Compilers.CompileFor(EngineCodes.MySql, q); + Assert.Equal("UPDATE `Posts` \nINNER JOIN `Authors` ON (`Authors`.`Id` = `Posts`.`AuthorId` AND `Authors`.`TenantId` = 42) SET `Views` = `Views` + 1 WHERE `Authors`.`Status` = 'Active'", mysql.ToString()); + Assert.Equal(new object[] { 42, 1, "Active" }, mysql.Bindings.ToArray()); + + var sqlServer = Compilers.CompileFor(EngineCodes.SqlServer, q); + Assert.Equal("UPDATE [Posts] SET [Views] = [Views] + 1 FROM [Posts] \nINNER JOIN [Authors] ON ([Authors].[Id] = [Posts].[AuthorId] AND [Authors].[TenantId] = 42) WHERE [Authors].[Status] = 'Active'", sqlServer.ToString()); + Assert.Equal(new object[] { 1, 42, "Active" }, sqlServer.Bindings.ToArray()); + } + + [Fact] + public void SqlServer_UpdateJoinedWithRawFrom_BindsCorrectly() + { + var q = new Query() + .FromRaw("(SELECT * FROM [Posts] WHERE [TenantId] = ?) AS [P]", 42) + .Join("Authors", "Authors.Id", "P.AuthorId") + .AsUpdate(new[] { "Title" }, new[] { "Updated" }); + + var c = Compilers.CompileFor(EngineCodes.SqlServer, q); + Assert.Equal("UPDATE [P] SET [Title] = 'Updated' FROM (SELECT * FROM [Posts] WHERE [TenantId] = 42) AS [P] \nINNER JOIN [Authors] ON [Authors].[Id] = [P].[AuthorId]", c.ToString()); + Assert.Equal(new object[] { "Updated", 42 }, c.Bindings.ToArray()); + + var qInc = new Query() + .FromRaw("(SELECT * FROM [Posts] WHERE [TenantId] = ?) AS [P]", 42) + .Join("Authors", "Authors.Id", "P.AuthorId") + .AsIncrement("P.Views", 1); + + var cInc = Compilers.CompileFor(EngineCodes.SqlServer, qInc); + Assert.Equal("UPDATE [P] SET [P].[Views] = [P].[Views] + 1 FROM (SELECT * FROM [Posts] WHERE [TenantId] = 42) AS [P] \nINNER JOIN [Authors] ON [Authors].[Id] = [P].[AuthorId]", cInc.ToString()); + Assert.Equal(new object[] { 1, 42 }, cInc.Bindings.ToArray()); + } + + [Fact] + public void SqlServer_UpdateNonJoinedWithRawFrom_PreservesBindingOrder() + { + var q = new Query() + .FromRaw("(SELECT * FROM [Posts] WHERE [TenantId] = ?) AS [P]", 42) + .AsUpdate(new[] { "Title" }, new[] { "Updated" }); + + var c = Compilers.CompileFor(EngineCodes.SqlServer, q); + Assert.Equal("UPDATE (SELECT * FROM [Posts] WHERE [TenantId] = 42) AS [P] SET [Title] = 'Updated'", c.ToString()); + Assert.Equal(new object[] { 42, "Updated" }, c.Bindings.ToArray()); + + var qInc = new Query() + .FromRaw("(SELECT * FROM [Posts] WHERE [TenantId] = ?) AS [P]", 42) + .AsIncrement("Views", 1); + + var cInc = Compilers.CompileFor(EngineCodes.SqlServer, qInc); + Assert.Equal("UPDATE (SELECT * FROM [Posts] WHERE [TenantId] = 42) AS [P] SET [Views] = [Views] + 1", cInc.ToString()); + Assert.Equal(new object[] { 42, 1 }, cInc.Bindings.ToArray()); + } } } diff --git a/QueryBuilder.Tests/WhereTests.cs b/QueryBuilder.Tests/WhereTests.cs index 9688c6c5..6aab3398 100644 --- a/QueryBuilder.Tests/WhereTests.cs +++ b/QueryBuilder.Tests/WhereTests.cs @@ -57,6 +57,24 @@ public void ConditionGroupStandalone() Assert.True(((AbstractCondition)group.Clauses[1]).IsOr); } + [Fact] + public void ConditionGroup_HasConditions_DeepCheck() + { + var query = new Query("Users") + .Where(q => q.For(EngineCodes.Oracle, q2 => q2.Where("Age", ">", 18))); + + Assert.True(query.Wheres.HasConditions(EngineCodes.Oracle)); + Assert.False(query.Wheres.HasConditions(EngineCodes.SqlServer)); + + var group = new ConditionGroup(); + var inner = new ConditionGroup { EngineScope = EngineCodes.Oracle }; + inner.Where("Age", ">", 18); + group.Clauses.Add(new NestedCondition { Conditions = inner }); + + Assert.True(group.HasConditions(EngineCodes.Oracle)); + Assert.False(group.HasConditions(EngineCodes.SqlServer)); + } + [Fact] public void Or_FollowedByWhere_DoesNotAffectSubsequentHaving() { @@ -246,5 +264,79 @@ public void SqlServer_LegacyPagination_InSubquery_ResolvesOuterVariables() Assert.Contains(42, result.Bindings); } + + [Fact] + public void NestedCondition_ScopedToAnotherEngine_ShouldNotProduceBareWhere() + { + var query = new Query("Users") + .Where(q => q.For(EngineCodes.Oracle, q2 => q2.Where("Age", ">", 18))); + + var compiler = new SqlServerCompiler(); + var result = compiler.Compile(query); + + Assert.Equal("SELECT * FROM [Users]", result.RawSql); + } + + [Fact] + public void NestedCondition_ScopedToAnotherEngine_ShouldNotProduceBareHaving() + { + var query = new Query("Users") + .Having(q => q.For(EngineCodes.Oracle, q2 => q2.Having("Age", ">", 18))); + + var compiler = new SqlServerCompiler(); + var result = compiler.Compile(query); + + Assert.Equal("SELECT * FROM [Users]", result.RawSql); + } + + [Fact] + public void WhereIn_EmptyCollection_ShouldEmitConstantPredicateWithoutColumn() + { + var query = new Query("Users").WhereIn("id", System.Array.Empty()); + + var c = Compile(query); + + Assert.Equal("SELECT * FROM [Users] WHERE 1 = 0 /* IN [empty list] */", c[EngineCodes.SqlServer]); + Assert.Equal("SELECT * FROM \"Users\" WHERE 1 = 0 /* IN [empty list] */", c[EngineCodes.PostgreSql]); + } + + [Fact] + public void WhereNotIn_EmptyCollection_ShouldEmitConstantPredicateWithoutColumn() + { + var query = new Query("Users").WhereNotIn("id", System.Array.Empty()); + + var c = Compile(query); + + Assert.Equal("SELECT * FROM [Users] WHERE 1 = 1 /* NOT IN [empty list] */", c[EngineCodes.SqlServer]); + Assert.Equal("SELECT * FROM \"Users\" WHERE 1 = 1 /* NOT IN [empty list] */", c[EngineCodes.PostgreSql]); + } + + [Fact] + public void WhereIn_EmptyCollection_CombinedWithOtherConditions() + { + var query = new Query("Users") + .Where("Status", "Active") + .WhereIn("id", System.Array.Empty()); + + var c = Compile(query); + + Assert.Equal("SELECT * FROM [Users] WHERE [Status] = 'Active' AND 1 = 0 /* IN [empty list] */", c[EngineCodes.SqlServer]); + } + + [Fact] + public void Subquery_AggregateFilter_ResolvesOuterVariables() + { + var q = new Query("Orders") + .Define("tenantId", 42) + .Where("Total", ">", sub => sub.From("Stats") + .SelectSum("Amount", f => f.Where("TenantId", Expressions.Variable("tenantId")))); + + var c = Compilers.Compile(q); + + Assert.Contains(42, c[EngineCodes.SqlServer].Bindings); + Assert.Equal("SELECT * FROM [Orders] WHERE [Total] > (SELECT SUM(CASE WHEN [TenantId] = ? THEN [Amount] END) FROM [Stats])", c[EngineCodes.SqlServer].RawSql); + Assert.Contains(42, c[EngineCodes.PostgreSql].Bindings); + Assert.Equal("SELECT * FROM \"Orders\" WHERE \"Total\" > (SELECT SUM(\"Amount\") FILTER (WHERE \"TenantId\" = ?) FROM \"Stats\")", c[EngineCodes.PostgreSql].RawSql); + } } } diff --git a/QueryBuilder/Clauses/ConditionClause.cs b/QueryBuilder/Clauses/ConditionClause.cs index 4e48e82c..f408ab37 100644 --- a/QueryBuilder/Clauses/ConditionClause.cs +++ b/QueryBuilder/Clauses/ConditionClause.cs @@ -37,9 +37,6 @@ public override AbstractClause Clone() public class BasicStringCondition : BasicCondition { - - public bool CaseSensitive { get; set; } = false; - private string escapeCharacter = null; public string EscapeCharacter { @@ -64,7 +61,6 @@ public override AbstractClause Clone() Value = Value, IsOr = IsOr, IsNot = IsNot, - CaseSensitive = CaseSensitive, EscapeCharacter = EscapeCharacter, Component = Component, }; diff --git a/QueryBuilder/Clauses/OrderClause.cs b/QueryBuilder/Clauses/OrderClause.cs index 69af772e..7b2a84e3 100644 --- a/QueryBuilder/Clauses/OrderClause.cs +++ b/QueryBuilder/Clauses/OrderClause.cs @@ -43,12 +43,16 @@ public override AbstractClause Clone() public class OrderByRandom : AbstractOrderBy { + public int? Seed { get; set; } + /// public override AbstractClause Clone() { return new OrderByRandom { Engine = Engine, + Component = Component, + Seed = Seed, }; } } diff --git a/QueryBuilder/Compilers/CompileContext.cs b/QueryBuilder/Compilers/CompileContext.cs new file mode 100644 index 00000000..e1f1a828 --- /dev/null +++ b/QueryBuilder/Compilers/CompileContext.cs @@ -0,0 +1,129 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace SqlKata.Compilers +{ + public class CompileContext + { + public StringBuilder Buffer { get; } = new(); + public List Bindings { get; set; } = []; + public string RawSql { get; set; } = ""; + public string ParameterPlaceholder { get; set; } = "?"; + public string EscapeCharacter { get; set; } = "\\"; + + private readonly Stack _queryStack = new(); + + public Query CurrentQuery => _queryStack.Count > 0 ? _queryStack.Peek() : null; + + public Query Query + { + get => CurrentQuery; + set + { + if (_queryStack.Count > 0) + { + _queryStack.Pop(); + } + + if (value != null) + { + _queryStack.Push(value); + } + } + } + + public IReadOnlyCollection QueryStack => _queryStack; + + public CompileContext() : this(null, "?", "\\") + { + } + + public CompileContext(Query query, string parameterPlaceholder = "?", string escapeCharacter = "\\") + { + ParameterPlaceholder = parameterPlaceholder; + EscapeCharacter = escapeCharacter; + if (query != null) + { + _queryStack.Push(query); + } + } + + public CompileContext(CompileContext other) + { + if (other != null) + { + ParameterPlaceholder = other.ParameterPlaceholder; + EscapeCharacter = other.EscapeCharacter; + CopyQueryStackFrom(other); + } + } + + public void CopyQueryStackFrom(CompileContext source) + { + if (source?.QueryStack == null) return; + _queryStack.Clear(); + foreach (var q in source.QueryStack.Reverse()) + { + if (q != null) + { + _queryStack.Push(q); + } + } + } + + public void PushQuery(Query query) + { + if (query != null) + { + _queryStack.Push(query); + } + } + + public Query PopQuery() + { + return _queryStack.Count > 0 ? _queryStack.Pop() : null; + } + + public object FindVariable(string name) + { + foreach (var q in _queryStack) + { + if (q?.Variables != null && q.Variables.TryGetValue(name, out var value)) + { + return value; + } + } + + throw new Exception($"Variable '{name}' not found"); + } + + public CompileContext Write(string value) + { + Buffer.Append(value); + return this; + } + + public CompileContext Write(char value) + { + Buffer.Append(value); + return this; + } + + public CompileContext EnsureSpace() + { + if (Buffer.Length > 0) + { + var lastChar = Buffer[Buffer.Length - 1]; + if (lastChar != ' ' && lastChar != '\n') + { + Buffer.Append(' '); + } + } + return this; + } + + public override string ToString() => Buffer.ToString(); + } +} diff --git a/QueryBuilder/Compilers/Compiler.Conditions.cs b/QueryBuilder/Compilers/Compiler.Conditions.cs index e252b1b1..218b1d59 100644 --- a/QueryBuilder/Compilers/Compiler.Conditions.cs +++ b/QueryBuilder/Compilers/Compiler.Conditions.cs @@ -1,108 +1,152 @@ using System; using System.Collections.Generic; using System.Linq; -using System.Reflection; namespace SqlKata.Compilers { public partial class Compiler { - protected virtual string CompileCondition(SqlResult ctx, AbstractCondition clause) - { - return clause switch - { - BasicDateCondition date => CompileBasicDateCondition(ctx, date), - BasicStringCondition str => CompileBasicStringCondition(ctx, str), - BasicCondition basic => CompileBasicCondition(ctx, basic), - TwoColumnsCondition two => CompileTwoColumnsCondition(ctx, two), - BetweenCondition between => CompileBetweenCondition(ctx, between), - InCondition inCondition => CompileInCondition(ctx, inCondition), - InQueryCondition inQuery => CompileInQueryCondition(ctx, inQuery), - NullCondition nullCondition => CompileNullCondition(ctx, nullCondition), - BooleanCondition booleanCondition => CompileBooleanCondition(ctx, booleanCondition), - NestedCondition nested => CompileNestedCondition(ctx, nested), - ExistsCondition exists => CompileExistsCondition(ctx, exists), - QueryCondition queryCondition => CompileQueryCondition(ctx, queryCondition), - SubQueryCondition subQuery => CompileSubQueryCondition(ctx, subQuery), - RawCondition raw => CompileRawCondition(ctx, raw), - _ => throw new ArgumentOutOfRangeException(nameof(clause), $"Unsupported condition type: {clause.GetType().Name}") - }; - } + private static readonly string[] stringOperators = ["starts", "ends", "contains", "like"]; - protected virtual string CompileConditions(SqlResult ctx, List conditions) + public virtual void VisitConditions(CompileContext ctx, List conditions) { - var result = new List(); + var isFirst = true; for (var i = 0; i < conditions.Count; i++) { - var compiled = CompileCondition(ctx, conditions[i]); + var cond = conditions[i]; + var beforeLen = ctx.Buffer.Length; - if (string.IsNullOrEmpty(compiled)) + if (!isFirst) { - continue; + ctx.Write(cond.IsOr ? "OR " : "AND "); } - var boolOperator = i == 0 ? "" : (conditions[i].IsOr ? "OR " : "AND "); + var condStart = ctx.Buffer.Length; + VisitCondition(ctx, cond); - result.Add(boolOperator + compiled); + if (ctx.Buffer.Length == condStart) + { + // Nothing was written for this condition, rollback operator if added + ctx.Buffer.Length = beforeLen; + } + else + { + isFirst = false; + if (i < conditions.Count - 1) + { + ctx.Write(" "); + } + } } - return string.Join(" ", result); + // trim the ending whitespace + if (!isFirst && ctx.Buffer.Length > 0 && ctx.Buffer[^1] == ' ') + { + ctx.Buffer.Length--; + } } - protected virtual string CompileRawCondition(SqlResult ctx, RawCondition x) + public virtual void VisitCondition(CompileContext ctx, AbstractCondition clause) { - ctx.Bindings.AddRange(x.Bindings); - return WrapIdentifiers(x.Expression); + switch (clause) + { + case BasicDateCondition date: + VisitBasicDateCondition(ctx, date); + break; + case BasicStringCondition str: + VisitBasicStringCondition(ctx, str); + break; + case BasicCondition basic: + VisitBasicCondition(ctx, basic); + break; + case TwoColumnsCondition two: + VisitTwoColumnsCondition(ctx, two); + break; + case BetweenCondition between: + VisitBetweenCondition(ctx, between); + break; + case InCondition inCondition: + VisitInCondition(ctx, inCondition); + break; + case InQueryCondition inQuery: + VisitInQueryCondition(ctx, inQuery); + break; + case NullCondition nullCondition: + VisitNullCondition(ctx, nullCondition); + break; + case BooleanCondition booleanCondition: + VisitBooleanCondition(ctx, booleanCondition); + break; + case NestedCondition nested: + VisitNestedCondition(ctx, nested); + break; + case ExistsCondition exists: + VisitExistsCondition(ctx, exists); + break; + case QueryCondition queryCondition: + VisitQueryCondition(ctx, queryCondition); + break; + case SubQueryCondition subQuery: + VisitSubQueryCondition(ctx, subQuery); + break; + case RawCondition raw: + VisitRawCondition(ctx, raw); + break; + default: + throw new ArgumentOutOfRangeException(nameof(clause), $"Unsupported condition type: {clause.GetType().Name}"); + } } - protected virtual string CompileQueryCondition(SqlResult ctx, QueryCondition x) + protected virtual void VisitRawCondition(CompileContext ctx, RawCondition x) { - var subCtx = CompileSubQuery(ctx, x.Query); - - ctx.Bindings.AddRange(subCtx.Bindings); - - return Wrap(x.Column) + " " + checkOperator(x.Operator) + " (" + subCtx.RawSql + ")"; + ctx.Bindings.AddRange(x.Bindings); + ctx.Write(WrapIdentifiers(x.Expression)); } - protected virtual string CompileSubQueryCondition(SqlResult ctx, SubQueryCondition x) + protected virtual void VisitQueryCondition(CompileContext ctx, QueryCondition x) { - var subCtx = CompileSubQuery(ctx, x.Query); - - ctx.Bindings.AddRange(subCtx.Bindings); - - return "(" + subCtx.RawSql + ") " + checkOperator(x.Operator) + " " + Parameter(ctx, x.Value); + ctx.Write(Wrap(x.Column)).Write(" ").Write(checkOperator(x.Operator)).Write(" ("); + CompileSubQuery(ctx, x.Query); + ctx.Write(")"); } - protected virtual string CompileBasicCondition(SqlResult ctx, BasicCondition x) + protected virtual void VisitSubQueryCondition(CompileContext ctx, SubQueryCondition x) { - var sql = $"{Wrap(x.Column)} {checkOperator(x.Operator)} {Parameter(ctx, x.Value)}"; + ctx.Write("("); + CompileSubQuery(ctx, x.Query); + ctx.Write(") ").Write(checkOperator(x.Operator)).Write(" ").Write(Parameter(ctx, x.Value)); + } + protected virtual void VisitBasicCondition(CompileContext ctx, BasicCondition x) + { if (x.IsNot) { - return $"NOT ({sql})"; + ctx.Write("NOT ("); } - return sql; + ctx.Write(Wrap(x.Column)).Write(" ").Write(checkOperator(x.Operator)).Write(" ").Write(Parameter(ctx, x.Value)); + + if (x.IsNot) + { + ctx.Write(")"); + } } - protected virtual string CompileBasicStringCondition(SqlResult ctx, BasicStringCondition x) + protected virtual void VisitBasicStringCondition(CompileContext ctx, BasicStringCondition x) { - var column = Wrap(x.Column); - var value = Resolve(ctx, x.Value) as string; - if (value == null) + if (Resolve(ctx, x.Value) is not string value) { throw new ArgumentException("Expecting a non nullable string"); } var method = x.Operator; - if (new[] { "starts", "ends", "contains", "like" }.Contains(x.Operator)) + if (stringOperators.Contains(x.Operator)) { - method = "LIKE"; switch (x.Operator) @@ -119,148 +163,199 @@ protected virtual string CompileBasicStringCondition(SqlResult ctx, BasicStringC } } - string sql; - - - if (!x.CaseSensitive) + if (x.IsNot) { - column = CompileLower(column); - value = value.ToLowerInvariant(); + ctx.Write("NOT ("); } if (x.Value is UnsafeLiteral) { - sql = $"{column} {checkOperator(method)} {value}"; + ctx.Write(column).Write(" ").Write(checkOperator(method)).Write(" ").Write(value); } else { - sql = $"{column} {checkOperator(method)} {Parameter(ctx, value)}"; + ctx.Write(column).Write(" ").Write(checkOperator(method)).Write(" ").Write(Parameter(ctx, value)); } if (!string.IsNullOrEmpty(x.EscapeCharacter)) { - sql = $"{sql} ESCAPE '{x.EscapeCharacter}'"; + ctx.Write(" ESCAPE '").Write(x.EscapeCharacter).Write("'"); } - return x.IsNot ? $"NOT ({sql})" : sql; - + if (x.IsNot) + { + ctx.Write(")"); + } } - protected virtual string CompileBasicDateCondition(SqlResult ctx, BasicDateCondition x) + protected virtual void VisitBasicDateCondition(CompileContext ctx, BasicDateCondition x) { var column = Wrap(x.Column); var op = checkOperator(x.Operator); - var sql = $"{x.Part.ToUpperInvariant()}({column}) {op} {Parameter(ctx, x.Value)}"; + if (x.IsNot) + { + ctx.Write("NOT ("); + } + + ctx.Write(x.Part.ToUpperInvariant()).Write("(").Write(column).Write(") ").Write(op).Write(" ").Write(Parameter(ctx, x.Value)); - return x.IsNot ? $"NOT ({sql})" : sql; + if (x.IsNot) + { + ctx.Write(")"); + } } - protected virtual string CompileNestedCondition(SqlResult ctx, NestedCondition x) + protected virtual void VisitNestedCondition(CompileContext ctx, NestedCondition x) { + List clauses; + if (x.Conditions is ConditionGroup group) { if (!group.HasConditions(EngineCode)) { - return null; + return; + } + + clauses = group.GetConditions(EngineCode); + } + else + { + if (!(x.Conditions.HasComponent("where", EngineCode) || x.Conditions.HasComponent("having", EngineCode))) + { + return; } - var sql = CompileConditions(ctx, group.GetConditions(EngineCode)); - return x.IsNot ? $"NOT ({sql})" : $"({sql})"; + var clause = x.Conditions.HasComponent("where", EngineCode) ? "where" : "having"; + clauses = x.Conditions.GetComponents(clause, EngineCode); } - if (!(x.Conditions.HasComponent("where", EngineCode) || x.Conditions.HasComponent("having", EngineCode))) + var startLen = ctx.Buffer.Length; + + if (x.IsNot) { - return null; + ctx.Write("NOT "); } - var clause = x.Conditions.HasComponent("where", EngineCode) ? "where" : "having"; + ctx.Write("("); + var innerStart = ctx.Buffer.Length; - var clauses = x.Conditions.GetComponents(clause, EngineCode); + VisitConditions(ctx, clauses); - var sql2 = CompileConditions(ctx, clauses); + if (ctx.Buffer.Length == innerStart) + { + // Nothing was written inside the nested group, roll back + ctx.Buffer.Length = startLen; + return; + } - return x.IsNot ? $"NOT ({sql2})" : $"({sql2})"; + ctx.Write(")"); } - protected string CompileTwoColumnsCondition(SqlResult ctx, TwoColumnsCondition clause) + protected virtual void VisitTwoColumnsCondition(CompileContext ctx, TwoColumnsCondition clause) { - var op = clause.IsNot ? "NOT " : ""; - return $"{op}{Wrap(clause.First)} {checkOperator(clause.Operator)} {Wrap(clause.Second)}"; + if (clause.IsNot) + { + ctx.Write("NOT "); + } + + ctx.Write(Wrap(clause.First)).Write(" ").Write(checkOperator(clause.Operator)).Write(" ").Write(Wrap(clause.Second)); } - protected virtual string CompileBetweenCondition(SqlResult ctx, BetweenCondition item) + protected virtual void VisitBetweenCondition(CompileContext ctx, BetweenCondition item) { var between = item.IsNot ? "NOT BETWEEN" : "BETWEEN"; - var lower = Parameter(ctx, item.Lower); - var higher = Parameter(ctx, item.Higher); - - return Wrap(item.Column) + $" {between} {lower} AND {higher}"; + ctx.Write(Wrap(item.Column)).Write(" ").Write(between).Write(" ") + .Write(Parameter(ctx, item.Lower)).Write(" AND ").Write(Parameter(ctx, item.Higher)); } - protected virtual string CompileInCondition(SqlResult ctx, InCondition item) + protected virtual void VisitInCondition(CompileContext ctx, InCondition item) { - var column = Wrap(item.Column); - if (!item.Values.Any()) { - return item.IsNot ? $"1 = 1 /* NOT IN [empty list] */" : "1 = 0 /* IN [empty list] */"; + ctx.Write(item.IsNot ? "1 = 1 /* NOT IN [empty list] */" : "1 = 0 /* IN [empty list] */"); + return; } - var inOperator = item.IsNot ? "NOT IN" : "IN"; - - var parameterizedValues = Parameterize(ctx, item.Values); - - return column + $" {inOperator} ({parameterizedValues})"; + var inOperator = item.IsNot ? " NOT IN (" : " IN ("; + ctx.Write(Wrap(item.Column)).Write(inOperator).Write(Parameterize(ctx, item.Values)).Write(")"); } - protected virtual string CompileInQueryCondition(SqlResult ctx, InQueryCondition item) + protected virtual void VisitInQueryCondition(CompileContext ctx, InQueryCondition item) { - - var subCtx = CompileSubQuery(ctx, item.Query); - - ctx.Bindings.AddRange(subCtx.Bindings); - - var inOperator = item.IsNot ? "NOT IN" : "IN"; - - return Wrap(item.Column) + $" {inOperator} ({subCtx.RawSql})"; + var inOperator = item.IsNot ? " NOT IN (" : " IN ("; + ctx.Write(Wrap(item.Column)).Write(inOperator); + CompileSubQuery(ctx, item.Query); + ctx.Write(")"); } - protected virtual string CompileNullCondition(SqlResult ctx, NullCondition item) + protected virtual void VisitNullCondition(CompileContext ctx, NullCondition item) { var op = item.IsNot ? "IS NOT NULL" : "IS NULL"; - return Wrap(item.Column) + " " + op; + ctx.Write(Wrap(item.Column)).Write(" ").Write(op); } - protected virtual string CompileBooleanCondition(SqlResult ctx, BooleanCondition item) + protected virtual void VisitBooleanCondition(CompileContext ctx, BooleanCondition item) { - var column = Wrap(item.Column); var value = item.Value ? CompileTrue() : CompileFalse(); - var op = item.IsNot ? "!=" : "="; - - return $"{column} {op} {value}"; + ctx.Write(Wrap(item.Column)).Write(" ").Write(op).Write(" ").Write(value); } - protected virtual string CompileExistsCondition(SqlResult ctx, ExistsCondition item) + protected virtual void VisitExistsCondition(CompileContext ctx, ExistsCondition item) { - var op = item.IsNot ? "NOT EXISTS" : "EXISTS"; + var op = item.IsNot ? "NOT EXISTS (" : "EXISTS ("; - - // remove unneeded components var query = item.Query.Clone(); - if (OmitSelectInsideExists) { query.SelectClauses.Clear(); query.SelectRaw("1"); } - var subCtx = CompileSubQuery(ctx, query); + ctx.Write(op); + CompileSubQuery(ctx, query); + ctx.Write(")"); + } - ctx.Bindings.AddRange(subCtx.Bindings); + // Backward compatibility wrappers delegating to visitor methods + protected virtual string CompileCondition(CompileContext ctx, AbstractCondition clause) + { + var temp = new CompileContext(ctx); + VisitCondition(temp, clause); + ctx.Bindings.AddRange(temp.Bindings); + return temp.Buffer.ToString(); + } - return $"{op} ({subCtx.RawSql})"; + protected virtual string CompileConditions(CompileContext ctx, List conditions) + { + var temp = new CompileContext(ctx); + VisitConditions(temp, conditions); + ctx.Bindings.AddRange(temp.Bindings); + return temp.Buffer.ToString(); + } + + protected virtual string CompileRawCondition(CompileContext ctx, RawCondition x) => RunToString(ctx, c => VisitRawCondition(c, x)); + protected virtual string CompileQueryCondition(CompileContext ctx, QueryCondition x) => RunToString(ctx, c => VisitQueryCondition(c, x)); + protected virtual string CompileSubQueryCondition(CompileContext ctx, SubQueryCondition x) => RunToString(ctx, c => VisitSubQueryCondition(c, x)); + protected virtual string CompileBasicCondition(CompileContext ctx, BasicCondition x) => RunToString(ctx, c => VisitBasicCondition(c, x)); + protected virtual string CompileBasicStringCondition(CompileContext ctx, BasicStringCondition x) => RunToString(ctx, c => VisitBasicStringCondition(c, x)); + protected virtual string CompileBasicDateCondition(CompileContext ctx, BasicDateCondition x) => RunToString(ctx, c => VisitBasicDateCondition(c, x)); + protected virtual string CompileNestedCondition(CompileContext ctx, NestedCondition x) => RunToString(ctx, c => VisitNestedCondition(c, x)); + protected virtual string CompileTwoColumnsCondition(CompileContext ctx, TwoColumnsCondition clause) => RunToString(ctx, c => VisitTwoColumnsCondition(c, clause)); + protected virtual string CompileBetweenCondition(CompileContext ctx, BetweenCondition item) => RunToString(ctx, c => VisitBetweenCondition(c, item)); + protected virtual string CompileInCondition(CompileContext ctx, InCondition item) => RunToString(ctx, c => VisitInCondition(c, item)); + protected virtual string CompileInQueryCondition(CompileContext ctx, InQueryCondition item) => RunToString(ctx, c => VisitInQueryCondition(c, item)); + protected virtual string CompileNullCondition(CompileContext ctx, NullCondition item) => RunToString(ctx, c => VisitNullCondition(c, item)); + protected virtual string CompileBooleanCondition(CompileContext ctx, BooleanCondition item) => RunToString(ctx, c => VisitBooleanCondition(c, item)); + protected virtual string CompileExistsCondition(CompileContext ctx, ExistsCondition item) => RunToString(ctx, c => VisitExistsCondition(c, item)); + + private static string RunToString(CompileContext ctx, Action action) + { + var temp = new CompileContext(ctx); + action(temp); + ctx.Bindings.AddRange(temp.Bindings); + return temp.Buffer.ToString(); } } } diff --git a/QueryBuilder/Compilers/Compiler.cs b/QueryBuilder/Compilers/Compiler.cs index a09d8124..668d871c 100644 --- a/QueryBuilder/Compilers/Compiler.cs +++ b/QueryBuilder/Compilers/Compiler.cs @@ -16,11 +16,9 @@ public partial class Compiler protected virtual string LastId { get; set; } = ""; protected virtual string EscapeCharacter { get; set; } = "\\"; - protected virtual string SingleInsertStartClause { get; set; } = "INSERT INTO"; protected virtual string MultiInsertStartClause { get; set; } = "INSERT INTO"; - protected Compiler() { } @@ -30,23 +28,20 @@ protected Compiler() /// /// Whether the compiler supports the `SELECT ... FILTER` syntax /// - /// public virtual bool SupportsFilterClause { get; set; } = false; /// /// If true the compiler will remove the SELECT clause for the query used inside WHERE EXISTS /// - /// public virtual bool OmitSelectInsideExists { get; set; } = true; - protected virtual string SingleRowDummyTableName { get => null; } + protected virtual string SingleRowDummyTableName => null; /// /// A list of white-listed operators /// - /// - protected readonly HashSet operators = new HashSet - { + protected readonly HashSet operators = + [ "=", "<", ">", "<=", ">=", "<>", "!=", "<=>", "like", "not like", "ilike", "not ilike", @@ -54,12 +49,9 @@ protected Compiler() "rlike", "not rlike", "regexp", "not regexp", "similar to", "not similar to" - }; + ]; - protected HashSet userOperators = new HashSet - { - - }; + protected HashSet userOperators = []; protected Dictionary generateNamedBindings(object[] bindings) { @@ -67,23 +59,23 @@ protected Dictionary generateNamedBindings(object[] bindings) .ToDictionary(x => parameterPrefix + x.i, x => x.v); } - protected SqlResult PrepareResult(SqlResult ctx) + protected SqlResult PrepareResult(CompileContext ctx) { - ctx.NamedBindings = generateNamedBindings(ctx.Bindings.ToArray()); - ctx.Sql = Helper.ReplaceAll(ctx.RawSql, parameterPlaceholder, EscapeCharacter, i => parameterPrefix + i); - return ctx; + return new SqlResult(parameterPlaceholder, EscapeCharacter) + { + Query = ctx.Query, + RawSql = ctx.RawSql, + Bindings = ctx.Bindings, + NamedBindings = generateNamedBindings(ctx.Bindings.ToArray()), + Sql = Helper.ReplaceAll(ctx.RawSql, parameterPlaceholder, EscapeCharacter, i => parameterPrefix + i) + }; } - private Query TransformAggregateQuery(Query query) { var clause = query.AggregateClauses.ForEngineOrDefault(EngineCode); // Case 1: Simple single-column aggregate without DISTINCT. - // Example: query.From("users").AsCount("id") - // Output: SELECT COUNT("id") FROM "users" - // No subquery is required; standard SQL natively supports single-column aggregates. - // Limits, orders, and group-by clauses are stripped because they do not apply to a scalar aggregate. if (clause.Columns.Count == 1 && !query.IsDistinct) { var single = query.Clone(); @@ -94,7 +86,6 @@ private Query TransformAggregateQuery(Query query) } // Case 2 & 3: Multi-column aggregates or DISTINCT aggregates require wrapping the query in a subquery: - // Output template: SELECT (*) FROM () AS Query var transformed = query.Clone(); transformed.LimitClauses.Clear(); transformed.OrderByClauses.Clear(); @@ -102,24 +93,12 @@ private Query TransformAggregateQuery(Query query) if (transformed.IsDistinct) { - // Case 2: DISTINCT aggregate. - // Example: query.From("users").Distinct().AsCount("first_name", "last_name") - // SQL dialects generally do not allow multi-column COUNT(DISTINCT a, b). - // Instead, the inner query projects the distinct columns: - // Inner: SELECT DISTINCT "first_name", "last_name" FROM "users" - // Outer: SELECT COUNT(*) FROM (SELECT DISTINCT "first_name", "last_name" FROM "users") AS countQuery transformed.AggregateClauses.ClearForEngine(EngineCode); transformed.SelectClauses.ClearForEngine(EngineCode); transformed.Select(clause.Columns.ToArray()); } else { - // Case 3: Multi-column aggregate without DISTINCT. - // Example: query.From("users").AsCount("first_name", "last_name") - // In standard SQL, COUNT(a, b) is invalid syntax because aggregates take a single expression or '*'. - // In SQL aggregate semantics, counting multiple columns means counting rows where none of those columns are NULL: - // Inner: SELECT * FROM "users" WHERE "first_name" IS NOT NULL AND "last_name" IS NOT NULL - // Outer: SELECT COUNT(*) FROM (SELECT * FROM "users" WHERE "first_name" IS NOT NULL AND "last_name" IS NOT NULL) AS countQuery foreach (var column in clause.Columns) { transformed.WhereNotNull(column); @@ -139,49 +118,17 @@ private Query TransformAggregateQuery(Query query) return outerQuery; } - protected virtual SqlResult CompileRaw(Query query) + protected virtual CompileContext CompileRaw(Query query) { - QueryStack.Push(query); - try - { - SqlResult ctx; - - if (query.Method == "insert") - { - ctx = CompileInsertQuery(query); - } - else if (query.Method == "update") - { - ctx = CompileUpdateQuery(query); - } - else if (query.Method == "delete") - { - ctx = CompileDeleteQuery(query); - } - else - { - if (query.Method == "aggregate") - { - query = TransformAggregateQuery(query); - } - - ctx = CompileSelectQuery(query); - } - - // handle CTEs - if (query.CteClauses.ForEngine(EngineCode).Any()) - { - ctx = CompileCteQuery(ctx, query); - } - - ctx.RawSql = Helper.ExpandParameters(ctx.RawSql, parameterPlaceholder, EscapeCharacter, ctx.Bindings.ToArray()); + var ctx = new CompileContext(query, parameterPlaceholder, EscapeCharacter); + return CompileRaw(ctx); + } - return ctx; - } - finally - { - QueryStack.Pop(); - } + protected virtual CompileContext CompileRaw(CompileContext ctx) + { + VisitQuery(ctx, ctx.Query); + ctx.RawSql = Helper.ExpandParameters(ctx.Buffer.ToString(), parameterPlaceholder, EscapeCharacter, ctx.Bindings.ToArray()); + return ctx; } /// @@ -205,9 +152,7 @@ public virtual SqlResult Compile(Query query) { var ctx = CompileRaw(query); - ctx = PrepareResult(ctx); - - return ctx; + return PrepareResult(ctx); } public virtual SqlResult Compile(IEnumerable queries) @@ -222,109 +167,79 @@ public virtual SqlResult Compile(IEnumerable queries) combinedBindings.AddRange(cb); } - var ctx = new SqlResult(parameterPlaceholder, EscapeCharacter) + var ctx = new CompileContext(null, parameterPlaceholder, EscapeCharacter) { RawSql = compiled.Select(r => r.RawSql).Aggregate((a, b) => a + ";\n" + b), Bindings = combinedBindings, }; - ctx = PrepareResult(ctx); - - return ctx; + return PrepareResult(ctx); } - [ThreadStatic] - private static Stack _queryStack; - - protected static Stack QueryStack => _queryStack ??= new Stack(); - - public virtual object FindVariable(string name) + public virtual void CompileSubQuery(CompileContext ctx, Query query) { - 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); + ctx.PushQuery(query); try { - return CompileSelectQuery(query); + VisitSelectQuery(ctx, query); } finally { - QueryStack.Pop(); + ctx.PopQuery(); } } - protected virtual SqlResult CompileSelectQuery(Query query) + // ========================================== + // Visitor Methods: Queries / Statements + // ========================================== + + public virtual void VisitQuery(CompileContext ctx, Query query) { - var ctx = new SqlResult(parameterPlaceholder, EscapeCharacter) + if (query.CteClauses.ForEngine(EngineCode).Any()) { - Query = query, - }; + VisitCteQuery(ctx, query); + } - var results = new[] { - this.CompileColumns(ctx), - this.CompileFrom(ctx), - this.CompileJoins(ctx), - this.CompileWheres(ctx), - this.CompileGroups(ctx), - this.CompileHaving(ctx), - this.CompileOrders(ctx), - this.CompileLimit(ctx), - this.CompileUnion(ctx), + if (query.Method == "insert") + { + VisitInsertQuery(ctx, query); + } + else if (query.Method == "update") + { + VisitUpdateQuery(ctx, query); + } + else if (query.Method == "delete") + { + VisitDeleteQuery(ctx, query); + } + else + { + if (query.Method == "aggregate") + { + query = TransformAggregateQuery(query); + ctx.Query = query; } - .Where(x => x != null) - .Where(x => !string.IsNullOrEmpty(x)) - .ToList(); - string sql = string.Join(" ", results); - - ctx.RawSql = sql; - - return ctx; + VisitSelectQuery(ctx, query); + } } - protected virtual SqlResult CompileAdHocQuery(AdHocTableFromClause adHoc) + public virtual void VisitSelectQuery(CompileContext ctx, Query query) { - var ctx = new SqlResult(parameterPlaceholder, EscapeCharacter); - - var row = "SELECT " + string.Join(", ", adHoc.Columns.Select(col => $"{parameterPlaceholder} AS {Wrap(col)}")); - - var fromTable = SingleRowDummyTableName; - - if (fromTable != null) - { - row += $" FROM {fromTable}"; - } - - var rows = string.Join(" UNION ALL ", Enumerable.Repeat(row, adHoc.Values.Count / adHoc.Columns.Count)); - - ctx.RawSql = rows; - ctx.Bindings = adHoc.Values; - - return ctx; + VisitColumns(ctx, query); + VisitFrom(ctx, query); + VisitJoins(ctx, query); + VisitWheres(ctx, query); + VisitGroups(ctx, query); + VisitHaving(ctx, query); + VisitOrders(ctx, query); + VisitLimit(ctx, query); + VisitUnions(ctx, query); } - protected virtual SqlResult CompileDeleteQuery(Query query) + public virtual void VisitDeleteQuery(CompileContext ctx, Query query) { - var ctx = new SqlResult(parameterPlaceholder, EscapeCharacter) - { - Query = query - }; - - var fromClause = ctx.Query.FromClauses.ForEngineOrDefault(EngineCode); + var fromClause = query.FromClauses.ForEngineOrDefault(EngineCode); if (fromClause is null) { throw new InvalidOperationException("No table set to delete"); @@ -348,44 +263,25 @@ protected virtual SqlResult CompileDeleteQuery(Query query) throw new InvalidOperationException("Invalid table expression"); } - var joins = CompileJoins(ctx); - - var where = CompileWheres(ctx); - - if (!string.IsNullOrEmpty(where)) - { - where = " " + where; - } + var hasJoins = query.Joins.ForEngine(EngineCode).Any(); - if (string.IsNullOrEmpty(joins)) + if (!hasJoins) { - ctx.RawSql = $"DELETE FROM {table}{where}"; + ctx.Write($"DELETE FROM {table}"); + VisitWheres(ctx, query); + return; } - else - { - // check if we have alias - if (fromClause is FromClause && !string.IsNullOrEmpty(fromClause.Alias)) - { - ctx.RawSql = $"DELETE {Wrap(fromClause.Alias)} FROM {table} {joins}{where}"; - } - else - { - ctx.RawSql = $"DELETE {table} FROM {table} {joins}{where}"; - } - } + var target = GetJoinTargetTable(fromClause, table); + ctx.Write($"DELETE {target} FROM {table}"); - return ctx; + VisitJoins(ctx, query); + VisitWheres(ctx, query); } - protected virtual SqlResult CompileUpdateQuery(Query query) + public virtual void VisitUpdateQuery(CompileContext ctx, Query query) { - var ctx = new SqlResult(parameterPlaceholder, EscapeCharacter) - { - Query = query - }; - - var fromClause = ctx.Query.FromClauses.ForEngineOrDefault(EngineCode); + var fromClause = query.FromClauses.ForEngineOrDefault(EngineCode); if (fromClause is null) { throw new InvalidOperationException("No table set to update"); @@ -398,10 +294,11 @@ protected virtual SqlResult CompileUpdateQuery(Query query) table = Wrap(fromClauseCast.Table); } + RawFromClause rawFrom = null; if (fromClause is RawFromClause rawFromClause) { + rawFrom = rawFromClause; table = WrapIdentifiers(rawFromClause.Expression); - ctx.Bindings.AddRange(rawFromClause.Bindings); } if (table is null) @@ -409,10 +306,15 @@ protected virtual SqlResult CompileUpdateQuery(Query query) throw new InvalidOperationException("Invalid table expression"); } - // check for increment statements - var clause = ctx.Query.UpdateClauses.ForEngineOrDefault(EngineCode); + var hasJoins = query.Joins.ForEngine(EngineCode).Any(); + var target = hasJoins ? GetJoinTargetTable(fromClause, table) : table; - string wheres; + if (!hasJoins && rawFrom != null) + { + ctx.Bindings.AddRange(rawFrom.Bindings); + } + + var clause = query.UpdateClauses.ForEngineOrDefault(EngineCode); if (clause != null && clause is IncrementClause increment) { @@ -420,19 +322,24 @@ protected virtual SqlResult CompileUpdateQuery(Query query) var value = Parameter(ctx, Math.Abs(increment.Value)); var sign = increment.Value >= 0 ? "+" : "-"; - wheres = CompileWheres(ctx); - - if (!string.IsNullOrEmpty(wheres)) + if (hasJoins) { - wheres = " " + wheres; + ctx.Write($"UPDATE {target} SET {column} = {column} {sign} {value} FROM {table}"); + if (rawFrom != null) + { + ctx.Bindings.AddRange(rawFrom.Bindings); + } + VisitJoins(ctx, query); + } + else + { + ctx.Write($"UPDATE {table} SET {column} = {column} {sign} {value}"); } - ctx.RawSql = $"UPDATE {table} SET {column} = {column} {sign} {value}{wheres}"; - - return ctx; + VisitWheres(ctx, query); + return; } - var toUpdate = clause as InsertClause; var parts = new List(); @@ -443,26 +350,63 @@ protected virtual SqlResult CompileUpdateQuery(Query query) var sets = string.Join(", ", parts); - wheres = CompileWheres(ctx); - - if (!string.IsNullOrEmpty(wheres)) + if (hasJoins) { - wheres = " " + wheres; + ctx.Write($"UPDATE {target} SET {sets} FROM {table}"); + if (rawFrom != null) + { + ctx.Bindings.AddRange(rawFrom.Bindings); + } + VisitJoins(ctx, query); + } + else + { + ctx.Write($"UPDATE {table} SET {sets}"); } - ctx.RawSql = $"UPDATE {table} SET {sets}{wheres}"; - - return ctx; + VisitWheres(ctx, query); } - protected virtual SqlResult CompileInsertQuery(Query query) + protected virtual string GetJoinTargetTable(AbstractFrom fromClause, string table) { - var ctx = new SqlResult(parameterPlaceholder, EscapeCharacter) + if (fromClause is FromClause from) { - Query = query - }; + var (_, alias) = SplitAlias(from.Table); + if (!string.IsNullOrEmpty(alias)) + { + return WrapValue(alias); + } - var fromClause = ctx.Query.FromClauses.ForEngineOrDefault(EngineCode); + var tableName = from.Table; + if (tableName.Contains('.')) + { + tableName = tableName.Substring(tableName.LastIndexOf('.') + 1); + } + + return WrapValue(tableName); + } + + if (!string.IsNullOrEmpty(fromClause.Alias)) + { + return WrapValue(fromClause.Alias); + } + + if (fromClause is RawFromClause raw) + { + var (_, alias) = SplitAlias(raw.Expression); + if (!string.IsNullOrEmpty(alias)) + { + var cleanAlias = alias.Trim().Trim('[', ']', '"', '`'); + return WrapValue(cleanAlias); + } + } + + return table; + } + + public virtual void VisitInsertQuery(CompileContext ctx, Query query) + { + var fromClause = query.FromClauses.ForEngineOrDefault(EngineCode); if (fromClause is null) throw new InvalidOperationException("No table set to insert"); @@ -478,471 +422,533 @@ protected virtual SqlResult CompileInsertQuery(Query query) if (table is null) throw new InvalidOperationException("Invalid table expression"); - var inserts = ctx.Query.InsertClauses.ForEngine(EngineCode).ToList(); + var inserts = query.InsertClauses.ForEngine(EngineCode).ToList(); if (inserts[0] is InsertQueryClause insertQueryClause) - return CompileInsertQueryClause(ctx, table, insertQueryClause); + VisitInsertQueryClause(ctx, table, insertQueryClause); else - return CompileValueInsertClauses(ctx, table, inserts.Cast()); + VisitValueInsertClauses(ctx, table, inserts.Cast()); } - protected virtual SqlResult CompileInsertQueryClause( - SqlResult ctx, string table, InsertQueryClause clause) + public virtual void VisitInsertQueryClause( + CompileContext ctx, string table, InsertQueryClause clause) { string columns = GetInsertColumnsList(clause.Columns); - var subCtx = CompileSubQuery(ctx, clause.Query); - ctx.Bindings.AddRange(subCtx.Bindings); - - ctx.RawSql = $"{SingleInsertStartClause} {table}{columns} {subCtx.RawSql}"; - - return ctx; + ctx.Write($"{SingleInsertStartClause} {table}{columns} "); + CompileSubQuery(ctx, clause.Query); } - protected virtual SqlResult CompileValueInsertClauses( - SqlResult ctx, string table, IEnumerable insertClauses) + public virtual void VisitValueInsertClauses( + CompileContext ctx, string table, IEnumerable insertClauses) { - bool isMultiValueInsert = insertClauses.Skip(1).Any(); + var list = insertClauses.ToList(); + bool isMultiValueInsert = list.Skip(1).Any(); - var insertInto = (isMultiValueInsert) ? MultiInsertStartClause : SingleInsertStartClause; + var insertInto = isMultiValueInsert ? MultiInsertStartClause : SingleInsertStartClause; - var firstInsert = insertClauses.First(); + var firstInsert = list.First(); string columns = GetInsertColumnsList(firstInsert.Columns); var values = string.Join(", ", Parameterize(ctx, firstInsert.Values)); - ctx.RawSql = $"{insertInto} {table}{columns} VALUES ({values})"; + ctx.Write($"{insertInto} {table}{columns} VALUES ({values})"); if (isMultiValueInsert) - return CompileRemainingInsertClauses(ctx, table, insertClauses); + { + VisitRemainingInsertClauses(ctx, table, list); + return; + } if (firstInsert.ReturnId && !string.IsNullOrEmpty(LastId)) - ctx.RawSql += ";" + LastId; - - return ctx; + { + ctx.Write(";" + LastId); + } } - protected virtual SqlResult CompileRemainingInsertClauses(SqlResult ctx, string table, IEnumerable inserts) + public virtual void VisitRemainingInsertClauses(CompileContext ctx, string table, IEnumerable inserts) { foreach (var insert in inserts.Skip(1)) { string values = string.Join(", ", Parameterize(ctx, insert.Values)); - ctx.RawSql += $", ({values})"; + ctx.Write($", ({values})"); } - return ctx; } - protected string GetInsertColumnsList(List columnList) - { - var columns = ""; - if (columnList.Any()) - columns = $" ({string.Join(", ", WrapArray(columnList))})"; - - return columns; - } - - protected virtual SqlResult CompileCteQuery(SqlResult ctx, Query query) + public virtual void VisitCteQuery(CompileContext ctx, Query query) { var cteFinder = new CteFinder(query, EngineCode); var cteSearchResult = cteFinder.Find(); - var rawSql = new StringBuilder("WITH "); - var cteBindings = new List(); + ctx.Write("WITH "); - foreach (var cte in cteSearchResult) + for (var i = 0; i < cteSearchResult.Count; i++) { - var cteCtx = CompileCte(cte); - - cteBindings.AddRange(cteCtx.Bindings); - rawSql.Append(cteCtx.RawSql.Trim()); - rawSql.Append(",\n"); - } - - rawSql.Length -= 2; // remove last comma - rawSql.Append('\n'); - rawSql.Append(ctx.RawSql); - - ctx.Bindings.InsertRange(0, cteBindings); - ctx.RawSql = rawSql.ToString(); - - return ctx; - } - - /// - /// Compile a single column clause - /// - /// - /// - /// - public virtual string CompileColumn(SqlResult ctx, AbstractColumn column) - { - if (column is RawColumn raw) - { - ctx.Bindings.AddRange(raw.Bindings); - return WrapIdentifiers(raw.Expression); - } - - if (column is QueryColumn queryColumn) - { - var alias = ""; - - if (!string.IsNullOrWhiteSpace(queryColumn.Query.QueryAlias)) - { - alias = $" {ColumnAsKeyword}{WrapValue(queryColumn.Query.QueryAlias)}"; - } - - var subCtx = CompileSubQuery(ctx, queryColumn.Query); - - ctx.Bindings.AddRange(subCtx.Bindings); - - return "(" + subCtx.RawSql + $"){alias}"; - } - - if (column is AggregatedColumn aggregatedColumn) - { - string agg = aggregatedColumn.Aggregate.ToUpperInvariant(); - - var (col, alias) = SplitAlias(CompileColumn(ctx, aggregatedColumn.Column)); - - alias = string.IsNullOrEmpty(alias) ? "" : $" {ColumnAsKeyword}{alias}"; - - string filterCondition = CompileFilterConditions(ctx, aggregatedColumn); - - if (string.IsNullOrEmpty(filterCondition)) - { - return $"{agg}({col}){alias}"; - } - - if (SupportsFilterClause) + if (i > 0) { - return $"{agg}({col}) FILTER (WHERE {filterCondition}){alias}"; + ctx.Write(",\n"); } - - return $"{agg}(CASE WHEN {filterCondition} THEN {col} END){alias}"; - } - - return Wrap((column as Column).Name); - - } - - protected virtual string CompileFilterConditions(SqlResult ctx, AggregatedColumn aggregatedColumn) - { - if (aggregatedColumn.Filter == null) - { - return null; + VisitCte(ctx, cteSearchResult[i]); } - var wheres = aggregatedColumn.Filter.Wheres.Clauses - .ForEngine(EngineCode) - .Cast() - .ToList(); - - return CompileConditions(ctx, wheres); + ctx.Write("\n"); } - public virtual SqlResult CompileCte(AbstractFrom cte) + public virtual void VisitCte(CompileContext ctx, AbstractFrom cte) { - var ctx = new SqlResult(parameterPlaceholder, EscapeCharacter); - - if (null == cte) - { - return ctx; - } + if (cte == null) return; if (cte is RawFromClause raw) { ctx.Bindings.AddRange(raw.Bindings); - ctx.RawSql = $"{WrapValue(raw.Alias)} AS ({WrapIdentifiers(raw.Expression)})"; + ctx.Write($"{WrapValue(raw.Alias)} AS ({WrapIdentifiers(raw.Expression)})"); } else if (cte is QueryFromClause queryFromClause) { - var subCtx = CompileSubQuery(ctx, queryFromClause.Query); - ctx.Bindings.AddRange(subCtx.Bindings); - - ctx.RawSql = $"{WrapValue(queryFromClause.Alias)} AS ({subCtx.RawSql})"; + ctx.Write($"{WrapValue(queryFromClause.Alias)} AS ("); + CompileSubQuery(ctx, queryFromClause.Query); + ctx.Write(")"); } else if (cte is AdHocTableFromClause adHoc) { - var subCtx = CompileAdHocQuery(adHoc); - ctx.Bindings.AddRange(subCtx.Bindings); - - ctx.RawSql = $"{WrapValue(adHoc.Alias)} AS ({subCtx.RawSql})"; + ctx.Write($"{WrapValue(adHoc.Alias)} AS ("); + VisitAdHocTableFromClause(ctx, adHoc); + ctx.Write(")"); } - - return ctx; } - protected virtual SqlResult OnBeforeSelect(SqlResult ctx) + // ========================================== + // Visitor Methods: Select Clauses & Elements + // ========================================== + + public virtual void VisitColumns(CompileContext ctx, Query query) { - return ctx; + VisitSelectPrefix(ctx, query); + VisitColumnExpressions(ctx, query); } - protected virtual string CompileColumns(SqlResult ctx) + public virtual void VisitSelectPrefix(CompileContext ctx, Query query) { - var aggregate = ctx.Query.AggregateClauses.ForEngineOrDefault(EngineCode); + var aggregate = query.AggregateClauses.ForEngineOrDefault(EngineCode); if (aggregate != null) { - var aggregateColumns = aggregate.Columns - .Select(x => CompileColumn(ctx, new Column { Name = x })) - .ToList(); + ctx.Write("SELECT "); + return; + } - string sql = string.Empty; + var distinct = query.IsDistinct ? "DISTINCT " : ""; + ctx.Write($"SELECT {distinct}"); + } - if (aggregateColumns.Count == 1) + public virtual void VisitColumnExpressions(CompileContext ctx, Query query) + { + var aggregate = query.AggregateClauses.ForEngineOrDefault(EngineCode); + if (aggregate != null) + { + if (aggregate.Columns.Count == 1) { - sql = string.Join(", ", aggregateColumns); - - if (ctx.Query.IsDistinct) + ctx.Write(aggregate.Type.ToUpperInvariant()).Write("("); + if (query.IsDistinct) { - sql = "DISTINCT " + sql; + ctx.Write("DISTINCT "); } - - return "SELECT " + aggregate.Type.ToUpperInvariant() + "(" + sql + $") {ColumnAsKeyword}" + WrapValue(aggregate.Type); + VisitColumn(ctx, new Column { Name = aggregate.Columns[0] }); + ctx.Write($") {ColumnAsKeyword}").Write(WrapValue(aggregate.Type)); + return; } - return "SELECT 1"; + ctx.Write("1"); + return; } - var columns = ctx.Query.SelectClauses - .ForEngine(EngineCode) - .Select(x => CompileColumn(ctx, x)) - .ToList(); + var selectClauses = query.SelectClauses.ForEngine(EngineCode).ToList(); + if (!selectClauses.Any()) + { + ctx.Write("*"); + return; + } - var distinct = ctx.Query.IsDistinct ? "DISTINCT " : ""; + for (var i = 0; i < selectClauses.Count; i++) + { + if (i > 0) ctx.Write(", "); + VisitColumn(ctx, selectClauses[i]); + } + } - var select = columns.Any() ? string.Join(", ", columns) : "*"; + public virtual void VisitColumn(CompileContext ctx, AbstractColumn column) + { + switch (column) + { + case RawColumn raw: + VisitRawColumn(ctx, raw); + break; + case QueryColumn queryColumn: + VisitQueryColumn(ctx, queryColumn); + break; + case AggregatedColumn aggregatedColumn: + VisitAggregatedColumn(ctx, aggregatedColumn); + break; + case Column col: + VisitBasicColumn(ctx, col); + break; + default: + throw InvalidClauseException("Column", column); + } + } - return $"SELECT {distinct}{select}"; + public virtual void VisitBasicColumn(CompileContext ctx, Column column) + { + ctx.Write(Wrap(column.Name)); + } + public virtual void VisitRawColumn(CompileContext ctx, RawColumn raw) + { + ctx.Bindings.AddRange(raw.Bindings); + ctx.Write(WrapIdentifiers(raw.Expression)); } - public virtual string CompileUnion(SqlResult ctx) + public virtual void VisitQueryColumn(CompileContext ctx, QueryColumn queryColumn) { + ctx.Write("("); + CompileSubQuery(ctx, queryColumn.Query); + ctx.Write(")"); - // Handle UNION, EXCEPT and INTERSECT - var clauses = ctx.Query.CombineClauses.ForEngine(EngineCode).ToList(); - if (!clauses.Any()) + if (!string.IsNullOrWhiteSpace(queryColumn.Query.QueryAlias)) { - return null; + ctx.Write($" {ColumnAsKeyword}").Write(WrapValue(queryColumn.Query.QueryAlias)); } + } - var combinedQueries = new List(); - - foreach (var clause in clauses) - { - if (clause is Combine combineClause) - { - var combineOperator = combineClause.Operation.ToUpperInvariant() + " " + (combineClause.All ? "ALL " : ""); - - var subCtx = CompileSubQuery(ctx, combineClause.Query); + public virtual void VisitAggregatedColumn(CompileContext ctx, AggregatedColumn aggregatedColumn) + { + string agg = aggregatedColumn.Aggregate.ToUpperInvariant(); - ctx.Bindings.AddRange(subCtx.Bindings); + var tempCtx = new CompileContext(ctx); + VisitColumn(tempCtx, aggregatedColumn.Column); + ctx.Bindings.AddRange(tempCtx.Bindings); - combinedQueries.Add($"{combineOperator}{subCtx.RawSql}"); - } - else - { - var combineRawClause = clause as RawCombine; + var (col, alias) = SplitAlias(tempCtx.Buffer.ToString()); + alias = string.IsNullOrEmpty(alias) ? "" : $" {ColumnAsKeyword}{alias}"; - ctx.Bindings.AddRange(combineRawClause.Bindings); + var filterCtx = new CompileContext(ctx); + VisitFilterConditions(filterCtx, aggregatedColumn); - combinedQueries.Add(WrapIdentifiers(combineRawClause.Expression)); - - } + if (filterCtx.Buffer.Length == 0) + { + ctx.Write($"{agg}({col}){alias}"); + return; } - return string.Join(" ", combinedQueries); + ctx.Bindings.AddRange(filterCtx.Bindings); + if (SupportsFilterClause) + { + ctx.Write($"{agg}({col}) FILTER (WHERE {filterCtx.Buffer}){alias}"); + } + else + { + ctx.Write($"{agg}(CASE WHEN {filterCtx.Buffer} THEN {col} END){alias}"); + } } - public virtual string CompileTableExpression(SqlResult ctx, AbstractFrom from) + public virtual void VisitFilterConditions(CompileContext ctx, AggregatedColumn aggregatedColumn) { - if (from is RawFromClause raw) + if (aggregatedColumn.Filter == null) { - ctx.Bindings.AddRange(raw.Bindings); - return WrapIdentifiers(raw.Expression); + return; } - if (from is QueryFromClause queryFromClause) - { - var fromQuery = queryFromClause.Query; - - var alias = string.IsNullOrEmpty(fromQuery.QueryAlias) ? "" : $" {TableAsKeyword}" + WrapValue(fromQuery.QueryAlias); - - var subCtx = CompileSubQuery(ctx, fromQuery); + var wheres = aggregatedColumn.Filter.Wheres.Clauses + .ForEngine(EngineCode) + .Cast() + .ToList(); - ctx.Bindings.AddRange(subCtx.Bindings); + VisitConditions(ctx, wheres); + } - return "(" + subCtx.RawSql + ")" + alias; + public virtual void VisitFrom(CompileContext ctx, Query query) + { + var from = query.FromClauses.ForEngineOrDefault(EngineCode); + if (from != null) + { + ctx.EnsureSpace().Write("FROM "); + VisitTableExpression(ctx, from); } + } - if (from is FromClause fromClause) - { - return Wrap(fromClause.Table); + public virtual void VisitTableExpression(CompileContext ctx, AbstractFrom from) + { + switch (from) + { + case RawFromClause raw: + VisitRawFromClause(ctx, raw); + break; + case QueryFromClause queryFromClause: + VisitQueryFromClause(ctx, queryFromClause); + break; + case FromClause fromClause: + VisitFromClause(ctx, fromClause); + break; + case AdHocTableFromClause adHoc: + VisitAdHocTableFromClause(ctx, adHoc); + break; + default: + throw InvalidClauseException("TableExpression", from); } + } - throw InvalidClauseException("TableExpression", from); + public virtual void VisitFromClause(CompileContext ctx, FromClause fromClause) + { + ctx.Write(Wrap(fromClause.Table)); } - public virtual string CompileFrom(SqlResult ctx) + public virtual void VisitRawFromClause(CompileContext ctx, RawFromClause raw) { - var from = ctx.Query.FromClauses.ForEngineOrDefault(EngineCode); - if (from != null) - { - return "FROM " + CompileTableExpression(ctx, from); - } + ctx.Bindings.AddRange(raw.Bindings); + ctx.Write(WrapIdentifiers(raw.Expression)); + } - return string.Empty; + public virtual void VisitQueryFromClause(CompileContext ctx, QueryFromClause queryFromClause) + { + var fromQuery = queryFromClause.Query; + var alias = string.IsNullOrEmpty(fromQuery.QueryAlias) ? "" : $" {TableAsKeyword}" + WrapValue(fromQuery.QueryAlias); + ctx.Write("("); + CompileSubQuery(ctx, fromQuery); + ctx.Write(")").Write(alias); } - public virtual string CompileJoins(SqlResult ctx) + public virtual void VisitAdHocTableFromClause(CompileContext ctx, AdHocTableFromClause adHoc) { - var joins = ctx.Query.Joins - .ForEngine(EngineCode) - .Select(x => CompileJoin(ctx, x.Join)) - .ToList(); + var row = "SELECT " + string.Join(", ", adHoc.Columns.Select(col => $"{parameterPlaceholder} AS {Wrap(col)}")); - if (!joins.Any()) + var fromTable = SingleRowDummyTableName; + if (fromTable != null) { - return null; + row += $" FROM {fromTable}"; } - return "\n" + string.Join("\n", joins); + var rows = string.Join(" UNION ALL ", Enumerable.Repeat(row, adHoc.Values.Count / adHoc.Columns.Count)); + ctx.Bindings.AddRange(adHoc.Values); + ctx.Write(rows); } - public virtual string CompileJoin(SqlResult ctx, Join join, bool isNested = false) + public virtual void VisitJoins(CompileContext ctx, Query query) { + var joins = query.Joins.ForEngine(EngineCode).ToList(); + if (!joins.Any()) return; + + ctx.EnsureSpace(); + for (var i = 0; i < joins.Count; i++) + { + if (i > 0) + { + ctx.Write("\n"); + } + else if (ctx.Buffer.Length > 0 && ctx.Buffer[ctx.Buffer.Length - 1] == ' ') + { + ctx.Write("\n"); + } + VisitJoin(ctx, joins[i].Join); + } + } + public virtual void VisitJoin(CompileContext ctx, Join join, bool isNested = false) + { var from = join.FromClause; var conditions = join.Conditions.GetConditions(EngineCode); - var joinTable = CompileTableExpression(ctx, from); - var constraints = CompileConditions(ctx, conditions); - - var onClause = conditions.Any() ? $" ON {constraints}" : ""; + ctx.Write(join.Type).Write(" "); + VisitTableExpression(ctx, from); - return $"{join.Type} {joinTable}{onClause}"; + if (conditions.Any()) + { + ctx.Write(" ON "); + VisitConditions(ctx, conditions); + } } - public virtual string CompileWheres(SqlResult ctx) + public virtual void VisitWheres(CompileContext ctx, Query query) => + VisitConditionsClause(ctx, "WHERE", query.Wheres); + + public virtual void VisitHaving(CompileContext ctx, Query query) => + VisitConditionsClause(ctx, "HAVING", query.Havings); + + protected virtual void VisitConditionsClause(CompileContext ctx, string keyword, ConditionGroup group) { - if (!ctx.Query.Wheres.HasConditions(EngineCode)) + if (!group.HasConditions(EngineCode)) { - return null; + return; } - var conditions = ctx.Query.Wheres.Clauses + var before = ctx.Buffer.Length; + ctx.EnsureSpace(); + ctx.Write(keyword).Write(" "); + var beforeConds = ctx.Buffer.Length; + + var conditions = group.Clauses .ForEngine(EngineCode) .Cast() .ToList(); - var sql = CompileConditions(ctx, conditions).Trim(); - return string.IsNullOrEmpty(sql) ? null : $"WHERE {sql}"; + VisitConditions(ctx, conditions); + + if (ctx.Buffer.Length == beforeConds) + { + ctx.Buffer.Length = before; + } } - public virtual string CompileGroups(SqlResult ctx) + public virtual void VisitGroups(CompileContext ctx, Query query) { - var columns = ctx.Query.GroupByClauses - .ForEngine(EngineCode) - .Select(x => CompileColumn(ctx, x)) - .ToList(); - + var columns = query.GroupByClauses.ForEngine(EngineCode).ToList(); if (!columns.Any()) { - return null; + return; } - return "GROUP BY " + string.Join(", ", columns); + ctx.EnsureSpace().Write("GROUP BY "); + for (var i = 0; i < columns.Count; i++) + { + if (i > 0) ctx.Write(", "); + VisitColumn(ctx, columns[i]); + } } - public virtual string CompileOrders(SqlResult ctx) + public virtual void VisitOrders(CompileContext ctx, Query query) { - var columns = ctx.Query.OrderByClauses - .ForEngine(EngineCode) - .Select(x => + var columns = query.OrderByClauses.ForEngine(EngineCode).ToList(); + if (!columns.Any()) { + return; + } + ctx.EnsureSpace().Write("ORDER BY "); + for (var i = 0; i < columns.Count; i++) + { + if (i > 0) ctx.Write(", "); + var x = columns[i]; if (x is RawOrderBy raw) { ctx.Bindings.AddRange(raw.Bindings); - return WrapIdentifiers(raw.Expression); + ctx.Write(WrapIdentifiers(raw.Expression)); + } + else if (x is OrderBy orderBy) + { + var direction = orderBy.Ascending ? "" : " DESC"; + ctx.Write(Wrap(orderBy.Column)).Write(direction); + } + else if (x is OrderByRandom random) + { + VisitOrderByRandom(ctx, random); } + } + } - var direction = (x as OrderBy).Ascending ? "" : " DESC"; + public virtual void VisitOrderByRandom(CompileContext ctx, OrderByRandom random) + { + ctx.Write(CompileRandom(random.Seed)); + } - return Wrap((x as OrderBy).Column) + direction; - }).ToList(); + public virtual void VisitLimit(CompileContext ctx, Query query) + { + var limit = query.GetLimit(EngineCode); + var offset = query.GetOffset(EngineCode); - if (!columns.Any()) + if (limit == 0 && offset == 0) { - return null; + return; } - return "ORDER BY " + string.Join(", ", columns); - } + ctx.EnsureSpace(); - public virtual string CompileHaving(SqlResult ctx) - { - if (!ctx.Query.Havings.HasConditions(EngineCode)) + if (offset == 0) { - return null; + ctx.Bindings.Add(limit); + ctx.Write($"LIMIT {parameterPlaceholder}"); + return; } - var sql = new List(); - string boolOperator; + if (limit == 0) + { + ctx.Bindings.Add(offset); + ctx.Write($"OFFSET {parameterPlaceholder}"); + return; + } - var having = ctx.Query.Havings.Clauses - .ForEngine(EngineCode) - .Cast() - .ToList(); + ctx.Bindings.Add(limit); + ctx.Bindings.Add(offset); + ctx.Write($"LIMIT {parameterPlaceholder} OFFSET {parameterPlaceholder}"); + } - for (var i = 0; i < having.Count; i++) + public virtual void VisitUnions(CompileContext ctx, Query query) + { + var clauses = query.CombineClauses.ForEngine(EngineCode).ToList(); + if (!clauses.Any()) { - var compiled = CompileCondition(ctx, having[i]); + return; + } - if (!string.IsNullOrEmpty(compiled)) + foreach (var clause in clauses) + { + ctx.EnsureSpace(); + if (clause is Combine combineClause) { - boolOperator = i > 0 ? having[i].IsOr ? "OR " : "AND " : ""; - - sql.Add(boolOperator + compiled); + var combineOperator = combineClause.Operation.ToUpperInvariant() + " " + (combineClause.All ? "ALL " : ""); + ctx.Write(combineOperator); + CompileSubQuery(ctx, combineClause.Query); + } + else if (clause is RawCombine rawCombine) + { + ctx.Bindings.AddRange(rawCombine.Bindings); + ctx.Write(WrapIdentifiers(rawCombine.Expression)); } } - - return $"HAVING {string.Join(" ", sql)}"; } - public virtual string CompileLimit(SqlResult ctx) - { - var limit = ctx.Query.GetLimit(EngineCode); - var offset = ctx.Query.GetOffset(EngineCode); + // ========================================== + // Backward Compatibility Wrappers + // ========================================== - if (limit == 0 && offset == 0) - { - return null; - } + protected virtual CompileContext OnBeforeSelect(CompileContext ctx) => ctx; - if (offset == 0) + public virtual string CompileLimit(CompileContext ctx) + { + var prevLen = ctx.Buffer.Length; + VisitLimit(ctx, ctx.Query); + if (ctx.Buffer.Length > prevLen) { - ctx.Bindings.Add(limit); - return $"LIMIT {parameterPlaceholder}"; + var result = ctx.Buffer.ToString(prevLen, ctx.Buffer.Length - prevLen).Trim(); + ctx.Buffer.Length = prevLen; + return result; } + return null; + } - if (limit == 0) + public virtual string CompileOrders(CompileContext ctx) + { + var prevLen = ctx.Buffer.Length; + VisitOrders(ctx, ctx.Query); + if (ctx.Buffer.Length > prevLen) { - ctx.Bindings.Add(offset); - return $"OFFSET {parameterPlaceholder}"; + var result = ctx.Buffer.ToString(prevLen, ctx.Buffer.Length - prevLen).Trim(); + ctx.Buffer.Length = prevLen; + return result; } + return null; + } - ctx.Bindings.Add(limit); - ctx.Bindings.Add(offset); + // ========================================== + // Helper / Formatting Methods + // ========================================== + + protected string GetInsertColumnsList(List columnList) + { + var columns = ""; + if (columnList.Any()) + columns = $" ({string.Join(", ", WrapArray(columnList))})"; - return $"LIMIT {parameterPlaceholder} OFFSET {parameterPlaceholder}"; + return columns; } - /// - /// Compile the random statement into SQL. - /// - /// - /// - public virtual string CompileRandom(string seed) + public virtual string CompileRandom(int? seed) { return "RANDOM()"; } @@ -986,14 +992,8 @@ protected string checkOperator(string op) return op; } - /// - /// Wrap a single string in a column identifier. - /// - /// - /// public virtual string Wrap(string value) { - if (value.ToLowerInvariant().Contains(" as ")) { var (before, after) = SplitAlias(value); @@ -1009,8 +1009,6 @@ public virtual string Wrap(string value) })); } - // If we reach here then the value does not contain an "AS" alias - // nor dot "." expression, so wrap it as regular value. return WrapValue(value); } @@ -1028,11 +1026,6 @@ public virtual (string, string) SplitAlias(string value) return (value, null); } - /// - /// Wrap a single string in keyword identifiers. - /// - /// - /// public virtual string WrapValue(string value) { if (value == "*") return value; @@ -1045,49 +1038,32 @@ public virtual string WrapValue(string value) return opening + value.Replace(closing, closing + closing) + closing; } - /// - /// Resolve a parameter - /// - /// - /// - /// - public virtual object Resolve(SqlResult ctx, object parameter) + public virtual object Resolve(CompileContext ctx, object parameter) { - // if we face a literal value we have to return it directly if (parameter is UnsafeLiteral literal) { return literal.Value; } - // if we face a variable we have to lookup the variable from the predefined variables if (parameter is Variable variable) { - var value = FindVariable(variable.Name); + var value = ctx.FindVariable(variable.Name); return value; } return parameter; - } - /// - /// Resolve a parameter and add it to the binding list - /// - /// - /// - /// - public virtual string Parameter(SqlResult ctx, object parameter) + public virtual string Parameter(CompileContext ctx, object parameter) { - // if we face a literal value we have to return it directly if (parameter is UnsafeLiteral literal) { return literal.Value; } - // if we face a variable we have to lookup the variable from the predefined variables if (parameter is Variable variable) { - var value = FindVariable(variable.Name); + var value = ctx.FindVariable(variable.Name); ctx.Bindings.Add(value); return parameterPlaceholder; } @@ -1096,22 +1072,11 @@ public virtual string Parameter(SqlResult ctx, object parameter) return parameterPlaceholder; } - /// - /// Create query parameter place-holders for an array. - /// - /// - /// - /// - public virtual string Parameterize(SqlResult ctx, IEnumerable values) + public virtual string Parameterize(CompileContext ctx, IEnumerable values) { return string.Join(", ", values.Select(x => Parameter(ctx, x))); } - /// - /// Wrap an array of values. - /// - /// - /// public virtual List WrapArray(List values) { return values.Select(x => Wrap(x)).ToList(); @@ -1120,11 +1085,8 @@ public virtual List WrapArray(List values) public virtual string WrapIdentifiers(string input) { return input - - // deprecated .ReplaceIdentifierUnlessEscaped(this.EscapeCharacter, "{", this.OpeningIdentifier) .ReplaceIdentifierUnlessEscaped(this.EscapeCharacter, "}", this.ClosingIdentifier) - .ReplaceIdentifierUnlessEscaped(this.EscapeCharacter, "[", this.OpeningIdentifier) .ReplaceIdentifierUnlessEscaped(this.EscapeCharacter, "]", this.ClosingIdentifier); } diff --git a/QueryBuilder/Compilers/FirebirdCompiler.cs b/QueryBuilder/Compilers/FirebirdCompiler.cs index 74637c63..3a61844a 100644 --- a/QueryBuilder/Compilers/FirebirdCompiler.cs +++ b/QueryBuilder/Compilers/FirebirdCompiler.cs @@ -14,63 +14,84 @@ public FirebirdCompiler() public override string EngineCode { get; } = EngineCodes.Firebird; protected override string SingleRowDummyTableName => "RDB$DATABASE"; - protected override SqlResult CompileInsertQuery(Query query) + public override void VisitUpdateQuery(CompileContext ctx, Query query) { - var ctx = base.CompileInsertQuery(query); + if (query.Joins.ForEngine(EngineCode).Any()) + { + throw new NotSupportedException("Firebird does not support UPDATE with JOIN clauses"); + } + + base.VisitUpdateQuery(ctx, query); + } - var inserts = ctx.Query.InsertClauses.ForEngine(EngineCode).ToList(); + public override void VisitDeleteQuery(CompileContext ctx, Query query) + { + if (query.Joins.ForEngine(EngineCode).Any()) + { + throw new NotSupportedException("Firebird does not support DELETE with JOIN clauses"); + } + + base.VisitDeleteQuery(ctx, query); + } + + public override void VisitInsertQuery(CompileContext ctx, Query query) + { + var before = ctx.Buffer.Length; + base.VisitInsertQuery(ctx, query); + var sql = ctx.Buffer.ToString(before, ctx.Buffer.Length - before); + ctx.Buffer.Length = before; + + var inserts = query.InsertClauses.ForEngine(EngineCode).ToList(); if (inserts.Count > 1) { - ctx.RawSql = Regex.Replace(ctx.RawSql, @"\)\s+VALUES\s+\(", ") SELECT "); - ctx.RawSql = Regex.Replace(ctx.RawSql, @"\),\s*\(", " FROM RDB$DATABASE UNION ALL SELECT "); - ctx.RawSql = Regex.Replace(ctx.RawSql, @"\)$", " FROM RDB$DATABASE"); + sql = Regex.Replace(sql, @"\)\s+VALUES\s+\(", ") SELECT "); + sql = Regex.Replace(sql, @"\),\s*\(", " FROM RDB$DATABASE UNION ALL SELECT "); + sql = Regex.Replace(sql, @"\)$", " FROM RDB$DATABASE"); } - return ctx; + ctx.Write(sql); } - public override string CompileLimit(SqlResult ctx) + public override void VisitLimit(CompileContext ctx, Query query) { - var limit = ctx.Query.GetLimit(EngineCode); - var offset = ctx.Query.GetOffset(EngineCode); + var limit = query.GetLimit(EngineCode); + var offset = query.GetOffset(EngineCode); if (limit > 0 && offset > 0) { + ctx.EnsureSpace(); ctx.Bindings.Add(offset + 1); ctx.Bindings.Add(limit + offset); - return $"ROWS {parameterPlaceholder} TO {parameterPlaceholder}"; + ctx.Write($"ROWS {parameterPlaceholder} TO {parameterPlaceholder}"); } - - return null; } - - protected override string CompileColumns(SqlResult ctx) + public override void VisitSelectPrefix(CompileContext ctx, Query query) { - var compiled = base.CompileColumns(ctx); - - var limit = ctx.Query.GetLimit(EngineCode); - var offset = ctx.Query.GetOffset(EngineCode); + var limit = query.GetLimit(EngineCode); + var offset = query.GetOffset(EngineCode); + var distinct = query.IsDistinct ? "DISTINCT " : ""; if (limit > 0 && offset == 0) { - ctx.Bindings.Insert(0, limit); - - return string.Concat($"SELECT FIRST {parameterPlaceholder}", compiled.AsSpan(6)); + ctx.Bindings.Add(limit); + ctx.Write($"SELECT FIRST {parameterPlaceholder} {distinct}"); + return; } - else if (limit == 0 && offset > 0) - { - ctx.Bindings.Insert(0, offset); - return string.Concat($"SELECT SKIP {parameterPlaceholder}", compiled.AsSpan(6)); + if (limit == 0 && offset > 0) + { + ctx.Bindings.Add(offset); + ctx.Write($"SELECT SKIP {parameterPlaceholder} {distinct}"); + return; } - return compiled; + base.VisitSelectPrefix(ctx, query); } - protected override string CompileBasicDateCondition(SqlResult ctx, BasicDateCondition condition) + protected override void VisitBasicDateCondition(CompileContext ctx, BasicDateCondition condition) { var column = Wrap(condition.Column); @@ -89,14 +110,17 @@ protected override string CompileBasicDateCondition(SqlResult ctx, BasicDateCond left = $"EXTRACT({condition.Part.ToUpperInvariant()} FROM {column})"; } - var sql = $"{left} {condition.Operator} {Parameter(ctx, condition.Value)}"; - if (condition.IsNot) { - return $"NOT ({sql})"; + ctx.Write("NOT ("); } - return sql; + ctx.Write(left).Write(" ").Write(condition.Operator).Write(" ").Write(Parameter(ctx, condition.Value)); + + if (condition.IsNot) + { + ctx.Write(")"); + } } public override string WrapValue(string value) @@ -113,5 +137,10 @@ public override string CompileFalse() { return "0"; } + + public override string CompileRandom(int? seed) + { + return "RAND()"; + } } } diff --git a/QueryBuilder/Compilers/MySqlCompiler.cs b/QueryBuilder/Compilers/MySqlCompiler.cs index 4729125b..b243c24f 100644 --- a/QueryBuilder/Compilers/MySqlCompiler.cs +++ b/QueryBuilder/Compilers/MySqlCompiler.cs @@ -1,3 +1,7 @@ +using System; +using System.Collections.Generic; +using System.Linq; + namespace SqlKata.Compilers { public class MySqlCompiler : Compiler @@ -10,40 +14,106 @@ public MySqlCompiler() public override string EngineCode { get; } = EngineCodes.MySql; - public override string CompileLimit(SqlResult ctx) + public override void VisitUpdateQuery(CompileContext ctx, Query query) { - var limit = ctx.Query.GetLimit(EngineCode); - var offset = ctx.Query.GetOffset(EngineCode); + var hasJoins = query.Joins.ForEngine(EngineCode).Any(); + if (!hasJoins) + { + base.VisitUpdateQuery(ctx, query); + return; + } + + var fromClause = query.FromClauses.ForEngineOrDefault(EngineCode); + + if (fromClause is null) + { + throw new InvalidOperationException("No table set to update"); + } + + string table = null; + + if (fromClause is FromClause fromClauseCast) + { + table = Wrap(fromClauseCast.Table); + } + + if (fromClause is RawFromClause rawFromClause) + { + table = WrapIdentifiers(rawFromClause.Expression); + ctx.Bindings.AddRange(rawFromClause.Bindings); + } + + if (table is null) + { + throw new InvalidOperationException("Invalid table expression"); + } + + var clause = query.UpdateClauses.ForEngineOrDefault(EngineCode); + + if (clause != null && clause is IncrementClause increment) + { + ctx.Write($"UPDATE {table}"); + VisitJoins(ctx, query); + var column = Wrap(increment.Column); + var value = Parameter(ctx, Math.Abs(increment.Value)); + var sign = increment.Value >= 0 ? "+" : "-"; + + ctx.Write($" SET {column} = {column} {sign} {value}"); + VisitWheres(ctx, query); + return; + } + + ctx.Write($"UPDATE {table}"); + VisitJoins(ctx, query); + + var toUpdate = clause as InsertClause; + var parts = new List(); + for (var i = 0; i < toUpdate.Columns.Count; i++) + { + parts.Add(Wrap(toUpdate.Columns[i]) + " = " + Parameter(ctx, toUpdate.Values[i])); + } + + var sets = string.Join(", ", parts); + + ctx.Write($" SET {sets}"); + VisitWheres(ctx, query); + } + + public override void VisitLimit(CompileContext ctx, Query query) + { + var limit = query.GetLimit(EngineCode); + var offset = query.GetOffset(EngineCode); if (offset == 0 && limit == 0) { - return null; + return; } + ctx.EnsureSpace(); + if (offset == 0) { ctx.Bindings.Add(limit); - return $"LIMIT {parameterPlaceholder}"; + ctx.Write($"LIMIT {parameterPlaceholder}"); + return; } if (limit == 0) { - - // MySql will not accept offset without limit, so we will put a large number - // to avoid this error. - ctx.Bindings.Add(offset); - return $"LIMIT 18446744073709551615 OFFSET {parameterPlaceholder}"; + ctx.Write($"LIMIT 18446744073709551615 OFFSET {parameterPlaceholder}"); + return; } - // We have both values - ctx.Bindings.Add(limit); ctx.Bindings.Add(offset); + ctx.Write($"LIMIT {parameterPlaceholder} OFFSET {parameterPlaceholder}"); + } - return $"LIMIT {parameterPlaceholder} OFFSET {parameterPlaceholder}"; - + public override string CompileRandom(int? seed) + { + return seed.HasValue ? $"RAND({seed})" : "RAND()"; } } } diff --git a/QueryBuilder/Compilers/OracleCompiler.cs b/QueryBuilder/Compilers/OracleCompiler.cs index 8bca3edf..7b4863d7 100644 --- a/QueryBuilder/Compilers/OracleCompiler.cs +++ b/QueryBuilder/Compilers/OracleCompiler.cs @@ -1,8 +1,6 @@ using System; using System.Collections.Generic; -using System.Diagnostics; using System.Linq; -using System.Text.RegularExpressions; namespace SqlKata.Compilers { @@ -20,89 +18,115 @@ public OracleCompiler() public bool UseLegacyPagination { get; set; } = false; protected override string SingleRowDummyTableName => "DUAL"; - protected override SqlResult CompileSelectQuery(Query query) + public override void VisitUpdateQuery(CompileContext ctx, Query query) { - if (!UseLegacyPagination) + if (query.Joins.ForEngine(EngineCode).Any()) + { + throw new NotSupportedException("Oracle does not support UPDATE with JOIN clauses"); + } + + base.VisitUpdateQuery(ctx, query); + } + + public override void VisitDeleteQuery(CompileContext ctx, Query query) + { + if (query.Joins.ForEngine(EngineCode).Any()) { - return base.CompileSelectQuery(query); + throw new NotSupportedException("Oracle does not support DELETE with JOIN clauses"); } - var result = base.CompileSelectQuery(query); + base.VisitDeleteQuery(ctx, query); + } + + public override void VisitSelectQuery(CompileContext ctx, Query query) + { + if (!UseLegacyPagination) + { + base.VisitSelectQuery(ctx, query); + return; + } - ApplyLegacyLimit(result); + var beforeInner = ctx.Buffer.Length; + base.VisitSelectQuery(ctx, query); + var innerSql = ctx.Buffer.ToString(beforeInner, ctx.Buffer.Length - beforeInner); + ctx.Buffer.Length = beforeInner; - return result; + ctx.Write(ApplyLegacyLimit(ctx, innerSql)); } - public override string CompileLimit(SqlResult ctx) + public override void VisitLimit(CompileContext ctx, Query query) { if (UseLegacyPagination) { - // in pre-12c versions of Oracle, limit is handled by ROWNUM techniques - return null; + return; } - var limit = ctx.Query.GetLimit(EngineCode); - var offset = ctx.Query.GetOffset(EngineCode); + var limit = query.GetLimit(EngineCode); + var offset = query.GetOffset(EngineCode); if (limit == 0 && offset == 0) { - return null; + return; } - var safeOrder = ""; + ctx.EnsureSpace(); - if (!ctx.Query.OrderByClauses.Any()) + if (!query.OrderByClauses.Any()) { - safeOrder = "ORDER BY (SELECT 0 FROM DUAL) "; + ctx.Write("ORDER BY (SELECT 0 FROM DUAL) "); } if (limit == 0) { ctx.Bindings.Add(offset); - return $"{safeOrder}OFFSET {parameterPlaceholder} ROWS"; + ctx.Write($"OFFSET {parameterPlaceholder} ROWS"); + return; } ctx.Bindings.Add(offset); ctx.Bindings.Add(limit); - return $"{safeOrder}OFFSET {parameterPlaceholder} ROWS FETCH NEXT {parameterPlaceholder} ROWS ONLY"; + ctx.Write($"OFFSET {parameterPlaceholder} ROWS FETCH NEXT {parameterPlaceholder} ROWS ONLY"); + } + + internal void ApplyLegacyLimit(CompileContext ctx) + { + ctx.RawSql = ApplyLegacyLimit(ctx, ctx.RawSql); } - internal void ApplyLegacyLimit(SqlResult ctx) + internal string ApplyLegacyLimit(CompileContext ctx, string sql) { var limit = ctx.Query.GetLimit(EngineCode); var offset = ctx.Query.GetOffset(EngineCode); if (limit == 0 && offset == 0) { - return; + return sql; } string newSql; if (limit == 0) { - newSql = $"SELECT * FROM (SELECT \"results_wrapper\".*, ROWNUM \"row_num\" FROM ({ctx.RawSql}) \"results_wrapper\") WHERE \"row_num\" > {parameterPlaceholder}"; + newSql = $"SELECT * FROM (SELECT \"results_wrapper\".*, ROWNUM \"row_num\" FROM ({sql}) \"results_wrapper\") WHERE \"row_num\" > {parameterPlaceholder}"; ctx.Bindings.Add(offset); } else if (offset == 0) { - newSql = $"SELECT * FROM ({ctx.RawSql}) WHERE ROWNUM <= {parameterPlaceholder}"; + newSql = $"SELECT * FROM ({sql}) WHERE ROWNUM <= {parameterPlaceholder}"; ctx.Bindings.Add(limit); } else { - newSql = $"SELECT * FROM (SELECT \"results_wrapper\".*, ROWNUM \"row_num\" FROM ({ctx.RawSql}) \"results_wrapper\" WHERE ROWNUM <= {parameterPlaceholder}) WHERE \"row_num\" > {parameterPlaceholder}"; + newSql = $"SELECT * FROM (SELECT \"results_wrapper\".*, ROWNUM \"row_num\" FROM ({sql}) \"results_wrapper\" WHERE ROWNUM <= {parameterPlaceholder}) WHERE \"row_num\" > {parameterPlaceholder}"; ctx.Bindings.Add(limit + offset); ctx.Bindings.Add(offset); } - ctx.RawSql = newSql; + return newSql; } - protected override string CompileBasicDateCondition(SqlResult ctx, BasicDateCondition condition) + protected override void VisitBasicDateCondition(CompileContext ctx, BasicDateCondition condition) { - var column = Wrap(condition.Column); var value = Parameter(ctx, condition.Value); @@ -125,10 +149,9 @@ protected override string CompileBasicDateCondition(SqlResult ctx, BasicDateCond valueFormat = $"{value}"; else { - // assume HH:MM format if (condition.Value.ToString().Split(':').Count() == 2) valueFormat = $"TO_DATE({value}, 'HH24:MI')"; - else // assume HH:MM:SS format + else valueFormat = $"TO_DATE({value}, 'HH24:MI:SS')"; } sql = $"TO_CHAR({column}, 'HH24:MI:SS') {condition.Operator} TO_CHAR({valueFormat}, 'HH24:MI:SS')"; @@ -148,15 +171,19 @@ protected override string CompileBasicDateCondition(SqlResult ctx, BasicDateCond if (condition.IsNot) { - return $"NOT ({sql})"; + ctx.Write("NOT ("); } - return sql; + ctx.Write(sql); + if (condition.IsNot) + { + ctx.Write(")"); + } } - protected override SqlResult CompileRemainingInsertClauses( - SqlResult ctx, string table, IEnumerable inserts) + public override void VisitRemainingInsertClauses( + CompileContext ctx, string table, IEnumerable inserts) { foreach (var insert in inserts.Skip(1)) { @@ -164,13 +191,15 @@ protected override SqlResult CompileRemainingInsertClauses( string values = string.Join(", ", Parameterize(ctx, insert.Values)); string intoFormat = " INTO {0}{1} VALUES ({2})"; - var nextInsert = string.Format(intoFormat, table, columns, values); - - ctx.RawSql += nextInsert; + ctx.Write(string.Format(intoFormat, table, columns, values)); } - ctx.RawSql += " SELECT 1 FROM DUAL"; - return ctx; + ctx.Write(" SELECT 1 FROM DUAL"); + } + + public override string CompileRandom(int? seed) + { + return "DBMS_RANDOM.VALUE"; } } } diff --git a/QueryBuilder/Compilers/PostgresCompiler.cs b/QueryBuilder/Compilers/PostgresCompiler.cs index 3b45d0e6..d30be455 100644 --- a/QueryBuilder/Compilers/PostgresCompiler.cs +++ b/QueryBuilder/Compilers/PostgresCompiler.cs @@ -13,61 +13,27 @@ public PostgresCompiler() public override string EngineCode { get; } = EngineCodes.PostgreSql; public override bool SupportsFilterClause { get; set; } = true; - - protected override string CompileBasicStringCondition(SqlResult ctx, BasicStringCondition x) + public override void VisitUpdateQuery(CompileContext ctx, Query query) { - - var column = Wrap(x.Column); - - var value = Resolve(ctx, x.Value) as string; - - if (value == null) - { - throw new ArgumentException("Expecting a non nullable string"); - } - - var method = x.Operator; - - if (new[] { "starts", "ends", "contains", "like", "ilike" }.Contains(x.Operator)) + if (query.Joins.ForEngine(EngineCode).Any()) { - method = x.CaseSensitive ? "LIKE" : "ILIKE"; - - switch (x.Operator) - { - case "starts": - value = $"{value}%"; - break; - case "ends": - value = $"%{value}"; - break; - case "contains": - value = $"%{value}%"; - break; - } + throw new NotSupportedException("PostgreSql does not support UPDATE with JOIN clauses"); } - string sql; - - if (x.Value is UnsafeLiteral) - { - sql = $"{column} {checkOperator(method)} {value}"; - } - else - { - sql = $"{column} {checkOperator(method)} {Parameter(ctx, value)}"; - } + base.VisitUpdateQuery(ctx, query); + } - if (!string.IsNullOrEmpty(x.EscapeCharacter)) + public override void VisitDeleteQuery(CompileContext ctx, Query query) + { + if (query.Joins.ForEngine(EngineCode).Any()) { - sql = $"{sql} ESCAPE '{x.EscapeCharacter}'"; + throw new NotSupportedException("PostgreSql does not support DELETE with JOIN clauses"); } - return x.IsNot ? $"NOT ({sql})" : sql; - + base.VisitDeleteQuery(ctx, query); } - - protected override string CompileBasicDateCondition(SqlResult ctx, BasicDateCondition condition) + protected override void VisitBasicDateCondition(CompileContext ctx, BasicDateCondition condition) { var column = Wrap(condition.Column); @@ -86,14 +52,17 @@ protected override string CompileBasicDateCondition(SqlResult ctx, BasicDateCond left = $"DATE_PART('{condition.Part.ToUpperInvariant()}', {column})"; } - var sql = $"{left} {condition.Operator} {Parameter(ctx, condition.Value)}"; - if (condition.IsNot) { - return $"NOT ({sql})"; + ctx.Write("NOT ("); } - return sql; + ctx.Write(left).Write(" ").Write(condition.Operator).Write(" ").Write(Parameter(ctx, condition.Value)); + + if (condition.IsNot) + { + ctx.Write(")"); + } } } } diff --git a/QueryBuilder/Compilers/SqlServerCompiler.cs b/QueryBuilder/Compilers/SqlServerCompiler.cs index d48141e3..f45ddd37 100644 --- a/QueryBuilder/Compilers/SqlServerCompiler.cs +++ b/QueryBuilder/Compilers/SqlServerCompiler.cs @@ -14,121 +14,123 @@ public SqlServerCompiler() public override string EngineCode { get; } = EngineCodes.SqlServer; public bool UseLegacyPagination { get; set; } = false; - protected override SqlResult CompileSelectQuery(Query query) + public override void VisitSelectQuery(CompileContext ctx, Query query) { if (!UseLegacyPagination || !query.HasOffset(EngineCode)) { - return base.CompileSelectQuery(query); + base.VisitSelectQuery(ctx, query); + return; } query = query.Clone(); - var ctx = new SqlResult(parameterPlaceholder, EscapeCharacter) - { - Query = query, - }; - var limit = query.GetLimit(EngineCode); var offset = query.GetOffset(EngineCode); - if (!query.SelectClauses.Any()) { query.Select("*"); } - var order = CompileOrders(ctx) ?? "ORDER BY (SELECT 0)"; + ctx.PushQuery(query); + var beforeOrder = ctx.Buffer.Length; + var beforeOrderBindings = ctx.Bindings.Count; + VisitOrders(ctx, query); + var order = ctx.Buffer.Length > beforeOrder ? ctx.Buffer.ToString(beforeOrder, ctx.Buffer.Length - beforeOrder).TrimStart() : "ORDER BY (SELECT 0)"; + var orderBindings = ctx.Bindings.Skip(beforeOrderBindings).ToArray(); + ctx.Buffer.Length = beforeOrder; + ctx.Bindings.RemoveRange(beforeOrderBindings, orderBindings.Length); + ctx.PopQuery(); - query.SelectRaw($"ROW_NUMBER() OVER ({order}) AS [row_num]", ctx.Bindings.ToArray()); + query.SelectRaw($"ROW_NUMBER() OVER ({order}) AS [row_num]", orderBindings); query.OrderByClauses.Clear(); + ctx.PushQuery(query); + var beforeInner = ctx.Buffer.Length; + try + { + base.VisitSelectQuery(ctx, query); + } + finally + { + ctx.PopQuery(); + } - var result = base.CompileSelectQuery(query); + var innerSql = ctx.Buffer.ToString(beforeInner, ctx.Buffer.Length - beforeInner); + ctx.Buffer.Length = beforeInner; if (limit == 0) { - result.RawSql = $"SELECT * FROM ({result.RawSql}) AS [results_wrapper] WHERE [row_num] >= {parameterPlaceholder}"; - result.Bindings.Add(offset + 1); - } - else - { - result.RawSql = $"SELECT * FROM ({result.RawSql}) AS [results_wrapper] WHERE [row_num] BETWEEN {parameterPlaceholder} AND {parameterPlaceholder}"; - result.Bindings.Add(offset + 1); - result.Bindings.Add(limit + offset); + ctx.Bindings.Add(offset + 1); + ctx.Write($"SELECT * FROM ({innerSql}) AS [results_wrapper] WHERE [row_num] >= {parameterPlaceholder}"); + return; } - return result; + ctx.Bindings.Add(offset + 1); + ctx.Bindings.Add(limit + offset); + ctx.Write($"SELECT * FROM ({innerSql}) AS [results_wrapper] WHERE [row_num] BETWEEN {parameterPlaceholder} AND {parameterPlaceholder}"); } - protected override string CompileColumns(SqlResult ctx) + public override void VisitSelectPrefix(CompileContext ctx, Query query) { - var compiled = base.CompileColumns(ctx); - if (!UseLegacyPagination) { - return compiled; + base.VisitSelectPrefix(ctx, query); + return; } - // If there is a limit on the query, but not an offset, we will add the top - // clause to the query, which serves as a "limit" type clause within the - // SQL Server system similar to the limit keywords available in MySQL. - var limit = ctx.Query.GetLimit(EngineCode); - var offset = ctx.Query.GetOffset(EngineCode); + var limit = query.GetLimit(EngineCode); + var offset = query.GetOffset(EngineCode); if (limit > 0 && offset == 0) { - // top bindings should be inserted first - ctx.Bindings.Insert(0, limit); - - // handle distinct - if (compiled.StartsWith("SELECT DISTINCT")) - { - return $"SELECT DISTINCT TOP ({parameterPlaceholder}){compiled.Substring(15)}"; - } + ctx.Bindings.Add(limit); - return $"SELECT TOP ({parameterPlaceholder}){compiled.Substring(6)}"; + var distinct = query.IsDistinct ? "DISTINCT " : ""; + ctx.Write($"SELECT {distinct}TOP ({parameterPlaceholder}) "); + return; } - return compiled; + base.VisitSelectPrefix(ctx, query); } - public override string CompileLimit(SqlResult ctx) + public override void VisitLimit(CompileContext ctx, Query query) { if (UseLegacyPagination) { - // in legacy versions of Sql Server, limit is handled by TOP - // and ROW_NUMBER techniques - return null; + return; } - var limit = ctx.Query.GetLimit(EngineCode); - var offset = ctx.Query.GetOffset(EngineCode); + var limit = query.GetLimit(EngineCode); + var offset = query.GetOffset(EngineCode); if (limit == 0 && offset == 0) { - return null; + return; } - var safeOrder = ""; - if (!ctx.Query.OrderByClauses.Any()) + ctx.EnsureSpace(); + + if (!query.OrderByClauses.Any()) { - safeOrder = "ORDER BY (SELECT 0) "; + ctx.Write("ORDER BY (SELECT 0) "); } if (limit == 0) { ctx.Bindings.Add(offset); - return $"{safeOrder}OFFSET {parameterPlaceholder} ROWS"; + ctx.Write($"OFFSET {parameterPlaceholder} ROWS"); + return; } ctx.Bindings.Add(offset); ctx.Bindings.Add(limit); - return $"{safeOrder}OFFSET {parameterPlaceholder} ROWS FETCH NEXT {parameterPlaceholder} ROWS ONLY"; + ctx.Write($"OFFSET {parameterPlaceholder} ROWS FETCH NEXT {parameterPlaceholder} ROWS ONLY"); } - public override string CompileRandom(string seed) + public override string CompileRandom(int? seed) { return "NEWID()"; } @@ -143,7 +145,7 @@ public override string CompileFalse() return "cast(0 as bit)"; } - protected override string CompileBasicDateCondition(SqlResult ctx, BasicDateCondition condition) + protected override void VisitBasicDateCondition(CompileContext ctx, BasicDateCondition condition) { var column = Wrap(condition.Column); var part = condition.Part.ToUpperInvariant(); @@ -159,30 +161,28 @@ protected override string CompileBasicDateCondition(SqlResult ctx, BasicDateCond left = $"DATEPART({part.ToUpperInvariant()}, {column})"; } - var sql = $"{left} {condition.Operator} {Parameter(ctx, condition.Value)}"; - if (condition.IsNot) { - return $"NOT ({sql})"; + ctx.Write("NOT ("); } - return sql; + ctx.Write(left).Write(" ").Write(condition.Operator).Write(" ").Write(Parameter(ctx, condition.Value)); + + if (condition.IsNot) + { + ctx.Write(")"); + } } - protected override SqlResult CompileAdHocQuery(AdHocTableFromClause adHoc) + public override void VisitAdHocTableFromClause(CompileContext ctx, AdHocTableFromClause adHoc) { - var ctx = new SqlResult(parameterPlaceholder, EscapeCharacter); - var colNames = string.Join(", ", adHoc.Columns.Select(Wrap)); var valueRow = string.Join(", ", Enumerable.Repeat(parameterPlaceholder, adHoc.Columns.Count)); var valueRows = string.Join(", ", Enumerable.Repeat($"({valueRow})", adHoc.Values.Count / adHoc.Columns.Count)); - var sql = $"SELECT {colNames} FROM (VALUES {valueRows}) AS tbl ({colNames})"; - - ctx.RawSql = sql; - ctx.Bindings = adHoc.Values; - return ctx; + ctx.Write($"SELECT {colNames} FROM (VALUES {valueRows}) AS tbl ({colNames})"); + ctx.Bindings.AddRange(adHoc.Values); } } } diff --git a/QueryBuilder/Compilers/SqliteCompiler.cs b/QueryBuilder/Compilers/SqliteCompiler.cs index 1401dd35..47e2ce85 100644 --- a/QueryBuilder/Compilers/SqliteCompiler.cs +++ b/QueryBuilder/Compilers/SqliteCompiler.cs @@ -1,4 +1,6 @@ +using System; using System.Collections.Generic; +using System.Linq; namespace SqlKata.Compilers { @@ -10,6 +12,26 @@ public class SqliteCompiler : Compiler protected override string LastId { get; set; } = "select last_insert_rowid() as id"; public override bool SupportsFilterClause { get; set; } = true; + public override void VisitUpdateQuery(CompileContext ctx, Query query) + { + if (query.Joins.ForEngine(EngineCode).Any()) + { + throw new NotSupportedException("Sqlite does not support UPDATE with JOIN clauses"); + } + + base.VisitUpdateQuery(ctx, query); + } + + public override void VisitDeleteQuery(CompileContext ctx, Query query) + { + if (query.Joins.ForEngine(EngineCode).Any()) + { + throw new NotSupportedException("Sqlite does not support DELETE with JOIN clauses"); + } + + base.VisitDeleteQuery(ctx, query); + } + public override string CompileTrue() { return "1"; @@ -20,21 +42,23 @@ public override string CompileFalse() return "0"; } - public override string CompileLimit(SqlResult ctx) + public override void VisitLimit(CompileContext ctx, Query query) { - var limit = ctx.Query.GetLimit(EngineCode); - var offset = ctx.Query.GetOffset(EngineCode); + var limit = query.GetLimit(EngineCode); + var offset = query.GetOffset(EngineCode); if (limit == 0 && offset > 0) { + ctx.EnsureSpace(); ctx.Bindings.Add(offset); - return $"LIMIT -1 OFFSET {parameterPlaceholder}"; + ctx.Write($"LIMIT -1 OFFSET {parameterPlaceholder}"); + return; } - return base.CompileLimit(ctx); + base.VisitLimit(ctx, query); } - protected override string CompileBasicDateCondition(SqlResult ctx, BasicDateCondition condition) + protected override void VisitBasicDateCondition(CompileContext ctx, BasicDateCondition condition) { var column = Wrap(condition.Column); var value = Parameter(ctx, condition.Value); @@ -51,17 +75,31 @@ protected override string CompileBasicDateCondition(SqlResult ctx, BasicDateCond if (!formatMap.ContainsKey(condition.Part)) { - return $"{column} {condition.Operator} {value}"; + if (condition.IsNot) + { + ctx.Write("NOT ("); + } + + ctx.Write(column).Write(" ").Write(condition.Operator).Write(" ").Write(value); + + if (condition.IsNot) + { + ctx.Write(")"); + } + + return; } var sql = $"strftime('{formatMap[condition.Part]}', {column}) {condition.Operator} cast({value} as text)"; if (condition.IsNot) { - return $"NOT ({sql})"; + ctx.Write($"NOT ({sql})"); + } + else + { + ctx.Write(sql); } - - return sql; } } } diff --git a/QueryBuilder/ConditionGroup.cs b/QueryBuilder/ConditionGroup.cs index d2b39e3f..626b706d 100644 --- a/QueryBuilder/ConditionGroup.cs +++ b/QueryBuilder/ConditionGroup.cs @@ -34,7 +34,32 @@ public virtual ConditionGroup Clone() public bool HasConditions(string engineCode = null) { engineCode ??= EngineScope; - return Clauses.Any(x => engineCode == null || x.Engine == null || engineCode == x.Engine); + + return Clauses.Any(x => + { + if (engineCode != null && x.Engine != null && engineCode != x.Engine) + { + return false; + } + + if (x is NestedCondition nested) + { + if (nested.Conditions is ConditionGroup group) + { + return group.HasConditions(engineCode); + } + + if (nested.Conditions != null) + { + var component = nested.Component ?? "where"; + return nested.Conditions.HasComponent(component, engineCode); + } + + return false; + } + + return true; + }); } public List GetConditions(string engineCode = null) diff --git a/QueryBuilder/Extensions/ConditionExtensions.cs b/QueryBuilder/Extensions/ConditionExtensions.cs index 3e1b45d1..e86b4c27 100644 --- a/QueryBuilder/Extensions/ConditionExtensions.cs +++ b/QueryBuilder/Extensions/ConditionExtensions.cs @@ -259,14 +259,13 @@ public static T OrWhereFalse(this T target, string column) where T : IConditi 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 + public static T WhereLike(this T target, string column, object value, 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(), @@ -275,29 +274,28 @@ public static T WhereLike(this T target, string column, object value, bool ca return target; } - public static T WhereNotLike(this T target, string column, object value, bool caseSensitive = false, string escapeCharacter = null) where T : IConditional + public static T WhereNotLike(this T target, string column, object value, string escapeCharacter = null) where T : IConditional { - return target.Not().WhereLike(column, value, caseSensitive, escapeCharacter); + return target.Not().WhereLike(column, value, escapeCharacter); } - public static T OrWhereLike(this T target, string column, object value, bool caseSensitive = false, string escapeCharacter = null) where T : IConditional + public static T OrWhereLike(this T target, string column, object value, string escapeCharacter = null) where T : IConditional { - return target.Or().WhereLike(column, value, caseSensitive, escapeCharacter); + return target.Or().WhereLike(column, value, escapeCharacter); } - public static T OrWhereNotLike(this T target, string column, object value, bool caseSensitive = false, string escapeCharacter = null) where T : IConditional + public static T OrWhereNotLike(this T target, string column, object value, string escapeCharacter = null) where T : IConditional { - return target.Or().Not().WhereLike(column, value, caseSensitive, escapeCharacter); + return target.Or().Not().WhereLike(column, value, escapeCharacter); } - public static T WhereStarts(this T target, string column, object value, bool caseSensitive = false, string escapeCharacter = null) where T : IConditional + public static T WhereStarts(this T target, string column, object value, 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(), @@ -306,29 +304,28 @@ public static T WhereStarts(this T target, string column, object value, bool return target; } - public static T WhereNotStarts(this T target, string column, object value, bool caseSensitive = false, string escapeCharacter = null) where T : IConditional + public static T WhereNotStarts(this T target, string column, object value, string escapeCharacter = null) where T : IConditional { - return target.Not().WhereStarts(column, value, caseSensitive, escapeCharacter); + return target.Not().WhereStarts(column, value, escapeCharacter); } - public static T OrWhereStarts(this T target, string column, object value, bool caseSensitive = false, string escapeCharacter = null) where T : IConditional + public static T OrWhereStarts(this T target, string column, object value, string escapeCharacter = null) where T : IConditional { - return target.Or().WhereStarts(column, value, caseSensitive, escapeCharacter); + return target.Or().WhereStarts(column, value, escapeCharacter); } - public static T OrWhereNotStarts(this T target, string column, object value, bool caseSensitive = false, string escapeCharacter = null) where T : IConditional + public static T OrWhereNotStarts(this T target, string column, object value, string escapeCharacter = null) where T : IConditional { - return target.Or().Not().WhereStarts(column, value, caseSensitive, escapeCharacter); + return target.Or().Not().WhereStarts(column, value, escapeCharacter); } - public static T WhereEnds(this T target, string column, object value, bool caseSensitive = false, string escapeCharacter = null) where T : IConditional + public static T WhereEnds(this T target, string column, object value, 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(), @@ -337,29 +334,28 @@ public static T WhereEnds(this T target, string column, object value, bool ca return target; } - public static T WhereNotEnds(this T target, string column, object value, bool caseSensitive = false, string escapeCharacter = null) where T : IConditional + public static T WhereNotEnds(this T target, string column, object value, string escapeCharacter = null) where T : IConditional { - return target.Not().WhereEnds(column, value, caseSensitive, escapeCharacter); + return target.Not().WhereEnds(column, value, escapeCharacter); } - public static T OrWhereEnds(this T target, string column, object value, bool caseSensitive = false, string escapeCharacter = null) where T : IConditional + public static T OrWhereEnds(this T target, string column, object value, string escapeCharacter = null) where T : IConditional { - return target.Or().WhereEnds(column, value, caseSensitive, escapeCharacter); + return target.Or().WhereEnds(column, value, escapeCharacter); } - public static T OrWhereNotEnds(this T target, string column, object value, bool caseSensitive = false, string escapeCharacter = null) where T : IConditional + public static T OrWhereNotEnds(this T target, string column, object value, string escapeCharacter = null) where T : IConditional { - return target.Or().Not().WhereEnds(column, value, caseSensitive, escapeCharacter); + return target.Or().Not().WhereEnds(column, value, escapeCharacter); } - public static T WhereContains(this T target, string column, object value, bool caseSensitive = false, string escapeCharacter = null) where T : IConditional + public static T WhereContains(this T target, string column, object value, 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(), @@ -368,19 +364,19 @@ public static T WhereContains(this T target, string column, object value, boo return target; } - public static T WhereNotContains(this T target, string column, object value, bool caseSensitive = false, string escapeCharacter = null) where T : IConditional + public static T WhereNotContains(this T target, string column, object value, string escapeCharacter = null) where T : IConditional { - return target.Not().WhereContains(column, value, caseSensitive, escapeCharacter); + return target.Not().WhereContains(column, value, escapeCharacter); } - public static T OrWhereContains(this T target, string column, object value, bool caseSensitive = false, string escapeCharacter = null) where T : IConditional + public static T OrWhereContains(this T target, string column, object value, string escapeCharacter = null) where T : IConditional { - return target.Or().WhereContains(column, value, caseSensitive, escapeCharacter); + return target.Or().WhereContains(column, value, escapeCharacter); } - public static T OrWhereNotContains(this T target, string column, object value, bool caseSensitive = false, string escapeCharacter = null) where T : IConditional + public static T OrWhereNotContains(this T target, string column, object value, string escapeCharacter = null) where T : IConditional { - return target.Or().Not().WhereContains(column, value, caseSensitive, escapeCharacter); + return target.Or().Not().WhereContains(column, value, escapeCharacter); } public static T WhereBetween(this T target, string column, TVal lower, TVal higher) where T : IConditional diff --git a/QueryBuilder/Query.Having.cs b/QueryBuilder/Query.Having.cs index 27c27b78..dc8f6839 100644 --- a/QueryBuilder/Query.Having.cs +++ b/QueryBuilder/Query.Having.cs @@ -190,99 +190,99 @@ public Query OrHavingFalse(string column) return this; } - public Query HavingLike(string column, object value, bool caseSensitive = false, string escapeCharacter = null) + public Query HavingLike(string column, object value, string escapeCharacter = null) { - Havings.WhereLike(column, value, caseSensitive, escapeCharacter); + Havings.WhereLike(column, value, escapeCharacter); return this; } - public Query HavingNotLike(string column, object value, bool caseSensitive = false, string escapeCharacter = null) + public Query HavingNotLike(string column, object value, string escapeCharacter = null) { - Havings.WhereNotLike(column, value, caseSensitive, escapeCharacter); + Havings.WhereNotLike(column, value, escapeCharacter); return this; } - public Query OrHavingLike(string column, object value, bool caseSensitive = false, string escapeCharacter = null) + public Query OrHavingLike(string column, object value, string escapeCharacter = null) { - Havings.OrWhereLike(column, value, caseSensitive, escapeCharacter); + Havings.OrWhereLike(column, value, escapeCharacter); return this; } - public Query OrHavingNotLike(string column, object value, bool caseSensitive = false, string escapeCharacter = null) + public Query OrHavingNotLike(string column, object value, string escapeCharacter = null) { - Havings.OrWhereNotLike(column, value, caseSensitive, escapeCharacter); + Havings.OrWhereNotLike(column, value, escapeCharacter); return this; } - public Query HavingStarts(string column, object value, bool caseSensitive = false, string escapeCharacter = null) + public Query HavingStarts(string column, object value, string escapeCharacter = null) { - Havings.WhereStarts(column, value, caseSensitive, escapeCharacter); + Havings.WhereStarts(column, value, escapeCharacter); return this; } - public Query HavingNotStarts(string column, object value, bool caseSensitive = false, string escapeCharacter = null) + public Query HavingNotStarts(string column, object value, string escapeCharacter = null) { - Havings.WhereNotStarts(column, value, caseSensitive, escapeCharacter); + Havings.WhereNotStarts(column, value, escapeCharacter); return this; } - public Query OrHavingStarts(string column, object value, bool caseSensitive = false, string escapeCharacter = null) + public Query OrHavingStarts(string column, object value, string escapeCharacter = null) { - Havings.OrWhereStarts(column, value, caseSensitive, escapeCharacter); + Havings.OrWhereStarts(column, value, escapeCharacter); return this; } - public Query OrHavingNotStarts(string column, object value, bool caseSensitive = false, string escapeCharacter = null) + public Query OrHavingNotStarts(string column, object value, string escapeCharacter = null) { - Havings.OrWhereNotStarts(column, value, caseSensitive, escapeCharacter); + Havings.OrWhereNotStarts(column, value, escapeCharacter); return this; } - public Query HavingEnds(string column, object value, bool caseSensitive = false, string escapeCharacter = null) + public Query HavingEnds(string column, object value, string escapeCharacter = null) { - Havings.WhereEnds(column, value, caseSensitive, escapeCharacter); + Havings.WhereEnds(column, value, escapeCharacter); return this; } - public Query HavingNotEnds(string column, object value, bool caseSensitive = false, string escapeCharacter = null) + public Query HavingNotEnds(string column, object value, string escapeCharacter = null) { - Havings.WhereNotEnds(column, value, caseSensitive, escapeCharacter); + Havings.WhereNotEnds(column, value, escapeCharacter); return this; } - public Query OrHavingEnds(string column, object value, bool caseSensitive = false, string escapeCharacter = null) + public Query OrHavingEnds(string column, object value, string escapeCharacter = null) { - Havings.OrWhereEnds(column, value, caseSensitive, escapeCharacter); + Havings.OrWhereEnds(column, value, escapeCharacter); return this; } - public Query OrHavingNotEnds(string column, object value, bool caseSensitive = false, string escapeCharacter = null) + public Query OrHavingNotEnds(string column, object value, string escapeCharacter = null) { - Havings.OrWhereNotEnds(column, value, caseSensitive, escapeCharacter); + Havings.OrWhereNotEnds(column, value, escapeCharacter); return this; } - public Query HavingContains(string column, object value, bool caseSensitive = false, string escapeCharacter = null) + public Query HavingContains(string column, object value, string escapeCharacter = null) { - Havings.WhereContains(column, value, caseSensitive, escapeCharacter); + Havings.WhereContains(column, value, escapeCharacter); return this; } - public Query HavingNotContains(string column, object value, bool caseSensitive = false, string escapeCharacter = null) + public Query HavingNotContains(string column, object value, string escapeCharacter = null) { - Havings.WhereNotContains(column, value, caseSensitive, escapeCharacter); + Havings.WhereNotContains(column, value, escapeCharacter); return this; } - public Query OrHavingContains(string column, object value, bool caseSensitive = false, string escapeCharacter = null) + public Query OrHavingContains(string column, object value, string escapeCharacter = null) { - Havings.OrWhereContains(column, value, caseSensitive, escapeCharacter); + Havings.OrWhereContains(column, value, escapeCharacter); return this; } - public Query OrHavingNotContains(string column, object value, bool caseSensitive = false, string escapeCharacter = null) + public Query OrHavingNotContains(string column, object value, string escapeCharacter = null) { - Havings.OrWhereNotContains(column, value, caseSensitive, escapeCharacter); + Havings.OrWhereNotContains(column, value, escapeCharacter); return this; } diff --git a/QueryBuilder/Query.Where.cs b/QueryBuilder/Query.Where.cs index ba24e7f9..314ee86c 100644 --- a/QueryBuilder/Query.Where.cs +++ b/QueryBuilder/Query.Where.cs @@ -190,99 +190,99 @@ public Query OrWhereFalse(string column) return this; } - public Query WhereLike(string column, object value, bool caseSensitive = false, string escapeCharacter = null) + public Query WhereLike(string column, object value, string escapeCharacter = null) { - Wheres.WhereLike(column, value, caseSensitive, escapeCharacter); + Wheres.WhereLike(column, value, escapeCharacter); return this; } - public Query WhereNotLike(string column, object value, bool caseSensitive = false, string escapeCharacter = null) + public Query WhereNotLike(string column, object value, string escapeCharacter = null) { - Wheres.WhereNotLike(column, value, caseSensitive, escapeCharacter); + Wheres.WhereNotLike(column, value, escapeCharacter); return this; } - public Query OrWhereLike(string column, object value, bool caseSensitive = false, string escapeCharacter = null) + public Query OrWhereLike(string column, object value, string escapeCharacter = null) { - Wheres.OrWhereLike(column, value, caseSensitive, escapeCharacter); + Wheres.OrWhereLike(column, value, escapeCharacter); return this; } - public Query OrWhereNotLike(string column, object value, bool caseSensitive = false, string escapeCharacter = null) + public Query OrWhereNotLike(string column, object value, string escapeCharacter = null) { - Wheres.OrWhereNotLike(column, value, caseSensitive, escapeCharacter); + Wheres.OrWhereNotLike(column, value, escapeCharacter); return this; } - public Query WhereStarts(string column, object value, bool caseSensitive = false, string escapeCharacter = null) + public Query WhereStarts(string column, object value, string escapeCharacter = null) { - Wheres.WhereStarts(column, value, caseSensitive, escapeCharacter); + Wheres.WhereStarts(column, value, escapeCharacter); return this; } - public Query WhereNotStarts(string column, object value, bool caseSensitive = false, string escapeCharacter = null) + public Query WhereNotStarts(string column, object value, string escapeCharacter = null) { - Wheres.WhereNotStarts(column, value, caseSensitive, escapeCharacter); + Wheres.WhereNotStarts(column, value, escapeCharacter); return this; } - public Query OrWhereStarts(string column, object value, bool caseSensitive = false, string escapeCharacter = null) + public Query OrWhereStarts(string column, object value, string escapeCharacter = null) { - Wheres.OrWhereStarts(column, value, caseSensitive, escapeCharacter); + Wheres.OrWhereStarts(column, value, escapeCharacter); return this; } - public Query OrWhereNotStarts(string column, object value, bool caseSensitive = false, string escapeCharacter = null) + public Query OrWhereNotStarts(string column, object value, string escapeCharacter = null) { - Wheres.OrWhereNotStarts(column, value, caseSensitive, escapeCharacter); + Wheres.OrWhereNotStarts(column, value, escapeCharacter); return this; } - public Query WhereEnds(string column, object value, bool caseSensitive = false, string escapeCharacter = null) + public Query WhereEnds(string column, object value, string escapeCharacter = null) { - Wheres.WhereEnds(column, value, caseSensitive, escapeCharacter); + Wheres.WhereEnds(column, value, escapeCharacter); return this; } - public Query WhereNotEnds(string column, object value, bool caseSensitive = false, string escapeCharacter = null) + public Query WhereNotEnds(string column, object value, string escapeCharacter = null) { - Wheres.WhereNotEnds(column, value, caseSensitive, escapeCharacter); + Wheres.WhereNotEnds(column, value, escapeCharacter); return this; } - public Query OrWhereEnds(string column, object value, bool caseSensitive = false, string escapeCharacter = null) + public Query OrWhereEnds(string column, object value, string escapeCharacter = null) { - Wheres.OrWhereEnds(column, value, caseSensitive, escapeCharacter); + Wheres.OrWhereEnds(column, value, escapeCharacter); return this; } - public Query OrWhereNotEnds(string column, object value, bool caseSensitive = false, string escapeCharacter = null) + public Query OrWhereNotEnds(string column, object value, string escapeCharacter = null) { - Wheres.OrWhereNotEnds(column, value, caseSensitive, escapeCharacter); + Wheres.OrWhereNotEnds(column, value, escapeCharacter); return this; } - public Query WhereContains(string column, object value, bool caseSensitive = false, string escapeCharacter = null) + public Query WhereContains(string column, object value, string escapeCharacter = null) { - Wheres.WhereContains(column, value, caseSensitive, escapeCharacter); + Wheres.WhereContains(column, value, escapeCharacter); return this; } - public Query WhereNotContains(string column, object value, bool caseSensitive = false, string escapeCharacter = null) + public Query WhereNotContains(string column, object value, string escapeCharacter = null) { - Wheres.WhereNotContains(column, value, caseSensitive, escapeCharacter); + Wheres.WhereNotContains(column, value, escapeCharacter); return this; } - public Query OrWhereContains(string column, object value, bool caseSensitive = false, string escapeCharacter = null) + public Query OrWhereContains(string column, object value, string escapeCharacter = null) { - Wheres.OrWhereContains(column, value, caseSensitive, escapeCharacter); + Wheres.OrWhereContains(column, value, escapeCharacter); return this; } - public Query OrWhereNotContains(string column, object value, bool caseSensitive = false, string escapeCharacter = null) + public Query OrWhereNotContains(string column, object value, string escapeCharacter = null) { - Wheres.OrWhereNotContains(column, value, caseSensitive, escapeCharacter); + Wheres.OrWhereNotContains(column, value, escapeCharacter); return this; } diff --git a/QueryBuilder/Query.cs b/QueryBuilder/Query.cs index d1ff0671..cbbd7228 100755 --- a/QueryBuilder/Query.cs +++ b/QueryBuilder/Query.cs @@ -425,11 +425,17 @@ public Query OrderByRaw(string expression, params object[] bindings) return this; } - public Query OrderByRandom(string seed) + public Query OrderByRandom() + { + return OrderByRandom(null); + } + + public Query OrderByRandom(int? seed) { OrderByClauses.Add(new OrderByRandom { - Engine = EngineScope + Engine = EngineScope, + Seed = seed, }); return this; } diff --git a/QueryBuilder/SqlResult.cs b/QueryBuilder/SqlResult.cs index b84baf8a..099424db 100644 --- a/QueryBuilder/SqlResult.cs +++ b/QueryBuilder/SqlResult.cs @@ -8,8 +8,11 @@ namespace SqlKata { public class SqlResult { - private string ParameterPlaceholder { get; set; } - private string EscapeCharacter { get; set; } + public string ParameterPlaceholder { get; set; } = "?"; + public string EscapeCharacter { get; set; } = "\\"; + + public SqlResult() { } + public SqlResult(string parameterPlaceholder, string escapeCharacter) { ParameterPlaceholder = parameterPlaceholder;