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