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 ///