diff --git a/QueryBuilder.Tests/DefineTest.cs b/QueryBuilder.Tests/DefineTest.cs index 0b5ff292..d3714dbd 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,34 @@ 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") + .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 +72,31 @@ public void Test_Define_SubQuery() } + [Fact] + public void Test_Define_Parameter_SubQuery() + { + + var query = new Query("Products") + .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("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() + { + { "@a", 1 }, + { "@b", "b" }, + { "@p3", 0 } + }; + Assert.Equal(expected, s.NamedBindings); + } + [Fact] public void Test_Define_WhereEnds() diff --git a/QueryBuilder.Tests/MySqlExecutionTest.cs b/QueryBuilder.Tests/MySql/MySqlExecutionTest.cs similarity index 87% rename from QueryBuilder.Tests/MySqlExecutionTest.cs rename to QueryBuilder.Tests/MySql/MySqlExecutionTest.cs index 6da915d2..45720b46 100644 --- a/QueryBuilder.Tests/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() { @@ -213,12 +243,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 +281,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 +297,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..29b5b801 --- /dev/null +++ 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/Compilers/Compiler.cs b/QueryBuilder/Compilers/Compiler.cs index 5ac6c080..84fff752 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; } @@ -63,16 +64,35 @@ protected Compiler() }; - protected Dictionary generateNamedBindings(object[] bindings) + protected (string Name,object Variable)[] generateNamedBindingsArray(object[] bindings) + { + return Helper.Flatten(bindings).Select((v, i) => + { + if (v is NamedParameterVariable param) + { + return (param.Variable, param.Value); + } + return (parameterPrefix + i, v); + }).ToArray(); + } + + protected Dictionary generateNamedBindings((string, object)[] bindings) { - return Helper.Flatten(bindings).Select((v, i) => new { i, v }) - .ToDictionary(x => parameterPrefix + x.i, x => x.v); + var dictionary = new Dictionary(); + foreach (var (name, variable) in bindings) + { + dictionary.TryAdd(name, variable); + } + + 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 bindings = generateNamedBindingsArray(ctx.Bindings.ToArray()); + ctx.NamedBindings = generateNamedBindings(bindings); + ctx.Sql = Helper.ReplaceAll(ctx.RawSql, parameterPlaceholder, EscapeCharacter, i => bindings[i].Name); return ctx; } @@ -295,7 +315,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/NamedParameterVariable.cs b/QueryBuilder/NamedParameterVariable.cs new file mode 100644 index 00000000..b9e1d8ff --- /dev/null +++ b/QueryBuilder/NamedParameterVariable.cs @@ -0,0 +1,7 @@ +namespace SqlKata; + +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); });