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, document this. #3604

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 5 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,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);
```
<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 -->
27 changes: 25 additions & 2 deletions docs/documents/querying/linq/sql.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,16 +21,39 @@ 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 -->

Alternatively, named parameters can be used by passing in an anonymous object or a `Dictionary<string, object?>`. This can be done anywhere where you can pass in sql parameters:

<!-- snippet: sample_query_with_matches_sql_parameters -->
<a id='snippet-sample_query_with_matches_sql_parameters'></a>
```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<User>().Single(x =>
x.MatchesSql("data->> 'FirstName' = @First and data->> 'LastName' = @Last", parameters));

user.Id.ShouldBe(u.Id);
}
```
<sup><a href='https://github.com/JasperFx/marten/blob/master/src/DocumentDbTests/Reading/query_by_sql.cs#L284-L301' title='Snippet source file'>snippet source</a> | <a href='#snippet-sample_query_with_matches_sql_parameters' title='Start of snippet'>anchor</a></sup>
<!-- endSnippet -->

**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 -->
25 changes: 20 additions & 5 deletions docs/documents/querying/sql.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,18 +15,33 @@ 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 -->

And finally asynchronously:
Expand All @@ -37,7 +52,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 All @@ -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<int>("select count(*) from mt_doc_target");
```
<sup><a href='https://github.com/JasperFx/marten/blob/master/src/DocumentDbTests/Reading/query_by_sql.cs#L376-L381' title='Snippet source file'>snippet source</a> | <a href='#snippet-sample_query_by_full_sql' title='Start of snippet'>anchor</a></sup>
<sup><a href='https://github.com/JasperFx/marten/blob/master/src/DocumentDbTests/Reading/query_by_sql.cs#L395-L400' title='Snippet source file'>snippet source</a> | <a href='#snippet-sample_query_by_full_sql' title='Start of snippet'>anchor</a></sup>
<!-- endSnippet -->

When querying single JSONB properties into a primitive/value type, you'll need to cast the value to the respective postgres type:
Expand All @@ -61,7 +76,7 @@ When querying single JSONB properties into a primitive/value type, you'll need t
var times = await session.QueryAsync<DateTimeOffset>(
"SELECT (data ->> 'ModifiedAt')::timestamptz from mt_doc_user");
```
<sup><a href='https://github.com/JasperFx/marten/blob/master/src/DocumentDbTests/Reading/query_by_sql.cs#L330-L335' title='Snippet source file'>snippet source</a> | <a href='#snippet-sample_using-queryasync-casting' title='Start of snippet'>anchor</a></sup>
<sup><a href='https://github.com/JasperFx/marten/blob/master/src/DocumentDbTests/Reading/query_by_sql.cs#L349-L354' title='Snippet source file'>snippet source</a> | <a href='#snippet-sample_using-queryasync-casting' title='Start of snippet'>anchor</a></sup>
<!-- endSnippet -->

The basic rules for how Marten handles user-supplied queries are:
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", "Jeremy", "Oskar", "Hawx");
}
}
}
21 changes: 20 additions & 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 Expand Up @@ -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<User>().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()
{
Expand Down
34 changes: 30 additions & 4 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,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<string, object> { { "Name1", "foo" }, { "Name2", "bar" } };

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

Expand All @@ -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<User>().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<User>().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<User>().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<User>().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]
Expand All @@ -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<string, string> { { "Name2", "jill" } }));

// no where clause
session.Query<User>().Where(x => x.MatchesSql(whereFragment)).OrderBy(x => x.UserName).Select(x => x.UserName)
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
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