Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support named parameters everywhere you can pass parameters in #3604

Open
wants to merge 11 commits into
base: master
Choose a base branch
from
16 changes: 13 additions & 3 deletions docs/documents/execute-custom-sql.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

<!-- snippet: sample_QueueSqlCommand -->
<a id='snippet-sample_queuesqlcommand'></a>
<a id='snippet-sample_QueueSqlCommand'></a>
```cs
theSession.QueueSqlCommand("insert into names (name) values ('Jeremy')");
theSession.QueueSqlCommand("insert into names (name) values ('Babu')");
Expand All @@ -14,6 +14,16 @@ 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);
```
<sup><a href='https://github.com/JasperFx/marten/blob/master/src/CoreTests/executing_arbitrary_sql_as_part_of_transaction.cs#L39-L47' title='Snippet source file'>snippet source</a> | <a href='#snippet-sample_queuesqlcommand' title='Start of snippet'>anchor</a></sup>
<sup><a href='https://github.com/JasperFx/marten/blob/master/src/CoreTests/executing_arbitrary_sql_as_part_of_transaction.cs#L39-L49' title='Snippet source file'>snippet source</a> | <a href='#snippet-sample_QueueSqlCommand' title='Start of snippet'>anchor</a></sup>
<!-- endSnippet -->

::: warning
There are a lot of caveats with using named parameters. Most importantly they cannot be mixed with positional parameters, so be careful when composing multiple queries together. They are also "global" meaning that they will match the parameter name anywhere in the sql command, not just the line you added alongside the parameters.

Under the hood, postgres does not support named parameters. [Npgsql supports named parameters](https://www.npgsql.org/doc/basic-usage.html#positional-and-named-placeholders) by parsing and replacing positional parameters as named parameters. Parsing is not bullet-proof and has a performance impact.

Overall, you should avoid using named parameters unless you really need to.
:::
6 changes: 4 additions & 2 deletions docs/documents/querying/linq/sql.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,16 +21,18 @@ public async Task query_with_matches_sql()
<sup><a href='https://github.com/JasperFx/marten/blob/master/src/DocumentDbTests/Reading/query_by_sql.cs#L267-L282' title='Snippet source file'>snippet source</a> | <a href='#snippet-sample_query_with_matches_sql' title='Start of snippet'>anchor</a></sup>
<!-- endSnippet -->

Named parameters are not supported here, and will throw at runtime if they are used.

**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:

<!-- snippet: sample_using_MatchesJsonPath -->
<a id='snippet-sample_using_matchesjsonpath'></a>
<a id='snippet-sample_using_MatchesJsonPath'></a>
```cs
var results2 = await theSession
.Query<Target>().Where(x => x.MatchesJsonPath("d.data @? '$ ? (@.Children[*] == null || @.Children[*].size() == 0)'"))
.ToListAsync();
```
<sup><a href='https://github.com/JasperFx/marten/blob/master/src/LinqTests/Bugs/Bug_3087_using_JsonPath_with_MatchesSql.cs#L28-L34' title='Snippet source file'>snippet source</a> | <a href='#snippet-sample_using_matchesjsonpath' title='Start of snippet'>anchor</a></sup>
<sup><a href='https://github.com/JasperFx/marten/blob/master/src/LinqTests/Bugs/Bug_3087_using_JsonPath_with_MatchesSql.cs#L28-L34' title='Snippet source file'>snippet source</a> | <a href='#snippet-sample_using_MatchesJsonPath' title='Start of snippet'>anchor</a></sup>
<!-- endSnippet -->
29 changes: 26 additions & 3 deletions docs/documents/querying/sql.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,20 +15,43 @@ Here's the simplest possible usage to query for `User` documents with a `WHERE`
var millers = session
.Query<User>("where data ->> 'LastName' = 'Miller'");
```
<sup><a href='https://github.com/JasperFx/marten/blob/master/src/Marten.Testing/Examples/QueryBySql.cs#L10-L15' title='Snippet source file'>snippet source</a> | <a href='#snippet-sample_query_for_whole_document_by_where_clause' title='Start of snippet'>anchor</a></sup>
<sup><a href='https://github.com/JasperFx/marten/blob/master/src/Marten.Testing/Examples/QueryBySql.cs#L11-L16' title='Snippet source file'>snippet source</a> | <a href='#snippet-sample_query_for_whole_document_by_where_clause' title='Start of snippet'>anchor</a></sup>
<!-- endSnippet -->

Or with parameterized SQL:

<!-- snippet: sample_query_with_sql_and_parameters -->
<a id='snippet-sample_query_with_sql_and_parameters'></a>
```cs
// pass in a list of anonymous parameters
var millers = session
.Query<User>("where data ->> 'LastName' = ?", "Miller");

// pass in named parameters using an anonymous object
var params1 = new { First = "Jeremy", Last = "Miller" };
var jeremysAndMillers1 = session
.Query<User>("where data ->> 'FirstName' = @First or data ->> 'LastName' = @Last", params1);

// pass in named parameters using a dictionary
var params2 = new Dictionary<string, object>
{
{ "First", "Jeremy" },
{ "Last", "Miller" }
};
var jeremysAndMillers2 = session
.Query<User>("where data ->> 'FirstName' = @First or data ->> 'LastName' = @Last", params2);
```
<sup><a href='https://github.com/JasperFx/marten/blob/master/src/Marten.Testing/Examples/QueryBySql.cs#L20-L25' title='Snippet source file'>snippet source</a> | <a href='#snippet-sample_query_with_sql_and_parameters' title='Start of snippet'>anchor</a></sup>
<sup><a href='https://github.com/JasperFx/marten/blob/master/src/Marten.Testing/Examples/QueryBySql.cs#L21-L41' title='Snippet source file'>snippet source</a> | <a href='#snippet-sample_query_with_sql_and_parameters' title='Start of snippet'>anchor</a></sup>
<!-- endSnippet -->

::: warning
There are a lot of caveats with using named parameters. Most importantly they cannot be mixed with positional parameters, so be careful when composing multiple queries together. They are also "global" meaning that they will match the parameter name anywhere in the sql command, not just the line you added alongside the parameters.

Under the hood, postgres does not support named parameters. [Npgsql supports named parameters](https://www.npgsql.org/doc/basic-usage.html#positional-and-named-placeholders) by parsing and replacing positional parameters as named parameters. Parsing is not bullet-proof and has a performance impact.

Overall, you should avoid using named parameters unless you really need to.
:::

And finally asynchronously:

<!-- snippet: sample_query_with_sql_async -->
Expand All @@ -37,7 +60,7 @@ And finally asynchronously:
var millers = await session
.QueryAsync<User>("where data ->> 'LastName' = ?", "Miller");
```
<sup><a href='https://github.com/JasperFx/marten/blob/master/src/Marten.Testing/Examples/QueryBySql.cs#L30-L35' title='Snippet source file'>snippet source</a> | <a href='#snippet-sample_query_with_sql_async' title='Start of snippet'>anchor</a></sup>
<sup><a href='https://github.com/JasperFx/marten/blob/master/src/Marten.Testing/Examples/QueryBySql.cs#L46-L51' title='Snippet source file'>snippet source</a> | <a href='#snippet-sample_query_with_sql_async' title='Start of snippet'>anchor</a></sup>
<!-- endSnippet -->

All of the samples so far are selecting the whole `User` document and merely supplying
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand All @@ -55,7 +57,7 @@ public async Task can_run_extra_sql()
var names = await conn.CreateCommand("select name from names order by name")
.FetchListAsync<string>();

names.ShouldHaveTheSameElementsAs("Babu", "Jeremy", "Oskar");
names.ShouldHaveTheSameElementsAs("Babu", "Hawx", "Jeremy", "Oskar");
}
}
}
2 changes: 1 addition & 1 deletion src/DocumentDbTests/Reading/query_by_sql.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
using System;
using System;
using System.IO;
using System.Linq;
using System.Threading;
Expand Down
5 changes: 3 additions & 2 deletions src/LinqTests/Acceptance/matches_sql_queries.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Marten.Linq.MatchesSql;
Expand All @@ -19,7 +20,7 @@ public async Task query_using_matches_sql()
var user3 = new User { UserName = "baz" };
var user4 = new User { UserName = "jack" };

