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()); }