From 4e1c354ca01f63241d1d22c480d0e50692118a07 Mon Sep 17 00:00:00 2001 From: Ben Edwards Date: Fri, 10 Jan 2025 17:40:37 +1100 Subject: [PATCH 1/4] Add overload that allows a custom parameter placeholder. The `char placeholder` param is always before the `string sql` param. This is to ensure that the overload is backwards compatible., Updated all methods that accept parameters except for: - Sync methods that are marked obsolete - `MatchesJsonPath` - This should probably be marked as obsolete as the placeholder overload does the same thing but better. Todo: - Tests. --- src/Marten/IAdvancedSql.cs | 106 ++++++++++++++++++ src/Marten/IDocumentOperations.cs | 9 ++ src/Marten/IQuerySession.cs | 51 +++++++++ .../Operations/ExecuteSqlStorageOperation.cs | 6 +- .../Internal/Sessions/DocumentSessionBase.cs | 7 +- .../Sessions/QuerySession.AdvancedSql.cs | 58 +++++++--- .../Internal/Sessions/QuerySession.Json.cs | 13 ++- .../Sessions/QuerySession.Querying.cs | 16 ++- src/Marten/Internal/Sessions/QuerySession.cs | 2 + .../Linq/MatchesSql/MatchesSqlExtensions.cs | 17 ++- .../Linq/MatchesSql/MatchesSqlParser.cs | 11 ++ .../QueryHandlers/AdvancedSqlQueryHandler.cs | 12 +- .../QueryHandlers/UserSuppliedQueryHandler.cs | 6 +- .../Services/BatchQuerying/BatchedQuery.cs | 7 +- .../Services/BatchQuerying/IBatchedQuery.cs | 11 ++ 15 files changed, 300 insertions(+), 32 deletions(-) diff --git a/src/Marten/IAdvancedSql.cs b/src/Marten/IAdvancedSql.cs index dd2c22828c..5b56994c15 100644 --- a/src/Marten/IAdvancedSql.cs +++ b/src/Marten/IAdvancedSql.cs @@ -1,7 +1,9 @@ #nullable enable +using System; using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; +using Marten.Internal.Sessions; namespace Marten; @@ -20,6 +22,21 @@ public interface IAdvancedSql /// Task> QueryAsync(string sql, CancellationToken token, params object[] parameters); + /// + /// Asynchronously queries the document storage with the supplied SQL. + /// The type parameter can be a document class, a scalar or any JSON-serializable class. + /// If the result is a document, the SQL must contain a select with the required fields in the correct order, + /// depending on the session type and the metadata the document might use, at least id and data must be + /// selected. + /// Use to specify a character that will be replaced by positional parameters. + /// + /// + /// + /// + /// + /// + Task> QueryAsync(char placeholder, string sql, CancellationToken token, params object[] parameters); + /// /// Asynchronously queries the document storage with the supplied SQL. /// The type parameters can be any document class, scalar or JSON-serializable class. @@ -35,6 +52,23 @@ public interface IAdvancedSql /// Task> QueryAsync(string sql, CancellationToken token, params object[] parameters); + /// + /// Asynchronously queries the document storage with the supplied SQL. + /// The type parameters can be any document class, scalar or JSON-serializable class. + /// For each result type parameter, the SQL SELECT statement must contain a ROW. + /// For document types, the row must contain the required fields in the correct order, + /// depending on the session type and the metadata the document might use, at least id and data must be + /// provided. + /// Use to specify a character that will be replaced by positional parameters. + /// + /// + /// + /// + /// + /// + /// + Task> QueryAsync(char placeholder, string sql, CancellationToken token, params object[] parameters); + /// /// Asynchronously queries the document storage with the supplied SQL. /// The type parameters can be any document class, scalar or JSON-serializable class. @@ -51,6 +85,24 @@ public interface IAdvancedSql /// Task> QueryAsync(string sql, CancellationToken token, params object[] parameters); + /// + /// Asynchronously queries the document storage with the supplied SQL. + /// The type parameters can be any document class, scalar or JSON-serializable class. + /// For each result type parameter, the SQL SELECT statement must contain a ROW. + /// For document types, the row must contain the required fields in the correct order, + /// depending on the session type and the metadata the document might use, at least id and data must be + /// provided. + /// Use to specify a character that will be replaced by positional parameters. + /// + /// + /// + /// + /// + /// + /// + /// + Task> QueryAsync(char placeholder, string sql, CancellationToken token, params object[] parameters); + /// /// Asynchronously queries the document storage with the supplied SQL. /// The type parameter can be a document class, a scalar or any JSON-serializable class. @@ -62,6 +114,7 @@ public interface IAdvancedSql /// /// /// + [Obsolete(QuerySession.SynchronousRemoval)] IReadOnlyList Query(string sql, params object[] parameters); /// @@ -77,6 +130,7 @@ public interface IAdvancedSql /// /// /// + [Obsolete(QuerySession.SynchronousRemoval)] IReadOnlyList<(T1, T2)> Query(string sql, params object[] parameters); /// @@ -93,6 +147,7 @@ public interface IAdvancedSql /// /// /// + [Obsolete(QuerySession.SynchronousRemoval)] IReadOnlyList<(T1, T2, T3)> Query(string sql, params object[] parameters); /// @@ -109,6 +164,22 @@ public interface IAdvancedSql /// An async enumerable iterating over the results IAsyncEnumerable StreamAsync(string sql, CancellationToken token, params object[] parameters); + /// + /// Asynchronously queries the document storage with the supplied SQL. + /// The type parameters can be any document class, scalar or JSON-serializable class. + /// For each result type parameter, the SQL SELECT statement must contain a ROW. + /// For document types, the row must contain the required fields in the correct order, + /// depending on the session type and the metadata the document might use, at least id and data must be + /// provided. + /// Use to specify a character that will be replaced by positional parameters. + /// + /// + /// + /// + /// + /// An async enumerable iterating over the results + IAsyncEnumerable StreamAsync(char placeholder, string sql, CancellationToken token, params object[] parameters); + /// /// Asynchronously queries the document storage with the supplied SQL. /// The type parameters can be any document class, scalar or JSON-serializable class. @@ -124,6 +195,23 @@ public interface IAdvancedSql /// An async enumerable iterating over the list of result tuples IAsyncEnumerable<(T1, T2)> StreamAsync(string sql, CancellationToken token, params object[] parameters); + /// + /// Asynchronously queries the document storage with the supplied SQL. + /// The type parameters can be any document class, scalar or JSON-serializable class. + /// For each result type parameter, the SQL SELECT statement must contain a ROW. + /// For document types, the row must contain the required fields in the correct order, + /// depending on the session type and the metadata the document might use, at least id and data must be + /// provided. + /// Use to specify a character that will be replaced by positional parameters. + /// + /// + /// + /// + /// + /// + /// An async enumerable iterating over the list of result tuples + IAsyncEnumerable<(T1, T2)> StreamAsync(char placeholder, string sql, CancellationToken token, params object[] parameters); + /// /// Asynchronously queries the document storage with the supplied SQL. /// The type parameters can be any document class, scalar or JSON-serializable class. @@ -139,4 +227,22 @@ public interface IAdvancedSql /// /// An async enumerable iterating over the list of result tuples IAsyncEnumerable<(T1, T2, T3)> StreamAsync(string sql, CancellationToken token, params object[] parameters); + + /// + /// Asynchronously queries the document storage with the supplied SQL. + /// The type parameters can be any document class, scalar or JSON-serializable class. + /// For each result type parameter, the SQL SELECT statement must contain a ROW. + /// For document types, the row must contain the required fields in the correct order, + /// depending on the session type and the metadata the document might use, at least id and data must be + /// provided. + /// Use to specify a character that will be replaced by positional parameters. + /// + /// + /// + /// + /// + /// + /// + /// An async enumerable iterating over the list of result tuples + IAsyncEnumerable<(T1, T2, T3)> StreamAsync(char placeholder, string sql, CancellationToken token, params object[] parameters); } diff --git a/src/Marten/IDocumentOperations.cs b/src/Marten/IDocumentOperations.cs index 68364f4f8b..cbac739cfe 100644 --- a/src/Marten/IDocumentOperations.cs +++ b/src/Marten/IDocumentOperations.cs @@ -232,6 +232,15 @@ public interface IDocumentOperations: IQuerySession /// void QueueSqlCommand(string sql, params object[] parameterValues); + /// + /// Registers a SQL command to be executed with the underlying unit of work as part of the batched command. + /// Use to specify a character that will be replaced by positional parameters. + /// + /// + /// + /// + void QueueSqlCommand(char placeholder, string sql, params object[] parameterValues); + /// /// In the case of a lightweight session, this will direct Marten to opt into identity map mechanics /// for only the document type T. This is a micro-optimization added for the event sourcing + projections diff --git a/src/Marten/IQuerySession.cs b/src/Marten/IQuerySession.cs index 9f11775fbc..c61288b0e2 100644 --- a/src/Marten/IQuerySession.cs +++ b/src/Marten/IQuerySession.cs @@ -184,6 +184,7 @@ public interface IQuerySession: IDisposable, IAsyncDisposable /// /// /// + [Obsolete(QuerySession.SynchronousRemoval)] IReadOnlyList Query(string sql, params object[] parameters); /// @@ -197,6 +198,19 @@ public interface IQuerySession: IDisposable, IAsyncDisposable /// Task StreamJson(Stream destination, CancellationToken token, string sql, params object[] parameters); + /// + /// Stream the results of a user-supplied query directly to a stream as a JSON array. + /// Use to specify a character that will be replaced by positional parameters. + /// + /// + /// + /// + /// + /// + /// + /// + Task StreamJson(Stream destination, CancellationToken token, char placeholder, string sql, params object[] parameters); + /// /// Stream the results of a user-supplied query directly to a stream as a JSON array /// @@ -207,6 +221,18 @@ public interface IQuerySession: IDisposable, IAsyncDisposable /// Task StreamJson(Stream destination, string sql, params object[] parameters); + /// + /// Stream the results of a user-supplied query directly to a stream as a JSON array. + /// Use to specify a character that will be replaced by positional parameters. + /// + /// + /// + /// + /// + /// + /// + Task StreamJson(Stream destination, char placeholder, string sql, params object[] parameters); + /// /// Asynchronously queries the document storage table for the document type T by supplied SQL. See /// https://martendb.io/documents/querying/sql.html for more information on usage. @@ -218,6 +244,19 @@ public interface IQuerySession: IDisposable, IAsyncDisposable /// Task> QueryAsync(string sql, CancellationToken token, params object[] parameters); + /// + /// Asynchronously queries the document storage table for the document type T by supplied SQL. See + /// https://martendb.io/documents/querying/sql.html for more information on usage. + /// Use to specify a character that will be replaced by positional parameters. + /// + /// + /// + /// + /// + /// + /// + Task> QueryAsync(char placeholder, string sql, CancellationToken token, params object[] parameters); + /// /// Asynchronously queries the document storage table for the document type T by supplied SQL. See /// https://martendb.io/documents/querying/sql.html for more information on usage. @@ -228,6 +267,18 @@ public interface IQuerySession: IDisposable, IAsyncDisposable /// Task> QueryAsync(string sql, params object[] parameters); + /// + /// Asynchronously queries the document storage table for the document type T by supplied SQL. See + /// https://martendb.io/documents/querying/sql.html for more information on usage. + /// Use to specify a character that will be replaced by positional parameters. + /// + /// + /// + /// + /// + /// + Task> QueryAsync(char placeholder, string sql, params object[] parameters); + /// /// Asynchronously queries the document storage with the supplied SQL. /// The type parameter can be a document class, a scalar or any JSON-serializable class. diff --git a/src/Marten/Internal/Operations/ExecuteSqlStorageOperation.cs b/src/Marten/Internal/Operations/ExecuteSqlStorageOperation.cs index b0f4502f03..3c114d92c5 100644 --- a/src/Marten/Internal/Operations/ExecuteSqlStorageOperation.cs +++ b/src/Marten/Internal/Operations/ExecuteSqlStorageOperation.cs @@ -12,17 +12,19 @@ namespace Marten.Internal.Operations; internal class ExecuteSqlStorageOperation: IStorageOperation, NoDataReturnedCall { private readonly string _commandText; + private readonly char _placeholder; private readonly object[] _parameterValues; - public ExecuteSqlStorageOperation(string commandText, params object[] parameterValues) + public ExecuteSqlStorageOperation(char placeholder, string commandText, params object[] parameterValues) { _commandText = commandText.TrimEnd(';'); + _placeholder = placeholder; _parameterValues = parameterValues; } public void ConfigureCommand(ICommandBuilder builder, IMartenSession session) { - var parameters = builder.AppendWithParameters(_commandText); + var parameters = builder.AppendWithParameters(_commandText, _placeholder); if (parameters.Length != _parameterValues.Length) { throw new InvalidOperationException( diff --git a/src/Marten/Internal/Sessions/DocumentSessionBase.cs b/src/Marten/Internal/Sessions/DocumentSessionBase.cs index 597ffdbd6a..fdbad417b7 100644 --- a/src/Marten/Internal/Sessions/DocumentSessionBase.cs +++ b/src/Marten/Internal/Sessions/DocumentSessionBase.cs @@ -226,13 +226,18 @@ public void InsertObjects(IEnumerable documents) } public void QueueSqlCommand(string sql, params object[] parameterValues) + { + QueueSqlCommand(DefaultParameterPlaceholder, sql, parameterValues: parameterValues); + } + + public void QueueSqlCommand(char placeholder, string sql, params object[] parameterValues) { sql = sql.TrimEnd(';'); if (sql.Contains(';')) throw new ArgumentOutOfRangeException(nameof(sql), "You must specify one SQL command at a time because of Marten's usage of command batching. ';' cannot be used as a command separator here."); - var operation = new ExecuteSqlStorageOperation(sql, parameterValues); + var operation = new ExecuteSqlStorageOperation(placeholder, sql, parameterValues); QueueOperation(operation); } diff --git a/src/Marten/Internal/Sessions/QuerySession.AdvancedSql.cs b/src/Marten/Internal/Sessions/QuerySession.AdvancedSql.cs index eb1bb54091..749de1b39b 100644 --- a/src/Marten/Internal/Sessions/QuerySession.AdvancedSql.cs +++ b/src/Marten/Internal/Sessions/QuerySession.AdvancedSql.cs @@ -10,11 +10,16 @@ namespace Marten.Internal.Sessions; public partial class QuerySession: IAdvancedSql { - async Task> IAdvancedSql.QueryAsync(string sql, CancellationToken token, params object[] parameters) + Task> IAdvancedSql.QueryAsync(string sql, CancellationToken token, params object[] parameters) + { + return ((IAdvancedSql)this).QueryAsync(DefaultParameterPlaceholder, sql, token, parameters); + } + + async Task> IAdvancedSql.QueryAsync(char placeholder, string sql, CancellationToken token, params object[] parameters) { assertNotDisposed(); - var handler = new AdvancedSqlQueryHandler(this, sql, parameters); + var handler = new AdvancedSqlQueryHandler(this, placeholder, sql, parameters); foreach (var documentType in handler.DocumentTypes) { @@ -25,11 +30,16 @@ async Task> IAdvancedSql.QueryAsync(string sql, Cancellation return await provider.ExecuteHandlerAsync(handler, token).ConfigureAwait(false); } - async Task> IAdvancedSql.QueryAsync(string sql, CancellationToken token, params object[] parameters) + Task> IAdvancedSql.QueryAsync(string sql, CancellationToken token, params object[] parameters) + { + return ((IAdvancedSql)this).QueryAsync(DefaultParameterPlaceholder, sql, token, parameters); + } + + async Task> IAdvancedSql.QueryAsync(char placeholder, string sql, CancellationToken token, params object[] parameters) { assertNotDisposed(); - var handler = new AdvancedSqlQueryHandler(this, sql, parameters); + var handler = new AdvancedSqlQueryHandler(this, placeholder, sql, parameters); foreach (var documentType in handler.DocumentTypes) { @@ -40,11 +50,16 @@ async Task> IAdvancedSql.QueryAsync(string sql, Cancellation return await provider.ExecuteHandlerAsync(handler, token).ConfigureAwait(false); } - async Task> IAdvancedSql.QueryAsync(string sql, CancellationToken token, params object[] parameters) + Task> IAdvancedSql.QueryAsync(string sql, CancellationToken token, params object[] parameters) + { + return ((IAdvancedSql)this).QueryAsync(DefaultParameterPlaceholder, sql, token, parameters); + } + + async Task> IAdvancedSql.QueryAsync(char placeholder, string sql, CancellationToken token, params object[] parameters) { assertNotDisposed(); - var handler = new AdvancedSqlQueryHandler(this, sql, parameters); + var handler = new AdvancedSqlQueryHandler(this, placeholder, sql, parameters); foreach (var documentType in handler.DocumentTypes) { @@ -59,7 +74,7 @@ IReadOnlyList IAdvancedSql.Query(string sql, params object[] parameters) { assertNotDisposed(); - var handler = new AdvancedSqlQueryHandler(this, sql, parameters); + var handler = new AdvancedSqlQueryHandler(this, DefaultParameterPlaceholder, sql, parameters); foreach (var documentType in handler.DocumentTypes) { @@ -74,7 +89,7 @@ IReadOnlyList IAdvancedSql.Query(string sql, params object[] parameters) { assertNotDisposed(); - var handler = new AdvancedSqlQueryHandler(this, sql, parameters); + var handler = new AdvancedSqlQueryHandler(this, DefaultParameterPlaceholder, sql, parameters); foreach (var documentType in handler.DocumentTypes) { @@ -89,7 +104,7 @@ IReadOnlyList IAdvancedSql.Query(string sql, params object[] parameters) { assertNotDisposed(); - var handler = new AdvancedSqlQueryHandler(this, sql, parameters); + var handler = new AdvancedSqlQueryHandler(this, DefaultParameterPlaceholder, sql, parameters); foreach (var documentType in handler.DocumentTypes) { @@ -100,12 +115,15 @@ IReadOnlyList IAdvancedSql.Query(string sql, params object[] parameters) return provider.ExecuteHandler(handler); } - async IAsyncEnumerable IAdvancedSql.StreamAsync(string sql, [EnumeratorCancellation] CancellationToken token, + IAsyncEnumerable IAdvancedSql.StreamAsync(string sql, CancellationToken token, params object[] parameters) + => ((IAdvancedSql)this).StreamAsync(DefaultParameterPlaceholder, sql, token, parameters); + + async IAsyncEnumerable IAdvancedSql.StreamAsync(char placeholder, string sql, [EnumeratorCancellation] CancellationToken token, params object[] parameters) { assertNotDisposed(); - var handler = new AdvancedSqlQueryHandler(this, sql, parameters); + var handler = new AdvancedSqlQueryHandler(this, placeholder, sql, parameters); foreach (var documentType in handler.DocumentTypes) { @@ -121,12 +139,17 @@ async IAsyncEnumerable IAdvancedSql.StreamAsync(string sql, [EnumeratorCan } } - async IAsyncEnumerable<(T1, T2)> IAdvancedSql.StreamAsync(string sql, [EnumeratorCancellation] CancellationToken token, + IAsyncEnumerable<(T1, T2)> IAdvancedSql.StreamAsync(string sql, CancellationToken token, params object[] parameters) + { + return ((IAdvancedSql)this).StreamAsync(DefaultParameterPlaceholder, sql, token, parameters); + } + + async IAsyncEnumerable<(T1, T2)> IAdvancedSql.StreamAsync(char placeholder, string sql, [EnumeratorCancellation] CancellationToken token, params object[] parameters) { assertNotDisposed(); - var handler = new AdvancedSqlQueryHandler(this, sql, parameters); + var handler = new AdvancedSqlQueryHandler(this, placeholder, sql, parameters); foreach (var documentType in handler.DocumentTypes) { @@ -142,12 +165,17 @@ async IAsyncEnumerable IAdvancedSql.StreamAsync(string sql, [EnumeratorCan } } - async IAsyncEnumerable<(T1, T2, T3)> IAdvancedSql.StreamAsync(string sql, [EnumeratorCancellation] CancellationToken token, + IAsyncEnumerable<(T1, T2, T3)> IAdvancedSql.StreamAsync(string sql, CancellationToken token, params object[] parameters) + { + return ((IAdvancedSql)this).StreamAsync(DefaultParameterPlaceholder, sql, token, parameters); + } + + async IAsyncEnumerable<(T1, T2, T3)> IAdvancedSql.StreamAsync(char placeholder, string sql, [EnumeratorCancellation] CancellationToken token, params object[] parameters) { assertNotDisposed(); - var handler = new AdvancedSqlQueryHandler(this, sql, parameters); + var handler = new AdvancedSqlQueryHandler(this, placeholder, sql, parameters); foreach (var documentType in handler.DocumentTypes) { diff --git a/src/Marten/Internal/Sessions/QuerySession.Json.cs b/src/Marten/Internal/Sessions/QuerySession.Json.cs index 743a3bdd34..3b7e62e911 100644 --- a/src/Marten/Internal/Sessions/QuerySession.Json.cs +++ b/src/Marten/Internal/Sessions/QuerySession.Json.cs @@ -53,20 +53,29 @@ public async Task ToJsonMany(ICompiledQuery quer return await stream.ReadAllTextAsync().ConfigureAwait(false); } - public Task StreamJson(Stream destination, CancellationToken token, string sql, params object[] parameters) + public Task StreamJson(Stream destination, CancellationToken token, char placeholder, string sql, params object[] parameters) { assertNotDisposed(); - var handler = new UserSuppliedQueryHandler(this, sql, parameters); + var handler = new UserSuppliedQueryHandler(this, placeholder, sql, parameters); var builder = new CommandBuilder(); handler.ConfigureCommand(builder, this); return StreamMany(builder.Compile(), destination, token); } + public Task StreamJson(Stream destination, CancellationToken token, string sql, params object[] parameters) + { + return StreamJson(destination, token, DefaultParameterPlaceholder, sql, parameters); + } public Task StreamJson(Stream destination, string sql, params object[] parameters) { return StreamJson(destination, CancellationToken.None, sql, parameters); } + public Task StreamJson(Stream destination, char placeholder, string sql, params object[] parameters) + { + return StreamJson(destination, CancellationToken.None, placeholder, sql, parameters); + } + public async Task StreamJson(IQueryHandler handler, Stream destination, CancellationToken token) { var cmd = this.BuildCommand(handler); diff --git a/src/Marten/Internal/Sessions/QuerySession.Querying.cs b/src/Marten/Internal/Sessions/QuerySession.Querying.cs index cba5919c49..2724af4614 100644 --- a/src/Marten/Internal/Sessions/QuerySession.Querying.cs +++ b/src/Marten/Internal/Sessions/QuerySession.Querying.cs @@ -35,7 +35,7 @@ public IReadOnlyList Query(string sql, params object[] parameters) { assertNotDisposed(); - var handler = new UserSuppliedQueryHandler(this, sql, parameters); + var handler = new UserSuppliedQueryHandler(this, DefaultParameterPlaceholder, sql, parameters); if (!handler.SqlContainsCustomSelect) { @@ -46,11 +46,16 @@ public IReadOnlyList Query(string sql, params object[] parameters) return provider.ExecuteHandler(handler); } - public async Task> QueryAsync(string sql, CancellationToken token, params object[] parameters) + public Task> QueryAsync(string sql, CancellationToken token, params object[] parameters) + { + return QueryAsync(DefaultParameterPlaceholder, sql, token, parameters); + } + + public async Task> QueryAsync(char placeholder, string sql, CancellationToken token, params object[] parameters) { assertNotDisposed(); - var handler = new UserSuppliedQueryHandler(this, sql, parameters); + var handler = new UserSuppliedQueryHandler(this, placeholder, sql, parameters); if (!handler.SqlContainsCustomSelect) { @@ -66,6 +71,11 @@ public Task> QueryAsync(string sql, params object[] paramete return QueryAsync(sql, CancellationToken.None, parameters); } + public Task> QueryAsync(char placeholder, string sql, params object[] parameters) + { + return QueryAsync(placeholder, sql, CancellationToken.None, parameters); + } + // TODO -- Obsolete, remove in 8.0, replaced by AdvancedSql.Query* #region Obsolete AdvancedSqlQuery* public Task> AdvancedSqlQueryAsync(string sql, CancellationToken token, diff --git a/src/Marten/Internal/Sessions/QuerySession.cs b/src/Marten/Internal/Sessions/QuerySession.cs index c0036a039b..99391aedd9 100644 --- a/src/Marten/Internal/Sessions/QuerySession.cs +++ b/src/Marten/Internal/Sessions/QuerySession.cs @@ -17,6 +17,8 @@ public partial class QuerySession: IMartenSession, IQuerySession public const string SynchronousRemoval = "All synchronous APIs that result in database calls will be removed in Marten 8.0. Please use the asynchronous equivalent"; + internal const char DefaultParameterPlaceholder = '?'; + private readonly DocumentStore _store; private readonly ResiliencePipeline _resilience; diff --git a/src/Marten/Linq/MatchesSql/MatchesSqlExtensions.cs b/src/Marten/Linq/MatchesSql/MatchesSqlExtensions.cs index 27f449cd00..addc3667af 100644 --- a/src/Marten/Linq/MatchesSql/MatchesSqlExtensions.cs +++ b/src/Marten/Linq/MatchesSql/MatchesSqlExtensions.cs @@ -32,6 +32,21 @@ public static bool MatchesSql(this object doc, string sql, params object[] param $"{nameof(MatchesSql)} extension method can only be used in Marten Linq queries."); } + /// + /// The search results should match the specified raw sql fragment. + /// Use to specify a character that will be replaced by positional parameters. + /// + /// + /// + /// + /// + /// + public static bool MatchesSql(this object doc, char placeholder, string sql, params object[] parameters) + { + throw new NotSupportedException( + $"{nameof(MatchesSql)} extension method can only be used in Marten Linq queries."); + } + /// /// The search results should match the specified raw sql fragment that is assumed to include JSONPath. Use "^" for parameters instead of "?" to disambiguate from JSONPath /// @@ -42,6 +57,6 @@ public static bool MatchesSql(this object doc, string sql, params object[] param public static bool MatchesJsonPath(this object doc, string sql, params object[] parameters) { throw new NotSupportedException( - $"{nameof(MatchesSql)} extension method can only be used in Marten Linq queries."); + $"{nameof(MatchesJsonPath)} extension method can only be used in Marten Linq queries."); } } diff --git a/src/Marten/Linq/MatchesSql/MatchesSqlParser.cs b/src/Marten/Linq/MatchesSql/MatchesSqlParser.cs index 6b3d805acb..7896e5856a 100644 --- a/src/Marten/Linq/MatchesSql/MatchesSqlParser.cs +++ b/src/Marten/Linq/MatchesSql/MatchesSqlParser.cs @@ -16,6 +16,10 @@ public class MatchesSqlParser: IMethodCallParser typeof(MatchesSqlExtensions).GetMethod(nameof(MatchesSqlExtensions.MatchesSql), new[] { typeof(object), typeof(string), typeof(object[]) })!; + private static readonly MethodInfo _sqlMethodWithPlaceholder = + typeof(MatchesSqlExtensions).GetMethod(nameof(MatchesSqlExtensions.MatchesSql), + new[] { typeof(object), typeof(char), typeof(string), typeof(object[]) })!; + private static readonly MethodInfo _fragmentMethod = typeof(MatchesSqlExtensions).GetMethod(nameof(MatchesSqlExtensions.MatchesSql), new[] { typeof(object), typeof(ISqlFragment) })!; @@ -34,6 +38,13 @@ public bool Matches(MethodCallExpression expression) expression.Arguments[2].Value().As()); } + if (expression.Method.Equals(_sqlMethodWithPlaceholder)) + { + return new CustomizableWhereFragment(expression.Arguments[1].Value().As(), + expression.Arguments[2].Value().As().ToString(), + expression.Arguments[3].Value().As()); + } + if (expression.Method.Equals(_fragmentMethod)) { return expression.Arguments[1].Value() as ISqlFragment; diff --git a/src/Marten/Linq/QueryHandlers/AdvancedSqlQueryHandler.cs b/src/Marten/Linq/QueryHandlers/AdvancedSqlQueryHandler.cs index 3765668a5a..38f9701f33 100644 --- a/src/Marten/Linq/QueryHandlers/AdvancedSqlQueryHandler.cs +++ b/src/Marten/Linq/QueryHandlers/AdvancedSqlQueryHandler.cs @@ -18,7 +18,7 @@ namespace Marten.Linq.QueryHandlers; internal class AdvancedSqlQueryHandler: AdvancedSqlQueryHandlerBase, IQueryHandler> { - public AdvancedSqlQueryHandler(IMartenSession session, string sql, object[] parameters):base(sql, parameters) + public AdvancedSqlQueryHandler(IMartenSession session, char placeholder, string sql, object[] parameters): base(placeholder, sql, parameters) { RegisterResultType(session); } @@ -46,7 +46,7 @@ public override async IAsyncEnumerable EnumerateResults(DbDataReader reader, internal class AdvancedSqlQueryHandler: AdvancedSqlQueryHandlerBase<(T1, T2)>, IQueryHandler> { - public AdvancedSqlQueryHandler(IMartenSession session, string sql, object[] parameters) : base(sql, parameters) + public AdvancedSqlQueryHandler(IMartenSession session, char placeholder, string sql, object[] parameters) : base(placeholder, sql, parameters) { RegisterResultType(session); RegisterResultType(session); @@ -77,7 +77,7 @@ public AdvancedSqlQueryHandler(IMartenSession session, string sql, object[] para } internal class AdvancedSqlQueryHandler: AdvancedSqlQueryHandlerBase<(T1, T2, T3)>, IQueryHandler> { - public AdvancedSqlQueryHandler(IMartenSession session, string sql, object[] parameters) : base(sql, parameters) + public AdvancedSqlQueryHandler(IMartenSession session, char placeholder, string sql, object[] parameters) : base(placeholder, sql, parameters) { RegisterResultType(session); RegisterResultType(session); @@ -112,13 +112,15 @@ public AdvancedSqlQueryHandler(IMartenSession session, string sql, object[] para internal abstract class AdvancedSqlQueryHandlerBase { + protected readonly char Placeholder; protected readonly object[] Parameters; protected readonly string Sql; protected List Selectors = new(); - protected AdvancedSqlQueryHandlerBase(string sql, object[] parameters) + protected AdvancedSqlQueryHandlerBase(char placeholder, string sql, object[] parameters) { Sql = sql.TrimStart(); + Placeholder = placeholder; Parameters = parameters; } @@ -135,7 +137,7 @@ public void ConfigureCommand(ICommandBuilder builder, IMartenSession session) } else { - var cmdParameters = builder.AppendWithParameters(Sql); + var cmdParameters = builder.AppendWithParameters(Sql, Placeholder); if (cmdParameters.Length != Parameters.Length) { throw new InvalidOperationException("Wrong number of supplied parameters"); diff --git a/src/Marten/Linq/QueryHandlers/UserSuppliedQueryHandler.cs b/src/Marten/Linq/QueryHandlers/UserSuppliedQueryHandler.cs index 65d6c99c8d..66aeb232dd 100644 --- a/src/Marten/Linq/QueryHandlers/UserSuppliedQueryHandler.cs +++ b/src/Marten/Linq/QueryHandlers/UserSuppliedQueryHandler.cs @@ -18,14 +18,16 @@ namespace Marten.Linq.QueryHandlers; internal class UserSuppliedQueryHandler: IQueryHandler> { + private readonly char _placeholder; private readonly object[] _parameters; private readonly ISelectClause _selectClause; private readonly ISelector _selector; private readonly string _sql; - public UserSuppliedQueryHandler(IMartenSession session, string sql, object[] parameters) + public UserSuppliedQueryHandler(IMartenSession session, char placeholder, string sql, object[] parameters) { _sql = sql.TrimStart(); + _placeholder = placeholder; _parameters = parameters; SqlContainsCustomSelect = _sql.StartsWith("select", StringComparison.OrdinalIgnoreCase) || IsWithFollowedBySelect(_sql); @@ -63,7 +65,7 @@ public void ConfigureCommand(ICommandBuilder builder, IMartenSession session) } else { - var cmdParameters = builder.AppendWithParameters(_sql); + var cmdParameters = builder.AppendWithParameters(_sql, _placeholder); if (cmdParameters.Length != _parameters.Length) { throw new InvalidOperationException("Wrong number of supplied parameters"); diff --git a/src/Marten/Services/BatchQuerying/BatchedQuery.cs b/src/Marten/Services/BatchQuerying/BatchedQuery.cs index f87b44f9c5..d2aae7ea83 100644 --- a/src/Marten/Services/BatchQuerying/BatchedQuery.cs +++ b/src/Marten/Services/BatchQuerying/BatchedQuery.cs @@ -59,7 +59,12 @@ public IBatchLoadByKeys LoadMany() where TDoc : class public Task> Query(string sql, params object[] parameters) where T : class { - var handler = new UserSuppliedQueryHandler(Parent, sql, parameters); + return Query(QuerySession.DefaultParameterPlaceholder, sql, parameters); + } + + public Task> Query(char placeholder, string sql, params object[] parameters) where T : class + { + var handler = new UserSuppliedQueryHandler(Parent, placeholder, sql, parameters); if (!handler.SqlContainsCustomSelect) { _documentTypes.Add(typeof(T)); diff --git a/src/Marten/Services/BatchQuerying/IBatchedQuery.cs b/src/Marten/Services/BatchQuerying/IBatchedQuery.cs index ed18b9e5c2..6b1cbb4250 100644 --- a/src/Marten/Services/BatchQuerying/IBatchedQuery.cs +++ b/src/Marten/Services/BatchQuerying/IBatchedQuery.cs @@ -113,6 +113,17 @@ public interface IBatchedQuery /// Task> Query(string sql, params object[] parameters) where T : class; + /// + /// Execute a user provided query against "T". + /// Use to specify a character that will be replaced by positional parameters. + /// + /// + /// + /// + /// + /// + Task> Query(char placeholder, string sql, params object[] parameters) where T : class; + /// /// Execute this batched query /// From 5e5b08599a405d96b1a5a9bac36ae6f8c6310d8e Mon Sep 17 00:00:00 2001 From: Ben Edwards Date: Sat, 11 Jan 2025 18:50:03 +1100 Subject: [PATCH 2/4] fix ambiguous calls to QueueSqlCommand --- .../Projections/Flattened/CallUpsertFunctionFrame.cs | 6 +++++- .../Events/Projections/Flattened/DeleteRowFrame.cs | 6 +++++- src/Marten/Internal/Sessions/QuerySession.Json.cs | 9 +++++---- 3 files changed, 15 insertions(+), 6 deletions(-) diff --git a/src/Marten/Events/Projections/Flattened/CallUpsertFunctionFrame.cs b/src/Marten/Events/Projections/Flattened/CallUpsertFunctionFrame.cs index af6fdf4769..ec60c856ec 100644 --- a/src/Marten/Events/Projections/Flattened/CallUpsertFunctionFrame.cs +++ b/src/Marten/Events/Projections/Flattened/CallUpsertFunctionFrame.cs @@ -16,8 +16,12 @@ internal class CallUpsertFunctionFrame: MethodCall, IEventHandlingFrame private readonly DbObjectName _functionIdentifier; private readonly MemberInfo[] _members; + private static readonly MethodInfo QueueSqlMethod = + typeof(IDocumentOperations).GetMethod(nameof(IDocumentOperations.QueueSqlCommand), + [typeof(string), typeof(object[])])!; + public CallUpsertFunctionFrame(Type eventType, DbObjectName functionIdentifier, List columnMaps, - MemberInfo[] members): base(typeof(IDocumentOperations), nameof(IDocumentOperations.QueueSqlCommand)) + MemberInfo[] members): base(typeof(IDocumentOperations), QueueSqlMethod) { _functionIdentifier = functionIdentifier ?? throw new ArgumentNullException(nameof(functionIdentifier)); _columnMaps = columnMaps; diff --git a/src/Marten/Events/Projections/Flattened/DeleteRowFrame.cs b/src/Marten/Events/Projections/Flattened/DeleteRowFrame.cs index 7e09c4cfe8..5608107e3d 100644 --- a/src/Marten/Events/Projections/Flattened/DeleteRowFrame.cs +++ b/src/Marten/Events/Projections/Flattened/DeleteRowFrame.cs @@ -19,8 +19,12 @@ internal class DeleteRowFrame: MethodCall, IEventHandlingFrame private readonly Table _table; private Variable? _event; + private static readonly MethodInfo QueueSqlMethod = + typeof(IDocumentOperations).GetMethod(nameof(IDocumentOperations.QueueSqlCommand), + [typeof(string), typeof(object[])])!; + public DeleteRowFrame(Table table, Type eventType, MemberInfo[] members): base(typeof(IDocumentOperations), - nameof(IDocumentOperations.QueueSqlCommand)) + QueueSqlMethod) { if (!members.Any()) { diff --git a/src/Marten/Internal/Sessions/QuerySession.Json.cs b/src/Marten/Internal/Sessions/QuerySession.Json.cs index 3b7e62e911..67681b10d9 100644 --- a/src/Marten/Internal/Sessions/QuerySession.Json.cs +++ b/src/Marten/Internal/Sessions/QuerySession.Json.cs @@ -53,6 +53,11 @@ public async Task ToJsonMany(ICompiledQuery quer return await stream.ReadAllTextAsync().ConfigureAwait(false); } + public Task StreamJson(Stream destination, CancellationToken token, string sql, params object[] parameters) + { + return StreamJson(destination, token, DefaultParameterPlaceholder, sql, parameters); + } + public Task StreamJson(Stream destination, CancellationToken token, char placeholder, string sql, params object[] parameters) { assertNotDisposed(); @@ -61,10 +66,6 @@ public Task StreamJson(Stream destination, CancellationToken token, char handler.ConfigureCommand(builder, this); return StreamMany(builder.Compile(), destination, token); } - public Task StreamJson(Stream destination, CancellationToken token, string sql, params object[] parameters) - { - return StreamJson(destination, token, DefaultParameterPlaceholder, sql, parameters); - } public Task StreamJson(Stream destination, string sql, params object[] parameters) { From 461b8e22368062daf39f0b074c394df37c88408d Mon Sep 17 00:00:00 2001 From: Ben Edwards Date: Mon, 13 Jan 2025 22:13:55 +1100 Subject: [PATCH 3/4] Add documentation for custom parameter placeholders --- docs/documents/execute-custom-sql.md | 8 ++++-- docs/documents/querying/advanced-sql.md | 15 +++++++++- docs/documents/querying/linq/sql.md | 15 ++++++---- docs/documents/querying/sql.md | 8 ++++-- ...ng_arbitrary_sql_as_part_of_transaction.cs | 2 ++ .../Reading/advanced_sql_query.cs | 28 +++++++++++++++++++ ...Bug_3087_using_JsonPath_with_MatchesSql.cs | 5 ++++ src/Marten.Testing/Examples/QueryBySql.cs | 8 ++++-- 8 files changed, 76 insertions(+), 13 deletions(-) diff --git a/docs/documents/execute-custom-sql.md b/docs/documents/execute-custom-sql.md index c18f2f1e40..b40ae538fe 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. If the `?` character is not suitable as a placeholder because you need to use `?` in your sql query, you can change the placeholder by providing an alternative. Pass this in before the sql argument. - + ```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); +// Use ^ as the parameter placeholder +theSession.QueueSqlCommand('^', "insert into data (raw_value) values (^::jsonb)", json); ``` -snippet source | anchor +snippet source | anchor diff --git a/docs/documents/querying/advanced-sql.md b/docs/documents/querying/advanced-sql.md index b053f78cf2..e48295f272 100644 --- a/docs/documents/querying/advanced-sql.md +++ b/docs/documents/querying/advanced-sql.md @@ -139,6 +139,19 @@ results[1].detail.Detail.ShouldBe("Likes to cook"); snippet source | anchor +All `AdvancedSql` methods also support parameters: + + + +```cs +var schema = theSession.DocumentStore.Options.Schema; + +schema.DatabaseSchemaName.ShouldBe("public"); +schema.EventsSchemaName.ShouldBe("public"); +``` +snippet source | anchor + + For sync queries you can use the `AdvancedSql.Query(...)` overloads. When you need to query for large datasets, the `AdvancedSql.StreamAsync<>(...)` methods can be used. They will return @@ -182,7 +195,7 @@ await foreach (var result in asyncEnumerable) collectedResults.Add(result); } ``` -snippet source | anchor +snippet source | anchor Using this you can resolve schemas: diff --git a/docs/documents/querying/linq/sql.md b/docs/documents/querying/linq/sql.md index ce7c0e7129..bac3d6805d 100644 --- a/docs/documents/querying/linq/sql.md +++ b/docs/documents/querying/linq/sql.md @@ -21,16 +21,21 @@ public async Task query_with_matches_sql() 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: +**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. + +Older version of Marten also offer the `MatchesJsonPath()` method which uses the `^` character as a placeholder. This will continue to be supported. - + ```cs var results2 = await theSession + .Query().Where(x => x.MatchesSql('^', "d.data @? '$ ? (@.Children[*] == null || @.Children[*].size() == 0)'")) + .ToListAsync(); + +// older approach that only supports the ^ placeholder +var results3 = 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..619168afb4 100644 --- a/docs/documents/querying/sql.md +++ b/docs/documents/querying/sql.md @@ -25,8 +25,12 @@ Or with parameterized SQL: ```cs var millers = session .Query("where data ->> 'LastName' = ?", "Miller"); + +// custom placeholder parameter +var millers2 = await session + .QueryAsync('$', "where data ->> 'LastName' = $", "Miller"); ``` -snippet source | anchor +snippet source | anchor And finally asynchronously: @@ -37,7 +41,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 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..027f3f714e 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); + // Use ^ as the parameter placeholder + theSession.QueueSqlCommand('^', "insert into data (raw_value) values (^::jsonb)", json); #endregion await theSession.SaveChangesAsync(); diff --git a/src/DocumentDbTests/Reading/advanced_sql_query.cs b/src/DocumentDbTests/Reading/advanced_sql_query.cs index d9dc651cc1..fbd85ae952 100644 --- a/src/DocumentDbTests/Reading/advanced_sql_query.cs +++ b/src/DocumentDbTests/Reading/advanced_sql_query.cs @@ -138,6 +138,34 @@ limit 2 #endregion } + [Fact] + public async Task can_query_with_parameters() + { + await using var session = theStore.LightweightSession(); + session.Store(new DocWithMeta { Id = 1, Name = "Max" }); + await session.SaveChangesAsync(); + + #region sample_advanced_sql_query_parameters + var schema = session.DocumentStore.Options.Schema; + + var name = (await session.AdvancedSql.QueryAsync( + $"select data ->> ? from {schema.For()} limit 1", + CancellationToken.None, + "Name")).First(); + + // Use ^ as the parameter placeholder + var name2 = (await session.AdvancedSql.QueryAsync( + '^', + $"select data ->> ^ from {schema.For()} limit 1", + CancellationToken.None, + "Name")).First(); + + #endregion + + name.ShouldBe("Max"); + name2.ShouldBe("Max"); + } + [Fact] public async Task can_async_stream_multiple_documents_and_scalar() { diff --git a/src/LinqTests/Bugs/Bug_3087_using_JsonPath_with_MatchesSql.cs b/src/LinqTests/Bugs/Bug_3087_using_JsonPath_with_MatchesSql.cs index 3d3321d269..4d655d9c3c 100644 --- a/src/LinqTests/Bugs/Bug_3087_using_JsonPath_with_MatchesSql.cs +++ b/src/LinqTests/Bugs/Bug_3087_using_JsonPath_with_MatchesSql.cs @@ -28,6 +28,11 @@ public async Task can_use_json_path_operations() #region sample_using_MatchesJsonPath var results2 = await theSession + .Query().Where(x => x.MatchesSql('^', "d.data @? '$ ? (@.Children[*] == null || @.Children[*].size() == 0)'")) + .ToListAsync(); + + // older approach that only supports the ^ placeholder + var results3 = await theSession .Query().Where(x => x.MatchesJsonPath("d.data @? '$ ? (@.Children[*] == null || @.Children[*].size() == 0)'")) .ToListAsync(); diff --git a/src/Marten.Testing/Examples/QueryBySql.cs b/src/Marten.Testing/Examples/QueryBySql.cs index 3543521137..8e8863a826 100644 --- a/src/Marten.Testing/Examples/QueryBySql.cs +++ b/src/Marten.Testing/Examples/QueryBySql.cs @@ -15,13 +15,17 @@ public void QueryForWholeDocumentByWhereClause(IQuerySession session) #endregion } - public void QueryWithParameters(IQuerySession session) + public async Task QueryWithParameters(IQuerySession session) { #region sample_query_with_sql_and_parameters var millers = session .Query("where data ->> 'LastName' = ?", "Miller"); + // custom placeholder parameter + var millers2 = await session + .QueryAsync('$', "where data ->> 'LastName' = $", "Miller"); + #endregion } @@ -35,4 +39,4 @@ public async Task QueryAsynchronously(IQuerySession session) #endregion } -} \ No newline at end of file +} From 61f4fa013b7e62d3332c33fe411033a94c695d8a Mon Sep 17 00:00:00 2001 From: Ben Edwards Date: Thu, 23 Jan 2025 16:38:43 +1100 Subject: [PATCH 4/4] bug fixes and test coverage --- docs/documents/querying/linq/sql.md | 2 +- docs/documents/querying/sql.md | 4 +- src/DocumentDbTests/Reading/query_by_sql.cs | 84 ++++++++++++++++++- .../Linq/MatchesSql/MatchesSqlParser.cs | 6 +- 4 files changed, 89 insertions(+), 7 deletions(-) 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()); }