From f732e0516a8e3d9144e62c3c245d8e2a5c04f7df Mon Sep 17 00:00:00 2001 From: Michel Bieleveld Date: Wed, 9 Apr 2025 12:21:46 -0300 Subject: [PATCH 1/4] Initial try --- QueryBuilder.Tests/DefineTest.cs | 60 +++++++++++++++++++++++++++++- QueryBuilder/Compilers/Compiler.cs | 42 +++++++++++++++++++-- QueryBuilder/NamedParameter.cs | 12 ++++++ QueryBuilder/Query.cs | 16 ++++++++ QueryBuilder/SqlResult.cs | 4 ++ 5 files changed, 129 insertions(+), 5 deletions(-) create mode 100644 QueryBuilder/NamedParameter.cs diff --git a/QueryBuilder.Tests/DefineTest.cs b/QueryBuilder.Tests/DefineTest.cs index 0b5ff292..e83a6c8a 100644 --- a/QueryBuilder.Tests/DefineTest.cs +++ b/QueryBuilder.Tests/DefineTest.cs @@ -1,3 +1,4 @@ +using System.Collections.Generic; using static SqlKata.Expressions; using SqlKata.Compilers; using SqlKata.Tests.Infrastructure; @@ -15,8 +16,8 @@ public class DefineTest : TestSupport public void Test_Define_Where() { var query = new Query("Products") - .Define("@name", "Anto") - .Where("ProductName", Variable("@name")); + .Define("@name", "Anto") + .Where("ProductName", Variable("@name")); var c = Compile(query); @@ -24,6 +25,32 @@ public void Test_Define_Where() } + [Fact] + public void Test_Define_Parameter_Where() + { + var query = new Query("Products") + .Define("@name", "Anto") + .DefineParameter("@param", "param") + .Where("ProductName", Variable("@name")) + .Where("ProductName", Variable("@param")) + .Where("ProductName", Variable("@param")); + + var c = Compile(query); + + Assert.Equal("SELECT * FROM [Products] WHERE [ProductName] = 'Anto' AND [ProductName] = 'param'" + + " AND [ProductName] = 'param'", c[EngineCodes.SqlServer]); + + var s = Compilers.Compile(query)[EngineCodes.SqlServer]; + Assert.Equal("SELECT * FROM [Products] WHERE [ProductName] = @p0 AND [ProductName] = @param" + + " AND [ProductName] = @param", s.Sql); + var expected = new Dictionary() + { + { "@param", "param" }, + { "@p0", "Anto" } + }; + Assert.Equal(expected, s.NamedBindings); + } + [Fact] public void Test_Define_SubQuery() { @@ -43,6 +70,35 @@ public void Test_Define_SubQuery() } + [Fact] + public void Test_Define_Parameter_SubQuery() + { + + var subquery = new Query("Products") + .AsAverage("unitprice") + .DefineParameter("@UnitsInSt", 10) + .Where("UnitsInStock", ">", Variable("@UnitsInSt")) + .Where("UnitsInStock", ">", Variable("@UnitsInSt")); + + var query = new Query("Products") + .Where("unitprice", ">", subquery) + .Where("UnitsOnOrder", ">", 5); + + var c = Compile(query); + + Assert.Equal("SELECT * FROM [Products] WHERE [unitprice] > (SELECT AVG([unitprice]) AS [avg] FROM [Products] WHERE"+ + " [UnitsInStock] > 10 AND [UnitsInStock] > 10) AND [UnitsOnOrder] > 5", c[EngineCodes.SqlServer]); + + var s = Compilers.Compile(query)[EngineCodes.SqlServer]; + Assert.Equal(2,s.NamedBindings.Count); + var expected = new Dictionary() + { + { "@UnitsInSt", 10 }, + { "@p2", 5 } + }; + Assert.Equal(expected, s.NamedBindings); + } + [Fact] public void Test_Define_WhereEnds() diff --git a/QueryBuilder/Compilers/Compiler.cs b/QueryBuilder/Compilers/Compiler.cs index 5ac6c080..b338a058 100644 --- a/QueryBuilder/Compilers/Compiler.cs +++ b/QueryBuilder/Compilers/Compiler.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.Linq; using System.Text; +using System.Text.RegularExpressions; namespace SqlKata.Compilers { @@ -32,13 +33,13 @@ 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; } @@ -69,10 +70,45 @@ protected Dictionary generateNamedBindings(object[] bindings) .ToDictionary(x => parameterPrefix + x.i, x => x.v); } + protected Dictionary generateNamedParameterMapping(Dictionary namedBindings) + { + return namedBindings + .Where(v => v.Value is NamedParameterVariable _) + .ToDictionary(x => x.Key, + x => ((NamedParameterVariable) x.Value).Variable); + } + + protected string remapNamedParameters(string sql, Dictionary mapping) + { + if (mapping.Count == 0) return sql; + var pattern = string.Join("|", mapping.Keys.Select(Regex.Escape)); + return Regex.Replace(sql, pattern, match => mapping[match.Value]); + } + + protected Dictionary cleanupNamedBindings(Dictionary namedBindings) + { + var result = new Dictionary(); + + foreach (var (key, value) in namedBindings) + { + var actualKey = value is NamedParameterVariable k ? k.Variable : key; + var actualValue = value is NamedParameterVariable v ? v.Value : value; + + result.TryAdd(actualKey, actualValue); + } + + return result; + } + + protected SqlResult PrepareResult(SqlResult ctx) { ctx.NamedBindings = generateNamedBindings(ctx.Bindings.ToArray()); ctx.Sql = Helper.ReplaceAll(ctx.RawSql, parameterPlaceholder, EscapeCharacter, i => parameterPrefix + i); + var mapping = generateNamedParameterMapping(ctx.NamedBindings); + if (mapping.Count == 0) return ctx; + ctx.NamedBindings = cleanupNamedBindings(ctx.NamedBindings); + ctx.Sql = remapNamedParameters(ctx.Sql,mapping); return ctx; } @@ -295,7 +331,7 @@ protected virtual SqlResult CompileDeleteQuery(Query query) } else { - // check if we have alias + // check if we have alias if (fromClause is FromClause && !string.IsNullOrEmpty(fromClause.Alias)) { ctx.RawSql = $"DELETE {Wrap(fromClause.Alias)} FROM {table} {joins}{where}"; diff --git a/QueryBuilder/NamedParameter.cs b/QueryBuilder/NamedParameter.cs new file mode 100644 index 00000000..2d8b02db --- /dev/null +++ b/QueryBuilder/NamedParameter.cs @@ -0,0 +1,12 @@ +namespace SqlKata; + +// public class NamedParameter(object value) +// { +// public object Value { get; set; } = value; +// } + +public class NamedParameterVariable(string variable, object value) +{ + public object Value { get; set; } = value; + public string Variable { get; set; } = variable; +} diff --git a/QueryBuilder/Query.cs b/QueryBuilder/Query.cs index 8435eca6..5e5f07a4 100755 --- a/QueryBuilder/Query.cs +++ b/QueryBuilder/Query.cs @@ -376,6 +376,22 @@ public Query Define(string variable, object value) return this; } + /// + /// Define a parameter to be used within the query + /// + /// + /// + /// + public Query DefineParameter(string variable, object value) + { + Variables.Add(variable, new NamedParameterVariable(variable,value)); + + return this; + } + + + + public object FindVariable(string variable) { var found = Variables.ContainsKey(variable); diff --git a/QueryBuilder/SqlResult.cs b/QueryBuilder/SqlResult.cs index b84baf8a..635bebcf 100644 --- a/QueryBuilder/SqlResult.cs +++ b/QueryBuilder/SqlResult.cs @@ -46,6 +46,10 @@ public override string ToString() } var value = deepParameters[i]; + if (value is NamedParameterVariable v) + { + return ChangeToSqlValue(v.Value); + } return ChangeToSqlValue(value); }); From e4b5f5cdf28fb9330b6f16a872651879be22355c Mon Sep 17 00:00:00 2001 From: Michel Bieleveld Date: Wed, 9 Apr 2025 13:08:25 -0300 Subject: [PATCH 2/4] Cleaned up and add mysql test --- .../{ => MySql}/MySqlExecutionTest.cs | 15 ++++++++------- QueryBuilder.Tests/MySql/docker-compose.yaml | 0 2 files changed, 8 insertions(+), 7 deletions(-) rename QueryBuilder.Tests/{ => MySql}/MySqlExecutionTest.cs (96%) create mode 100644 QueryBuilder.Tests/MySql/docker-compose.yaml diff --git a/QueryBuilder.Tests/MySqlExecutionTest.cs b/QueryBuilder.Tests/MySql/MySqlExecutionTest.cs similarity index 96% rename from QueryBuilder.Tests/MySqlExecutionTest.cs rename to QueryBuilder.Tests/MySql/MySqlExecutionTest.cs index 6da915d2..46c989db 100644 --- a/QueryBuilder.Tests/MySqlExecutionTest.cs +++ b/QueryBuilder.Tests/MySql/MySqlExecutionTest.cs @@ -213,12 +213,12 @@ public void BasicSelectFilter() // 2020 {"2020-01-01", 10}, {"2020-05-01", 20}, - + // 2021 {"2021-01-01", 40}, {"2021-02-01", 10}, {"2021-04-01", -10}, - + // 2022 {"2022-01-01", 80}, {"2022-02-01", -30}, @@ -251,10 +251,11 @@ public void BasicSelectFilter() QueryFactory DB() { - var host = System.Environment.GetEnvironmentVariable("SQLKATA_MYSQL_HOST"); - var user = System.Environment.GetEnvironmentVariable("SQLKATA_MYSQL_USER"); - var dbName = System.Environment.GetEnvironmentVariable("SQLKATA_MYSQL_DB"); - var cs = $"server={host};user={user};database={dbName}"; + var host = System.Environment.GetEnvironmentVariable("SQLKATA_MYSQL_HOST") ?? "localhost"; + var user = System.Environment.GetEnvironmentVariable("SQLKATA_MYSQL_USER") ?? "root"; + var password = System.Environment.GetEnvironmentVariable("SQLKATA_MYSQL_PASSWORD") ?? "my-secret-pw"; + var dbName = System.Environment.GetEnvironmentVariable("SQLKATA_MYSQL_DB") ?? "test"; + var cs = $"server={host};user={user};database={dbName};password={password}"; var connection = new MySqlConnection(cs); @@ -266,4 +267,4 @@ QueryFactory DB() } -} \ No newline at end of file +} diff --git a/QueryBuilder.Tests/MySql/docker-compose.yaml b/QueryBuilder.Tests/MySql/docker-compose.yaml new file mode 100644 index 00000000..e69de29b From 154b1bcc8f96bd65e14b223af273991ecfdcdf80 Mon Sep 17 00:00:00 2001 From: Michel Bieleveld Date: Wed, 9 Apr 2025 17:26:52 -0300 Subject: [PATCH 3/4] Cleaned up test --- QueryBuilder.Tests/DefineTest.cs | 28 ++++++------- .../MySql/MySqlExecutionTest.cs | 42 ++++++++++++++++--- QueryBuilder.Tests/MySql/docker-compose.yaml | 13 ++++++ ...Parameter.cs => NamedParameterVariable.cs} | 5 --- QueryBuilder/Query.cs | 5 ++- QueryBuilder/Variable.cs | 2 +- 6 files changed, 65 insertions(+), 30 deletions(-) rename QueryBuilder/{NamedParameter.cs => NamedParameterVariable.cs} (64%) diff --git a/QueryBuilder.Tests/DefineTest.cs b/QueryBuilder.Tests/DefineTest.cs index e83a6c8a..b37be0d1 100644 --- a/QueryBuilder.Tests/DefineTest.cs +++ b/QueryBuilder.Tests/DefineTest.cs @@ -74,27 +74,23 @@ public void Test_Define_SubQuery() public void Test_Define_Parameter_SubQuery() { - var subquery = new Query("Products") - .AsAverage("unitprice") - .DefineParameter("@UnitsInSt", 10) - .Where("UnitsInStock", ">", Variable("@UnitsInSt")) - .Where("UnitsInStock", ">", Variable("@UnitsInSt")); - var query = new Query("Products") - .Where("unitprice", ">", subquery) - .Where("UnitsOnOrder", ">", 5); - - var c = Compile(query); - - Assert.Equal("SELECT * FROM [Products] WHERE [unitprice] > (SELECT AVG([unitprice]) AS [avg] FROM [Products] WHERE"+ - " [UnitsInStock] > 10 AND [UnitsInStock] > 10) AND [UnitsOnOrder] > 5", c[EngineCodes.SqlServer]); + .DefineParameter("@a", 1) + .DefineParameter("@b","b") + .Where(q=> q.Where("a", "=", Variable("@a"))) + .OrWhere(q => + q.Where("a", "=", Variable("@a")) + .Where("b",">",Variable("@b")) + .Where("a",">",0)); var s = Compilers.Compile(query)[EngineCodes.SqlServer]; - Assert.Equal(2,s.NamedBindings.Count); + Assert.Equal("SELECT * FROM [Products] WHERE ([a] = @a) OR ([a] = @a AND [b] > @b AND [a] > @p3)", s.Sql); + Assert.Equal(3,s.NamedBindings.Count); var expected = new Dictionary() { - { "@UnitsInSt", 10 }, - { "@p2", 5 } + { "@a", 1 }, + { "@b", "b" }, + { "@p3", 0 } }; Assert.Equal(expected, s.NamedBindings); } diff --git a/QueryBuilder.Tests/MySql/MySqlExecutionTest.cs b/QueryBuilder.Tests/MySql/MySqlExecutionTest.cs index 46c989db..45720b46 100644 --- a/QueryBuilder.Tests/MySql/MySqlExecutionTest.cs +++ b/QueryBuilder.Tests/MySql/MySqlExecutionTest.cs @@ -1,13 +1,12 @@ +using System.Collections.Generic; +using System.Linq; +using MySql.Data.MySqlClient; using SqlKata.Compilers; -using Xunit; using SqlKata.Execution; -using MySql.Data.MySqlClient; -using System; -using System.Linq; +using Xunit; using static SqlKata.Expressions; -using System.Collections.Generic; -namespace SqlKata.Tests +namespace SqlKata.Tests.MySql { public class MySqlExecutionTest { @@ -134,6 +133,37 @@ public void QueryWithVariable() db.Drop("Cars"); } + [Fact] + public void QueryWithParameter() + { + var db = DB().Create("Cars", new[] { + "Id INT PRIMARY KEY AUTO_INCREMENT", + "Brand TEXT NOT NULL", + "Year INT NOT NULL", + "Color TEXT NULL", + }); + + for (int i = 0; i < 10; i++) + { + db.Query("Cars").Insert(new + { + Brand = "Brand " + i, + Year = "2020", + }); + } + + + var count = db.Query("Cars") + .DefineParameter("Threshold", 5) + .Where("Id", "<", Variable("Threshold")) + .Where("Id", "<", Variable("Threshold")) + .Count(); + + Assert.Equal(4, count); + + db.Drop("Cars"); + } + [Fact] public void InlineTable() { diff --git a/QueryBuilder.Tests/MySql/docker-compose.yaml b/QueryBuilder.Tests/MySql/docker-compose.yaml index e69de29b..29b5b801 100644 --- a/QueryBuilder.Tests/MySql/docker-compose.yaml +++ b/QueryBuilder.Tests/MySql/docker-compose.yaml @@ -0,0 +1,13 @@ +# Use root/my-secret-pw as user/password credentials +version: '3.1' + +services: + + db: + image: mysql + restart: always + environment: + MYSQL_ROOT_PASSWORD: my-secret-pw + MYSQL_DATABASE: test + ports: + - "3306:3306" diff --git a/QueryBuilder/NamedParameter.cs b/QueryBuilder/NamedParameterVariable.cs similarity index 64% rename from QueryBuilder/NamedParameter.cs rename to QueryBuilder/NamedParameterVariable.cs index 2d8b02db..b9e1d8ff 100644 --- a/QueryBuilder/NamedParameter.cs +++ b/QueryBuilder/NamedParameterVariable.cs @@ -1,10 +1,5 @@ namespace SqlKata; -// public class NamedParameter(object value) -// { -// public object Value { get; set; } = value; -// } - public class NamedParameterVariable(string variable, object value) { public object Value { get; set; } = value; diff --git a/QueryBuilder/Query.cs b/QueryBuilder/Query.cs index 5e5f07a4..dbe9d222 100755 --- a/QueryBuilder/Query.cs +++ b/QueryBuilder/Query.cs @@ -371,7 +371,7 @@ public Query IncludeMany(string relationName, Query query, string foreignKey = n /// public Query Define(string variable, object value) { - Variables.Add(variable, value); + Variables.Add($"@{variable.TrimStart('@')}", value); return this; } @@ -384,7 +384,8 @@ public Query Define(string variable, object value) /// public Query DefineParameter(string variable, object value) { - Variables.Add(variable, new NamedParameterVariable(variable,value)); + var name = $"@{variable.TrimStart('@')}"; + Variables.Add(name, new NamedParameterVariable(name,value)); return this; } diff --git a/QueryBuilder/Variable.cs b/QueryBuilder/Variable.cs index 63b936ea..93509faa 100644 --- a/QueryBuilder/Variable.cs +++ b/QueryBuilder/Variable.cs @@ -6,7 +6,7 @@ public class Variable public Variable(string name) { - this.Name = name; + this.Name = $"@{name.TrimStart('@')}"; } } From 38bf8f90a21539a4f6c70b939cca980e2f8c9eb3 Mon Sep 17 00:00:00 2001 From: Michel Bieleveld Date: Wed, 9 Apr 2025 22:00:02 -0300 Subject: [PATCH 4/4] Reconsidered approach, now clean and now utilizes the standard helper.ReplaceAll --- QueryBuilder.Tests/DefineTest.cs | 2 ++ QueryBuilder/Compilers/Compiler.cs | 50 ++++++++++-------------------- QueryBuilder/Query.cs | 5 ++- QueryBuilder/Variable.cs | 2 +- 4 files changed, 22 insertions(+), 37 deletions(-) diff --git a/QueryBuilder.Tests/DefineTest.cs b/QueryBuilder.Tests/DefineTest.cs index b37be0d1..d3714dbd 100644 --- a/QueryBuilder.Tests/DefineTest.cs +++ b/QueryBuilder.Tests/DefineTest.cs @@ -28,6 +28,8 @@ public void Test_Define_Where() [Fact] public void Test_Define_Parameter_Where() { + // note parameters need to start with @ or any other standard parameter indicator for + // the library running the query. var query = new Query("Products") .Define("@name", "Anto") .DefineParameter("@param", "param") diff --git a/QueryBuilder/Compilers/Compiler.cs b/QueryBuilder/Compilers/Compiler.cs index b338a058..84fff752 100644 --- a/QueryBuilder/Compilers/Compiler.cs +++ b/QueryBuilder/Compilers/Compiler.cs @@ -64,51 +64,35 @@ protected Compiler() }; - protected Dictionary generateNamedBindings(object[] bindings) + protected (string Name,object Variable)[] generateNamedBindingsArray(object[] bindings) { - return Helper.Flatten(bindings).Select((v, i) => new { i, v }) - .ToDictionary(x => parameterPrefix + x.i, x => x.v); - } - - protected Dictionary generateNamedParameterMapping(Dictionary namedBindings) - { - return namedBindings - .Where(v => v.Value is NamedParameterVariable _) - .ToDictionary(x => x.Key, - x => ((NamedParameterVariable) x.Value).Variable); - } - - protected string remapNamedParameters(string sql, Dictionary mapping) - { - if (mapping.Count == 0) return sql; - var pattern = string.Join("|", mapping.Keys.Select(Regex.Escape)); - return Regex.Replace(sql, pattern, match => mapping[match.Value]); + return Helper.Flatten(bindings).Select((v, i) => + { + if (v is NamedParameterVariable param) + { + return (param.Variable, param.Value); + } + return (parameterPrefix + i, v); + }).ToArray(); } - protected Dictionary cleanupNamedBindings(Dictionary namedBindings) + protected Dictionary generateNamedBindings((string, object)[] bindings) { - var result = new Dictionary(); - - foreach (var (key, value) in namedBindings) + var dictionary = new Dictionary(); + foreach (var (name, variable) in bindings) { - var actualKey = value is NamedParameterVariable k ? k.Variable : key; - var actualValue = value is NamedParameterVariable v ? v.Value : value; - - result.TryAdd(actualKey, actualValue); + dictionary.TryAdd(name, variable); } - return result; + return dictionary; } protected SqlResult PrepareResult(SqlResult ctx) { - ctx.NamedBindings = generateNamedBindings(ctx.Bindings.ToArray()); - ctx.Sql = Helper.ReplaceAll(ctx.RawSql, parameterPlaceholder, EscapeCharacter, i => parameterPrefix + i); - var mapping = generateNamedParameterMapping(ctx.NamedBindings); - if (mapping.Count == 0) return ctx; - ctx.NamedBindings = cleanupNamedBindings(ctx.NamedBindings); - ctx.Sql = remapNamedParameters(ctx.Sql,mapping); + var bindings = generateNamedBindingsArray(ctx.Bindings.ToArray()); + ctx.NamedBindings = generateNamedBindings(bindings); + ctx.Sql = Helper.ReplaceAll(ctx.RawSql, parameterPlaceholder, EscapeCharacter, i => bindings[i].Name); return ctx; } diff --git a/QueryBuilder/Query.cs b/QueryBuilder/Query.cs index dbe9d222..5e5f07a4 100755 --- a/QueryBuilder/Query.cs +++ b/QueryBuilder/Query.cs @@ -371,7 +371,7 @@ public Query IncludeMany(string relationName, Query query, string foreignKey = n /// public Query Define(string variable, object value) { - Variables.Add($"@{variable.TrimStart('@')}", value); + Variables.Add(variable, value); return this; } @@ -384,8 +384,7 @@ public Query Define(string variable, object value) /// public Query DefineParameter(string variable, object value) { - var name = $"@{variable.TrimStart('@')}"; - Variables.Add(name, new NamedParameterVariable(name,value)); + Variables.Add(variable, new NamedParameterVariable(variable,value)); return this; } diff --git a/QueryBuilder/Variable.cs b/QueryBuilder/Variable.cs index 93509faa..63b936ea 100644 --- a/QueryBuilder/Variable.cs +++ b/QueryBuilder/Variable.cs @@ -6,7 +6,7 @@ public class Variable public Variable(string name) { - this.Name = $"@{name.TrimStart('@')}"; + this.Name = name; } }