using var session = theStore.LightweightSession();
await using var session = theStore.LightweightSession();
session.Store(user1, user2, user3, user4);
await session.SaveChangesAsync();

Expand All @@ -43,7 +44,7 @@ public async Task query_using_where_fragment()
var user3 = new User { UserName = "baz" };
var user4 = new User { UserName = "jack" };

using var session = theStore.LightweightSession();
await using var session = theStore.LightweightSession();
session.Store(user1, user2, user3, user4);
await session.SaveChangesAsync();

Expand Down
18 changes: 17 additions & 1 deletion src/Marten.Testing/Examples/QueryBySql.cs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
using System.Collections.Generic;
using System.Threading.Tasks;
using Marten.Testing.Documents;

Expand All @@ -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<User>("where data ->> 'LastName' = ?", "Miller");

// pass in named parameters using an anonymous object
var params1 = new { First = "Jeremy", Last = "Miller" };
var jeremysAndMillers1 = session
.Query<User>("where data ->> 'FirstName' = @First or data ->> 'LastName' = @Last", params1);

// pass in named parameters using a dictionary
var params2 = new Dictionary<string, object>
{
{ "First", "Jeremy" },
{ "Last", "Miller" }
};
var jeremysAndMillers2 = session
.Query<User>("where data ->> 'FirstName' = @First or data ->> 'LastName' = @Last", params2);

#endregion
}

Expand All @@ -35,4 +51,4 @@ public async Task QueryAsynchronously(IQuerySession session)
#endregion
}

}
}
9 changes: 9 additions & 0 deletions src/Marten/Internal/Operations/ExecuteSqlStorageOperation.cs
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -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<string> }))
{
builder.Append(_commandText);
builder.AddParameters(first);
return;
}

var parameters = builder.AppendWithParameters(_commandText);
if (parameters.Length != _parameterValues.Length)
{
Expand Down
3 changes: 2 additions & 1 deletion src/Marten/Linq/QueryHandlers/AdvancedSqlQueryHandler.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
#nullable enable
using System;
using System.Collections;
using System.Collections.Generic;
using System.Data.Common;
using System.IO;
Expand Down Expand Up @@ -128,7 +129,7 @@ public void ConfigureCommand(ICommandBuilder builder, IMartenSession session)
{
var firstParameter = Parameters.FirstOrDefault();

if (Parameters.Length == 1 && firstParameter != null && firstParameter.IsAnonymousType())
if (Parameters.Length == 1 && firstParameter != null && firstParameter.IsAnonymousType() || firstParameter is IDictionary { Keys: ICollection<string> })
{
builder.Append(Sql);
builder.AddParameters(firstParameter);
Expand Down
39 changes: 18 additions & 21 deletions src/Marten/Linq/QueryHandlers/UserSuppliedQueryHandler.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
#nullable enable
using System;
using System.Collections;
using System.Collections.Generic;
using System.Data.Common;
using System.IO;
Expand Down Expand Up @@ -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<string> }))
{
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());
}
}
}
Expand Down
Loading