diff --git a/docs/documents/execute-custom-sql.md b/docs/documents/execute-custom-sql.md
index c18f2f1e40..95c626f534 100644
--- a/docs/documents/execute-custom-sql.md
+++ b/docs/documents/execute-custom-sql.md
@@ -2,10 +2,10 @@
Use `QueueSqlCommand(string sql, params object[] parameterValues)` method to register and execute any custom/arbitrary SQL commands with the underlying unit of work, as part of the batched commands within `IDocumentSession`.
-`?` placeholders can be used to denote parameter values. Postgres [type casts `::`](https://www.postgresql.org/docs/15/sql-expressions.html#SQL-SYNTAX-TYPE-CASTS) can be applied to the parameter if needed.
+`?` placeholders can be used to denote parameter values. Postgres [type casts `::`](https://www.postgresql.org/docs/15/sql-expressions.html#SQL-SYNTAX-TYPE-CASTS) can be applied to the parameter if needed. Alternatively named parameters can be used by passing in an anonymous object or a dictionary.
-
+
```cs
theSession.QueueSqlCommand("insert into names (name) values ('Jeremy')");
theSession.QueueSqlCommand("insert into names (name) values ('Babu')");
@@ -14,6 +14,8 @@ theSession.QueueSqlCommand("insert into names (name) values ('Oskar')");
theSession.Store(Target.Random());
var json = "{ \"answer\": 42 }";
theSession.QueueSqlCommand("insert into data (raw_value) values (?::jsonb)", json);
+var parameters = new { newName = "Hawx" };
+theSession.QueueSqlCommand("insert into names (name) values (@newName)", parameters);
```
-snippet source | anchor
+snippet source | anchor
diff --git a/docs/documents/querying/linq/sql.md b/docs/documents/querying/linq/sql.md
index ce7c0e7129..9438a4d64a 100644
--- a/docs/documents/querying/linq/sql.md
+++ b/docs/documents/querying/linq/sql.md
@@ -21,16 +21,39 @@ public async Task query_with_matches_sql()
snippet source | anchor
+Alternatively, named parameters can be used by passing in an anonymous object or a `Dictionary`. This can be done anywhere where you can pass in sql parameters:
+
+
+
+```cs
+[Fact]
+public async Task query_with_matches_sql_and_parameters()
+{
+ using var session = theStore.LightweightSession();
+ var u = new User { FirstName = "Eric", LastName = "Smith" };
+ session.Store(u);
+ await session.SaveChangesAsync();
+
+ var parameters = new { First = "Eric", Last = "Smith" };
+ var user = session.Query().Single(x =>
+ x.MatchesSql("data->> 'FirstName' = @First and data->> 'LastName' = @Last", parameters));
+
+ user.Id.ShouldBe(u.Id);
+}
+```
+snippet source | anchor
+
+
**But**, if you want to take advantage of the more recent and very powerful JSONPath style querying, use this flavor of
the same functionality that behaves exactly the same, but uses the '^' character for parameter placeholders to disambiguate
from the '?' character that is widely used in JSONPath expressions:
-
+
```cs
var results2 = await theSession
.Query().Where(x => x.MatchesJsonPath("d.data @? '$ ? (@.Children[*] == null || @.Children[*].size() == 0)'"))
.ToListAsync();
```
-snippet source | anchor
+snippet source | anchor
diff --git a/docs/documents/querying/sql.md b/docs/documents/querying/sql.md
index 79aae6b5be..bdd5feb4f3 100644
--- a/docs/documents/querying/sql.md
+++ b/docs/documents/querying/sql.md
@@ -15,7 +15,7 @@ Here's the simplest possible usage to query for `User` documents with a `WHERE`
var millers = session
.Query("where data ->> 'LastName' = 'Miller'");
```
-snippet source | anchor
+snippet source | anchor
Or with parameterized SQL:
@@ -23,10 +23,25 @@ Or with parameterized SQL:
```cs
+// pass in a list of anonymous parameters
var millers = session
.Query("where data ->> 'LastName' = ?", "Miller");
+
+// pass in named parameters using an anonymous object
+var params1 = new { First = "Jeremy", Last = "Miller" };
+var jeremysAndMillers1 = session
+ .Query("where data ->> 'FirstName' = @First or data ->> 'LastName' = @Last", params1);
+
+// pass in named parameters using a dictionary
+var params2 = new Dictionary
+{
+ { "First", "Jeremy" },
+ { "Last", "Miller" }
+};
+var jeremysAndMillers2 = session
+ .Query("where data ->> 'FirstName' = @First or data ->> 'LastName' = @Last", params2);
```
-snippet source | anchor
+snippet source | anchor
And finally asynchronously:
@@ -37,7 +52,7 @@ And finally asynchronously:
var millers = await session
.QueryAsync("where data ->> 'LastName' = ?", "Miller");
```
-snippet source | anchor
+snippet source | anchor
All of the samples so far are selecting the whole `User` document and merely supplying
@@ -50,7 +65,7 @@ a document body, but in that case you will need to supply the full SQL statement
var sumResults = await session
.QueryAsync("select count(*) from mt_doc_target");
```
-snippet source | anchor
+snippet source | anchor
When querying single JSONB properties into a primitive/value type, you'll need to cast the value to the respective postgres type:
@@ -61,7 +76,7 @@ When querying single JSONB properties into a primitive/value type, you'll need t
var times = await session.QueryAsync(
"SELECT (data ->> 'ModifiedAt')::timestamptz from mt_doc_user");
```
-snippet source | anchor
+snippet source | anchor
The basic rules for how Marten handles user-supplied queries are:
diff --git a/src/CoreTests/executing_arbitrary_sql_as_part_of_transaction.cs b/src/CoreTests/executing_arbitrary_sql_as_part_of_transaction.cs
index 32c0154cac..f1f19480e7 100644
--- a/src/CoreTests/executing_arbitrary_sql_as_part_of_transaction.cs
+++ b/src/CoreTests/executing_arbitrary_sql_as_part_of_transaction.cs
@@ -44,6 +44,8 @@ public async Task can_run_extra_sql()
theSession.Store(Target.Random());
var json = "{ \"answer\": 42 }";
theSession.QueueSqlCommand("insert into data (raw_value) values (?::jsonb)", json);
+ var parameters = new { newName = "Hawx" };
+ theSession.QueueSqlCommand("insert into names (name) values (@newName)", parameters);
#endregion
await theSession.SaveChangesAsync();
@@ -55,7 +57,7 @@ public async Task can_run_extra_sql()
var names = await conn.CreateCommand("select name from names order by name")
.FetchListAsync();
- names.ShouldHaveTheSameElementsAs("Babu", "Jeremy", "Oskar");
+ names.ShouldHaveTheSameElementsAs("Babu", "Jeremy", "Oskar", "Hawx");
}
}
}
diff --git a/src/DocumentDbTests/Reading/query_by_sql.cs b/src/DocumentDbTests/Reading/query_by_sql.cs
index c0a6547cf8..c943ed033c 100644
--- a/src/DocumentDbTests/Reading/query_by_sql.cs
+++ b/src/DocumentDbTests/Reading/query_by_sql.cs
@@ -1,4 +1,4 @@
-using System;
+using System;
using System.IO;
using System.Linq;
using System.Threading;
@@ -281,6 +281,25 @@ public async Task query_with_matches_sql()
#endregion
+ #region sample_query_with_matches_sql_parameters
+
+ [Fact]
+ public async Task query_with_matches_sql_and_parameters()
+ {
+ using var session = theStore.LightweightSession();
+ var u = new User { FirstName = "Eric", LastName = "Smith" };
+ session.Store(u);
+ await session.SaveChangesAsync();
+
+ var parameters = new { First = "Eric", Last = "Smith" };
+ var user = session.Query().Single(x =>
+ x.MatchesSql("data->> 'FirstName' = @First and data->> 'LastName' = @Last", parameters));
+
+ user.Id.ShouldBe(u.Id);
+ }
+
+ #endregion
+
[Fact]
public async Task query_with_select_in_query()
{
diff --git a/src/LinqTests/Acceptance/matches_sql_queries.cs b/src/LinqTests/Acceptance/matches_sql_queries.cs
index 05b18946fb..4efa2500d6 100644
--- a/src/LinqTests/Acceptance/matches_sql_queries.cs
+++ b/src/LinqTests/Acceptance/matches_sql_queries.cs
@@ -1,4 +1,5 @@
using System;
+using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Marten.Linq.MatchesSql;
@@ -19,7 +20,10 @@ public async Task query_using_matches_sql()
var user3 = new User { UserName = "baz" };
var user4 = new User { UserName = "jack" };
- using var session = theStore.LightweightSession();
+ var paramObject = new { Name1 = "foo", Name2 = "bar" };
+ var paramDict = new Dictionary { { "Name1", "foo" }, { "Name2", "bar" } };
+
+ await using var session = theStore.LightweightSession();
session.Store(user1, user2, user3, user4);
await session.SaveChangesAsync();
@@ -33,6 +37,26 @@ public async Task query_using_matches_sql()
.ToList()
.Select(x => x.UserName)
.Single().ShouldBe("jack");
+
+
+
+ // no where clause, named params
+ session.Query().Where(x => x.MatchesSql("d.data ->> 'UserName' = @Name1 or d.data ->> 'UserName' = @Name2", paramObject)).OrderBy(x => x.UserName).Select(x => x.UserName)
+ .ToList().ShouldHaveTheSameElementsAs("baz", "jack");
+ session.Query().Where(x => x.MatchesSql("d.data ->> 'UserName' = @Name1 or d.data ->> 'UserName' = @Name2", paramDict)).OrderBy(x => x.UserName).Select(x => x.UserName)
+ .ToList().ShouldHaveTheSameElementsAs("baz", "jack");
+
+ // with a where clause, named params
+ session.Query().Where(x => x.UserName != "baz" && x.MatchesSql("d.data ->> 'UserName' != @Name1 and d.data ->> 'UserName' != @Name2", paramObject))
+ .OrderBy(x => x.UserName)
+ .ToList()
+ .Select(x => x.UserName)
+ .Single().ShouldBe("jack");
+ session.Query().Where(x => x.UserName != "baz" && x.MatchesSql("d.data ->> 'UserName' != @Name1 and d.data ->> 'UserName' != @Name2", paramDict))
+ .OrderBy(x => x.UserName)
+ .ToList()
+ .Select(x => x.UserName)
+ .Single().ShouldBe("jack");
}
[Fact]
@@ -42,14 +66,16 @@ public async Task query_using_where_fragment()
var user2 = new User { UserName = "bar" };
var user3 = new User { UserName = "baz" };
var user4 = new User { UserName = "jack" };
+ var user5 = new User { UserName = "jill" };
- using var session = theStore.LightweightSession();
- session.Store(user1, user2, user3, user4);
+ await using var session = theStore.LightweightSession();
+ session.Store(user1, user2, user3, user4, user5);
await session.SaveChangesAsync();
var whereFragment = CompoundWhereFragment.And();
whereFragment.Add(new WhereFragment("d.data ->> 'UserName' != ?", "baz"));
- whereFragment.Add(new WhereFragment("d.data ->> 'UserName' != ?", "jack"));
+ whereFragment.Add(new WhereFragment("d.data ->> 'UserName' != @Name1", new { Name1 = "jack" }));
+ whereFragment.Add(new WhereFragment("d.data ->> 'UserName' != @Name2", new Dictionary { { "Name2", "jill" } }));
// no where clause
session.Query().Where(x => x.MatchesSql(whereFragment)).OrderBy(x => x.UserName).Select(x => x.UserName)
diff --git a/src/Marten.Testing/Examples/QueryBySql.cs b/src/Marten.Testing/Examples/QueryBySql.cs
index 3543521137..76bbed75db 100644
--- a/src/Marten.Testing/Examples/QueryBySql.cs
+++ b/src/Marten.Testing/Examples/QueryBySql.cs
@@ -1,3 +1,4 @@
+using System.Collections.Generic;
using System.Threading.Tasks;
using Marten.Testing.Documents;
@@ -19,9 +20,24 @@ public void QueryWithParameters(IQuerySession session)
{
#region sample_query_with_sql_and_parameters
+ // pass in a list of anonymous parameters
var millers = session
.Query("where data ->> 'LastName' = ?", "Miller");
+ // pass in named parameters using an anonymous object
+ var params1 = new { First = "Jeremy", Last = "Miller" };
+ var jeremysAndMillers1 = session
+ .Query("where data ->> 'FirstName' = @First or data ->> 'LastName' = @Last", params1);
+
+ // pass in named parameters using a dictionary
+ var params2 = new Dictionary
+ {
+ { "First", "Jeremy" },
+ { "Last", "Miller" }
+ };
+ var jeremysAndMillers2 = session
+ .Query("where data ->> 'FirstName' = @First or data ->> 'LastName' = @Last", params2);
+
#endregion
}
@@ -35,4 +51,4 @@ public async Task QueryAsynchronously(IQuerySession session)
#endregion
}
-}
\ No newline at end of file
+}
diff --git a/src/Marten/Internal/Operations/ExecuteSqlStorageOperation.cs b/src/Marten/Internal/Operations/ExecuteSqlStorageOperation.cs
index b0f4502f03..403eb205d9 100644
--- a/src/Marten/Internal/Operations/ExecuteSqlStorageOperation.cs
+++ b/src/Marten/Internal/Operations/ExecuteSqlStorageOperation.cs
@@ -1,8 +1,10 @@
using System;
+using System.Collections;
using System.Collections.Generic;
using System.Data.Common;
using System.Threading;
using System.Threading.Tasks;
+using JasperFx.Core.Reflection;
using Marten.Services;
using Marten.Storage;
using Weasel.Postgresql;
@@ -22,6 +24,13 @@ public ExecuteSqlStorageOperation(string commandText, params object[] parameterV
public void ConfigureCommand(ICommandBuilder builder, IMartenSession session)
{
+ if (_parameterValues is [{ } first] && (first.IsAnonymousType() || first is IDictionary { Keys: ICollection }))
+ {
+ builder.Append(_commandText);
+ builder.AddParameters(first);
+ return;
+ }
+
var parameters = builder.AppendWithParameters(_commandText);
if (parameters.Length != _parameterValues.Length)
{
diff --git a/src/Marten/Linq/QueryHandlers/UserSuppliedQueryHandler.cs b/src/Marten/Linq/QueryHandlers/UserSuppliedQueryHandler.cs
index 65d6c99c8d..3451bb466f 100644
--- a/src/Marten/Linq/QueryHandlers/UserSuppliedQueryHandler.cs
+++ b/src/Marten/Linq/QueryHandlers/UserSuppliedQueryHandler.cs
@@ -1,5 +1,6 @@
#nullable enable
using System;
+using System.Collections;
using System.Collections.Generic;
using System.Data.Common;
using System.IO;
@@ -53,34 +54,30 @@ public void ConfigureCommand(ICommandBuilder builder, IMartenSession session)
}
}
-
- var firstParameter = _parameters.FirstOrDefault();
-
- if (_parameters.Length == 1 && firstParameter != null && firstParameter.IsAnonymousType())
+ if (_parameters is [{ } first] && (first.IsAnonymousType() || first is IDictionary { Keys: ICollection }))
{
builder.Append(_sql);
- builder.AddParameters(firstParameter);
+ builder.AddParameters(first);
+ return;
}
- else
+
+ var cmdParameters = builder.AppendWithParameters(_sql);
+ if (cmdParameters.Length != _parameters.Length)
{
- var cmdParameters = builder.AppendWithParameters(_sql);
- if (cmdParameters.Length != _parameters.Length)
+ throw new InvalidOperationException("Wrong number of supplied parameters");
+ }
+
+ for (var i = 0; i < cmdParameters.Length; i++)
+ {
+ if (_parameters[i] == null!)
{
- throw new InvalidOperationException("Wrong number of supplied parameters");
+ cmdParameters[i].Value = DBNull.Value;
}
-
- for (var i = 0; i < cmdParameters.Length; i++)
+ else
{
- if (_parameters[i] == null!)
- {
- cmdParameters[i].Value = DBNull.Value;
- }
- else
- {
- cmdParameters[i].Value = _parameters[i];
- cmdParameters[i].NpgsqlDbType =
- PostgresqlProvider.Instance.ToParameterType(_parameters[i].GetType());
- }
+ cmdParameters[i].Value = _parameters[i];
+ cmdParameters[i].NpgsqlDbType =
+ PostgresqlProvider.Instance.ToParameterType(_parameters[i].GetType());
}
}
}