diff --git a/docs/documents/querying/linq/sql.md b/docs/documents/querying/linq/sql.md
index bac3d6805d..edc1802753 100644
--- a/docs/documents/querying/linq/sql.md
+++ b/docs/documents/querying/linq/sql.md
@@ -18,7 +18,7 @@ public async Task query_with_matches_sql()
user.Id.ShouldBe(u.Id);
}
```
-snippet source | anchor
+snippet source | anchor
**But**, if you want to take advantage of the more recent and very powerful JSONPath style querying, you will find that using `?` as a placeholder is not suitable, as that character is widely used in JSONPath expressions. If you encounter this issue or write another query where the `?` character is not suitable, you can change the placeholder by providing an alternative. Pass this in before the sql argument.
diff --git a/docs/documents/querying/sql.md b/docs/documents/querying/sql.md
index 619168afb4..a38a41e0b8 100644
--- a/docs/documents/querying/sql.md
+++ b/docs/documents/querying/sql.md
@@ -54,7 +54,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:
@@ -65,7 +65,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/DocumentDbTests/Reading/query_by_sql.cs b/src/DocumentDbTests/Reading/query_by_sql.cs
index c0a6547cf8..a351d5abc9 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;
@@ -70,6 +70,31 @@ public async Task stream_query_by_one_parameter()
firstnames[2].ShouldBe("Max");
}
+ [Fact]
+ public async Task stream_query_by_one_parameter_custom_placeholder()
+ {
+ await using var session = theStore.LightweightSession();
+ session.Store(new User { FirstName = "Jeremy", LastName = "Miller" });
+ session.Store(new User { FirstName = "Lindsey", LastName = "Miller" });
+ session.Store(new User { FirstName = "Max", LastName = "Miller" });
+ session.Store(new User { FirstName = "Frank", LastName = "Zombo" });
+ await session.SaveChangesAsync();
+
+ var stream = new MemoryStream();
+ await session.StreamJson(stream, '$', "where data ->> 'LastName' = $", "Miller");
+
+ stream.Position = 0;
+ var results = theStore.Options.Serializer().FromJson(stream);
+ var firstnames = results
+ .OrderBy(x => x.FirstName)
+ .Select(x => x.FirstName).ToArray();
+
+ firstnames.Length.ShouldBe(3);
+ firstnames[0].ShouldBe("Jeremy");
+ firstnames[1].ShouldBe("Lindsey");
+ firstnames[2].ShouldBe("Max");
+ }
+
[Fact]
public async Task query_by_one_parameter()
{
@@ -90,6 +115,50 @@ public async Task query_by_one_parameter()
firstnames[2].ShouldBe("Max");
}
+ [Fact]
+ public async Task query_by_one_parameter_async()
+ {
+ await using var session = theStore.LightweightSession();
+ session.Store(new User { FirstName = "Jeremy", LastName = "Miller" });
+ session.Store(new User { FirstName = "Lindsey", LastName = "Miller" });
+ session.Store(new User { FirstName = "Max", LastName = "Miller" });
+ session.Store(new User { FirstName = "Frank", LastName = "Zombo" });
+ await session.SaveChangesAsync();
+
+ var firstnames =
+ (await session.QueryAsync("where data ->> 'LastName' = ?", "Miller"))
+ .OrderBy(x => x.FirstName)
+ .Select(x => x.FirstName)
+ .ToArray();
+
+ firstnames.Length.ShouldBe(3);
+ firstnames[0].ShouldBe("Jeremy");
+ firstnames[1].ShouldBe("Lindsey");
+ firstnames[2].ShouldBe("Max");
+ }
+
+ [Fact]
+ public async Task query_by_one_parameter_async_custom_placeholder()
+ {
+ await using var session = theStore.LightweightSession();
+ session.Store(new User { FirstName = "Jeremy", LastName = "Miller" });
+ session.Store(new User { FirstName = "Lindsey", LastName = "Miller" });
+ session.Store(new User { FirstName = "Max", LastName = "Miller" });
+ session.Store(new User { FirstName = "Frank", LastName = "Zombo" });
+ await session.SaveChangesAsync();
+
+ var firstnames =
+ (await session.QueryAsync('$', "where data ->> 'LastName' = $", "Miller"))
+ .OrderBy(x => x.FirstName)
+ .Select(x => x.FirstName)
+ .ToArray();
+
+ firstnames.Length.ShouldBe(3);
+ firstnames[0].ShouldBe("Jeremy");
+ firstnames[1].ShouldBe("Lindsey");
+ firstnames[2].ShouldBe("Max");
+ }
+
[Fact]
public async Task query_ignores_case_of_where_keyword()
{
@@ -280,6 +349,19 @@ public async Task query_with_matches_sql()
}
#endregion
+
+ [Fact]
+ public async Task query_with_matches_sql_custom_placeholder()
+ {
+ await using var session = theStore.LightweightSession();
+ var u = new User { FirstName = "Eric", LastName = "Smith" };
+ session.Store(u);
+ await session.SaveChangesAsync();
+
+ var user = await session.Query().Where(x => x.MatchesSql('$', "data->> 'FirstName' = $", "Eric")).SingleAsync();
+ user.LastName.ShouldBe("Smith");
+ user.Id.ShouldBe(u.Id);
+ }
[Fact]
public async Task query_with_select_in_query()
diff --git a/src/Marten/Linq/MatchesSql/MatchesSqlParser.cs b/src/Marten/Linq/MatchesSql/MatchesSqlParser.cs
index 7896e5856a..9f66ff88ce 100644
--- a/src/Marten/Linq/MatchesSql/MatchesSqlParser.cs
+++ b/src/Marten/Linq/MatchesSql/MatchesSqlParser.cs
@@ -26,7 +26,7 @@ public class MatchesSqlParser: IMethodCallParser
public bool Matches(MethodCallExpression expression)
{
- return Equals(expression.Method, _sqlMethod) || Equals(expression.Method, _fragmentMethod);
+ return Equals(expression.Method, _sqlMethod) || Equals(expression.Method, _sqlMethodWithPlaceholder) || Equals(expression.Method, _fragmentMethod);
}
public ISqlFragment? Parse(IQueryableMemberCollection memberCollection, IReadOnlyStoreOptions options,
@@ -40,8 +40,8 @@ public bool Matches(MethodCallExpression expression)
if (expression.Method.Equals(_sqlMethodWithPlaceholder))
{
- return new CustomizableWhereFragment(expression.Arguments[1].Value().As(),
- expression.Arguments[2].Value().As().ToString(),
+ return new CustomizableWhereFragment(expression.Arguments[2].Value().As(),
+ expression.Arguments[1].Value().As().ToString(),
expression.Arguments[3].Value().As