From 3c4ae7d74a18ec313cffdf14c0338d47695289c5 Mon Sep 17 00:00:00 2001 From: Marc Gravell Date: Wed, 13 Sep 2023 21:52:03 +0100 Subject: [PATCH 1/7] release notes 2.1.4 --- docs/index.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/index.md b/docs/index.md index 15d68faff..9ebb9efbb 100644 --- a/docs/index.md +++ b/docs/index.md @@ -24,6 +24,8 @@ Note: to get the latest pre-release build, add ` -Pre` to the end of the command (note: new PRs will not be merged until they add release note wording here) +### 2.1.4 + - add untyped `GridReader.ReadUnbufferedAsync` API (#1958 via @mgravell) - tweak NRT annotations on type-handler API (#1960 via @mgravell, fixes #1959) From 47ef1e7100b7ecdb68bcf97432b9674373a6f75c Mon Sep 17 00:00:00 2001 From: Marc Gravell Date: Thu, 14 Sep 2023 06:25:35 +0100 Subject: [PATCH 2/7] missed an object? type-handler --- Dapper/DataTableHandler.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Dapper/DataTableHandler.cs b/Dapper/DataTableHandler.cs index 3e4bab2c1..df4dc07a5 100644 --- a/Dapper/DataTableHandler.cs +++ b/Dapper/DataTableHandler.cs @@ -4,12 +4,12 @@ namespace Dapper { internal sealed class DataTableHandler : SqlMapper.ITypeHandler { - public object Parse(Type destinationType, object? value) + public object Parse(Type destinationType, object value) { throw new NotImplementedException(); } - public void SetValue(IDbDataParameter parameter, object? value) + public void SetValue(IDbDataParameter parameter, object value) { TableValuedParameter.Set(parameter, value as DataTable, null); } From a37b151bb745ed0763441aa50f29c504221aa7d3 Mon Sep 17 00:00:00 2001 From: Marc Gravell Date: Tue, 10 Oct 2023 15:06:21 +0100 Subject: [PATCH 3/7] auto-detect stored procedures as anything without whitespace (#1975) * 1. auto-detect stored procedures as anything without whitespace 2. we have public fields? (hangs head in shame) * release notes * no need to use regex --- Dapper/CommandDefinition.cs | 22 ++++++++-- Dapper/DynamicParameters.cs | 2 +- Dapper/PublicAPI.Shipped.txt | 5 +++ Dapper/SqlMapper.Async.cs | 16 +++---- Dapper/SqlMapper.Identity.cs | 62 +++++++++++++++++++++++++++- Dapper/SqlMapper.cs | 48 ++++++++++----------- docs/index.md | 2 + tests/Dapper.Tests/ProcedureTests.cs | 20 +++++++++ 8 files changed, 140 insertions(+), 37 deletions(-) diff --git a/Dapper/CommandDefinition.cs b/Dapper/CommandDefinition.cs index 6112353e7..1820d86f4 100644 --- a/Dapper/CommandDefinition.cs +++ b/Dapper/CommandDefinition.cs @@ -48,10 +48,15 @@ internal void OnCompleted() /// public int? CommandTimeout { get; } + internal readonly CommandType CommandTypeDirect; + /// /// The type of command that the command-text represents /// - public CommandType? CommandType { get; } +#if DEBUG // prevent use in our own code + [Obsolete("Prefer " + nameof(CommandTypeDirect), true)] +#endif + public CommandType? CommandType => CommandTypeDirect; /// /// Should data be buffered before returning? @@ -92,11 +97,21 @@ public CommandDefinition(string commandText, object? parameters = null, IDbTrans Parameters = parameters; Transaction = transaction; CommandTimeout = commandTimeout; - CommandType = commandType; + CommandTypeDirect = commandType ?? InferCommandType(commandText); Flags = flags; CancellationToken = cancellationToken; + + static CommandType InferCommandType(string sql) + { + if (sql is null || sql.IndexOfAny(WhitespaceChars) >= 0) return System.Data.CommandType.Text; + return System.Data.CommandType.StoredProcedure; + } } + // if the sql contains any whitespace character (space/tab/cr/lf): interpret as ad-hoc; but "SomeName" should be treated as a stored-proc + // (note TableDirect would need to be specified explicitly, but in reality providers don't usually support TableDirect anyway) + private static readonly char[] WhitespaceChars = new char[] { ' ', '\t', '\r', '\n' }; + private CommandDefinition(object? parameters) : this() { Parameters = parameters; @@ -124,8 +139,7 @@ internal IDbCommand SetupCommand(IDbConnection cnn, Action? { cmd.CommandTimeout = SqlMapper.Settings.CommandTimeout.Value; } - if (CommandType.HasValue) - cmd.CommandType = CommandType.Value; + cmd.CommandType = CommandTypeDirect; paramReader?.Invoke(cmd, Parameters); return cmd; } diff --git a/Dapper/DynamicParameters.cs b/Dapper/DynamicParameters.cs index d4759a37b..f6708b5ce 100644 --- a/Dapper/DynamicParameters.cs +++ b/Dapper/DynamicParameters.cs @@ -168,7 +168,7 @@ internal static bool ShouldSetDbType(DbType dbType) /// Information about the query protected void AddParameters(IDbCommand command, SqlMapper.Identity identity) { - var literals = SqlMapper.GetLiteralTokens(identity.sql); + var literals = SqlMapper.GetLiteralTokens(identity.Sql); if (templates is not null) { diff --git a/Dapper/PublicAPI.Shipped.txt b/Dapper/PublicAPI.Shipped.txt index 36cdc1c78..39a8881e4 100644 --- a/Dapper/PublicAPI.Shipped.txt +++ b/Dapper/PublicAPI.Shipped.txt @@ -152,12 +152,17 @@ override Dapper.SqlMapper.Identity.ToString() -> string! override Dapper.SqlMapper.StringTypeHandler.Parse(object! value) -> T override Dapper.SqlMapper.StringTypeHandler.SetValue(System.Data.IDbDataParameter! parameter, T? value) -> void readonly Dapper.SqlMapper.Identity.commandType -> System.Data.CommandType? +Dapper.SqlMapper.Identity.CommandType.get -> System.Data.CommandType? readonly Dapper.SqlMapper.Identity.connectionString -> string! readonly Dapper.SqlMapper.Identity.gridIndex -> int +Dapper.SqlMapper.Identity.GridIndex.get -> int readonly Dapper.SqlMapper.Identity.hashCode -> int readonly Dapper.SqlMapper.Identity.parametersType -> System.Type? +Dapper.SqlMapper.Identity.ParametersType.get -> System.Type? readonly Dapper.SqlMapper.Identity.sql -> string! +Dapper.SqlMapper.Identity.Sql.get -> string! readonly Dapper.SqlMapper.Identity.type -> System.Type? +Dapper.SqlMapper.Identity.Type.get -> System.Type? static Dapper.DbString.IsAnsiDefault.get -> bool static Dapper.DbString.IsAnsiDefault.set -> void static Dapper.DefaultTypeMap.MatchNamesWithUnderscores.get -> bool diff --git a/Dapper/SqlMapper.Async.cs b/Dapper/SqlMapper.Async.cs index 07f073ee5..6fb8ca4c2 100644 --- a/Dapper/SqlMapper.Async.cs +++ b/Dapper/SqlMapper.Async.cs @@ -422,7 +422,7 @@ private static DbCommand TrySetupAsyncCommand(this CommandDefinition command, ID private static async Task> QueryAsync(this IDbConnection cnn, Type effectiveType, CommandDefinition command) { object? param = command.Parameters; - var identity = new Identity(command.CommandText, command.CommandType, cnn, effectiveType, param?.GetType()); + var identity = new Identity(command.CommandText, command.CommandTypeDirect, cnn, effectiveType, param?.GetType()); var info = GetCacheInfo(identity, param, command.AddToCache); bool wasClosed = cnn.State == ConnectionState.Closed; var cancel = command.CancellationToken; @@ -477,7 +477,7 @@ private static async Task> QueryAsync(this IDbConnection cnn, private static async Task QueryRowAsync(this IDbConnection cnn, Row row, Type effectiveType, CommandDefinition command) { object? param = command.Parameters; - var identity = new Identity(command.CommandText, command.CommandType, cnn, effectiveType, param?.GetType()); + var identity = new Identity(command.CommandText, command.CommandTypeDirect, cnn, effectiveType, param?.GetType()); var info = GetCacheInfo(identity, param, command.AddToCache); bool wasClosed = cnn.State == ConnectionState.Closed; var cancel = command.CancellationToken; @@ -652,7 +652,7 @@ private static async Task ExecuteMultiImplAsync(IDbConnection cnn, CommandD private static async Task ExecuteImplAsync(IDbConnection cnn, CommandDefinition command, object? param) { - var identity = new Identity(command.CommandText, command.CommandType, cnn, null, param?.GetType()); + var identity = new Identity(command.CommandText, command.CommandTypeDirect, cnn, null, param?.GetType()); var info = GetCacheInfo(identity, param, command.AddToCache); bool wasClosed = cnn.State == ConnectionState.Closed; using var cmd = command.TrySetupAsyncCommand(cnn, info.ParamReader); @@ -930,7 +930,7 @@ public static Task> QueryAsync> MultiMapAsync(this IDbConnection cnn, CommandDefinition command, Delegate map, string splitOn) { object? param = command.Parameters; - var identity = new Identity(command.CommandText, command.CommandType, cnn, typeof(TFirst), param?.GetType()); + var identity = new Identity(command.CommandText, command.CommandTypeDirect, cnn, typeof(TFirst), param?.GetType()); var info = GetCacheInfo(identity, param, command.AddToCache); bool wasClosed = cnn.State == ConnectionState.Closed; try @@ -979,7 +979,7 @@ private static async Task> MultiMapAsync(this IDbC } object? param = command.Parameters; - var identity = new IdentityWithTypes(command.CommandText, command.CommandType, cnn, types[0], param?.GetType(), types); + var identity = new IdentityWithTypes(command.CommandText, command.CommandTypeDirect, cnn, types[0], param?.GetType(), types); var info = GetCacheInfo(identity, param, command.AddToCache); bool wasClosed = cnn.State == ConnectionState.Closed; try @@ -1029,7 +1029,7 @@ public static Task QueryMultipleAsync(this IDbConnection cnn, string public static async Task QueryMultipleAsync(this IDbConnection cnn, CommandDefinition command) { object? param = command.Parameters; - var identity = new Identity(command.CommandText, command.CommandType, cnn, typeof(GridReader), param?.GetType()); + var identity = new Identity(command.CommandText, command.CommandTypeDirect, cnn, typeof(GridReader), param?.GetType()); CacheInfo info = GetCacheInfo(identity, param, command.AddToCache); DbCommand? cmd = null; @@ -1227,7 +1227,7 @@ private static async Task ExecuteWrappedReaderImplAsync(IDbConnect object? param = command.Parameters; if (param is not null) { - var identity = new Identity(command.CommandText, command.CommandType, cnn, null, param.GetType()); + var identity = new Identity(command.CommandText, command.CommandTypeDirect, cnn, null, param.GetType()); paramReader = GetCacheInfo(identity, command.Parameters, command.AddToCache).ParamReader; } @@ -1296,7 +1296,7 @@ static async IAsyncEnumerable Impl(IDbConnection cnn, Type effectiveType, Com [EnumeratorCancellation] CancellationToken cancel) { object? param = command.Parameters; - var identity = new Identity(command.CommandText, command.CommandType, cnn, effectiveType, param?.GetType()); + var identity = new Identity(command.CommandText, command.CommandTypeDirect, cnn, effectiveType, param?.GetType()); var info = GetCacheInfo(identity, param, command.AddToCache); bool wasClosed = cnn.State == ConnectionState.Closed; using var cmd = command.TrySetupAsyncCommand(cnn, info.ParamReader); diff --git a/Dapper/SqlMapper.Identity.cs b/Dapper/SqlMapper.Identity.cs index cfaab8e5f..4871d9c05 100644 --- a/Dapper/SqlMapper.Identity.cs +++ b/Dapper/SqlMapper.Identity.cs @@ -1,4 +1,5 @@ using System; +using System.ComponentModel; using System.Data; using System.Runtime.CompilerServices; @@ -92,6 +93,7 @@ public class Identity : IEquatable internal virtual Type GetType(int index) => throw new IndexOutOfRangeException(nameof(index)); +#pragma warning disable CS0618 // Type or member is obsolete internal Identity ForGrid(Type primaryType, int gridIndex) => new Identity(sql, commandType, connectionString, primaryType, parametersType, gridIndex); @@ -110,12 +112,14 @@ internal Identity ForGrid(Type primaryType, Type[] otherTypes, int gridIndex) => /// public Identity ForDynamicParameters(Type type) => new Identity(sql, commandType, connectionString, this.type, type, 0, -1); +#pragma warning restore CS0618 // Type or member is obsolete internal Identity(string sql, CommandType? commandType, IDbConnection connection, Type? type, Type? parametersType) : this(sql, commandType, connection.ConnectionString, type, parametersType, 0, 0) { /* base call */ } private protected Identity(string sql, CommandType? commandType, string connectionString, Type? type, Type? parametersType, int otherTypesHash, int gridIndex) { +#pragma warning disable CS0618 // Type or member is obsolete this.sql = sql; this.commandType = commandType; this.connectionString = connectionString; @@ -133,6 +137,7 @@ private protected Identity(string sql, CommandType? commandType, string connecti hashCode = (hashCode * 23) + (connectionString is null ? 0 : connectionStringComparer.GetHashCode(connectionString)); hashCode = (hashCode * 23) + (parametersType?.GetHashCode() ?? 0); } +#pragma warning restore CS0618 // Type or member is obsolete } /// @@ -144,48 +149,101 @@ private protected Identity(string sql, CommandType? commandType, string connecti /// /// The raw SQL command. /// + [Browsable(false), EditorBrowsable(EditorBrowsableState.Never)] + [Obsolete("Please use " + nameof(Sql) + ". This API may be removed at a later date.")] public readonly string sql; + /// + /// The raw SQL command. + /// +#pragma warning disable CS0618 // Type or member is obsolete + public string Sql => sql; +#pragma warning restore CS0618 // Type or member is obsolete + /// /// The SQL command type. /// + [Browsable(false), EditorBrowsable(EditorBrowsableState.Never)] + [Obsolete("Please use " + nameof(CommandType) + ". This API may be removed at a later date.")] public readonly CommandType? commandType; + /// + /// The SQL command type. + /// +#pragma warning disable CS0618 // Type or member is obsolete + public CommandType? CommandType => commandType; +#pragma warning restore CS0618 // Type or member is obsolete + /// /// The hash code of this Identity. /// + [Browsable(false), EditorBrowsable(EditorBrowsableState.Never)] + [Obsolete("Please use " + nameof(GetHashCode) + ". This API may be removed at a later date.")] public readonly int hashCode; /// /// The grid index (position in the reader) of this Identity. /// + [Browsable(false), EditorBrowsable(EditorBrowsableState.Never)] + [Obsolete("Please use " + nameof(GridIndex) + ". This API may be removed at a later date.")] public readonly int gridIndex; /// - /// This of this Identity. + /// The grid index (position in the reader) of this Identity. /// +#pragma warning disable CS0618 // Type or member is obsolete + public int GridIndex => gridIndex; +#pragma warning restore CS0618 // Type or member is obsolete + + /// + /// The of this Identity. + /// + [Browsable(false), EditorBrowsable(EditorBrowsableState.Never)] + [Obsolete("Please use " + nameof(Type) + ". This API may be removed at a later date.")] public readonly Type? type; + /// + /// The of this Identity. + /// +#pragma warning disable CS0618 // Type or member is obsolete + public Type? Type => type; +#pragma warning restore CS0618 // Type or member is obsolete + /// /// The connection string for this Identity. /// + [Browsable(false), EditorBrowsable(EditorBrowsableState.Never)] + [Obsolete("This API may be removed at a later date.")] public readonly string connectionString; /// /// The type of the parameters object for this Identity. /// + [Browsable(false), EditorBrowsable(EditorBrowsableState.Never)] + [Obsolete("Please use " + nameof(ParametersType) + ". This API may be removed at a later date.")] public readonly Type? parametersType; + /// + /// The type of the parameters object for this Identity. + /// +#pragma warning disable CS0618 // Type or member is obsolete + public Type? ParametersType => parametersType; +#pragma warning restore CS0618 // Type or member is obsolete + /// /// Gets the hash code for this identity. /// /// +#pragma warning disable CS0618 // Type or member is obsolete public override int GetHashCode() => hashCode; +#pragma warning restore CS0618 // Type or member is obsolete /// /// See object.ToString() /// +#pragma warning disable CS0618 // Type or member is obsolete public override string ToString() => sql; +#pragma warning restore CS0618 // Type or member is obsolete /// /// Compare 2 Identity objects @@ -198,6 +256,7 @@ public bool Equals(Identity? other) if (other is null) return false; int typeCount; +#pragma warning disable CS0618 // Type or member is obsolete return gridIndex == other.gridIndex && type == other.type && sql == other.sql @@ -206,6 +265,7 @@ public bool Equals(Identity? other) && parametersType == other.parametersType && (typeCount = TypeCount) == other.TypeCount && (typeCount == 0 || TypesEqual(this, other, typeCount)); +#pragma warning restore CS0618 // Type or member is obsolete } [MethodImpl(MethodImplOptions.NoInlining)] diff --git a/Dapper/SqlMapper.cs b/Dapper/SqlMapper.cs index 5f5974f75..b9f8b09d8 100644 --- a/Dapper/SqlMapper.cs +++ b/Dapper/SqlMapper.cs @@ -115,7 +115,7 @@ private static void PurgeQueryCacheByType(Type type) { foreach (var entry in _queryCache) { - if (entry.Key.type == type) + if (entry.Key.Type == type) _queryCache.TryRemove(entry.Key, out _); } TypeDeserializerCache.Purge(type); @@ -137,7 +137,9 @@ public static int GetCachedSQLCount() /// public static IEnumerable> GetCachedSQL(int ignoreHitCountAbove = int.MaxValue) { - var data = _queryCache.Select(pair => Tuple.Create(pair.Key.connectionString, pair.Key.sql, pair.Value.GetHitCount())); +#pragma warning disable CS0618 // Type or member is obsolete + var data = _queryCache.Select(pair => Tuple.Create(pair.Key.connectionString, pair.Key.Sql, pair.Value.GetHitCount())); +#pragma warning restore CS0618 // Type or member is obsolete return (ignoreHitCountAbove < int.MaxValue) ? data.Where(tuple => tuple.Item3 <= ignoreHitCountAbove) : data; @@ -146,19 +148,19 @@ public static IEnumerable> GetCachedSQL(int ignoreHit /// /// Deep diagnostics only: find any hash collisions in the cache /// - /// - public static IEnumerable> GetHashCollissions() + public static IEnumerable> GetHashCollissions() // legacy incorrect spelling, oops { var counts = new Dictionary(); foreach (var key in _queryCache.Keys) { - if (!counts.TryGetValue(key.hashCode, out int count)) + var hash = key.GetHashCode(); + if (!counts.TryGetValue(hash, out int count)) { - counts.Add(key.hashCode, 1); + counts.Add(hash, 1); } else { - counts[key.hashCode] = count + 1; + counts[hash] = count + 1; } } return from pair in counts @@ -648,7 +650,7 @@ private static int ExecuteImpl(this IDbConnection cnn, ref CommandDefinition com // nice and simple if (param is not null) { - identity = new Identity(command.CommandText, command.CommandType, cnn, null, param.GetType()); + identity = new Identity(command.CommandText, command.CommandTypeDirect, cnn, null, param.GetType()); info = GetCacheInfo(identity, param, command.AddToCache); } return ExecuteCommand(cnn, ref command, param is null ? null : info!.ParamReader); @@ -1108,7 +1110,7 @@ public static GridReader QueryMultiple(this IDbConnection cnn, CommandDefinition private static GridReader QueryMultipleImpl(this IDbConnection cnn, ref CommandDefinition command) { object? param = command.Parameters; - var identity = new Identity(command.CommandText, command.CommandType, cnn, typeof(GridReader), param?.GetType()); + var identity = new Identity(command.CommandText, command.CommandTypeDirect, cnn, typeof(GridReader), param?.GetType()); CacheInfo info = GetCacheInfo(identity, param, command.AddToCache); IDbCommand? cmd = null; @@ -1167,7 +1169,7 @@ private static DbDataReader ExecuteReaderWithFlagsFallback(IDbCommand cmd, bool private static IEnumerable QueryImpl(this IDbConnection cnn, CommandDefinition command, Type effectiveType) { object? param = command.Parameters; - var identity = new Identity(command.CommandText, command.CommandType, cnn, effectiveType, param?.GetType()); + var identity = new Identity(command.CommandText, command.CommandTypeDirect, cnn, effectiveType, param?.GetType()); var info = GetCacheInfo(identity, param, command.AddToCache); IDbCommand? cmd = null; @@ -1260,7 +1262,7 @@ private static void ThrowZeroRows(Row row) private static T QueryRowImpl(IDbConnection cnn, Row row, ref CommandDefinition command, Type effectiveType) { object? param = command.Parameters; - var identity = new Identity(command.CommandText, command.CommandType, cnn, effectiveType, param?.GetType()); + var identity = new Identity(command.CommandText, command.CommandTypeDirect, cnn, effectiveType, param?.GetType()); var info = GetCacheInfo(identity, param, command.AddToCache); IDbCommand? cmd = null; @@ -1549,7 +1551,7 @@ private static IEnumerable MultiMap MultiMapImpl(this IDbConnection? cnn, CommandDefinition command, Delegate map, string splitOn, DbDataReader? reader, Identity? identity, bool finalize) { object? param = command.Parameters; - identity ??= new Identity(command.CommandText, command.CommandType, cnn!, typeof(TFirst), param?.GetType()); + identity ??= new Identity(command.CommandText, command.CommandTypeDirect, cnn!, typeof(TFirst), param?.GetType()); CacheInfo cinfo = GetCacheInfo(identity, param, command.AddToCache); IDbCommand? ownedCommand = null; @@ -1620,7 +1622,7 @@ private static IEnumerable MultiMapImpl(this IDbConnection? cn } object? param = command.Parameters; - identity ??= new IdentityWithTypes(command.CommandText, command.CommandType, cnn!, types[0], param?.GetType(), types); + identity ??= new IdentityWithTypes(command.CommandText, command.CommandTypeDirect, cnn!, types[0], param?.GetType(), types); CacheInfo cinfo = GetCacheInfo(identity, param, command.AddToCache); IDbCommand? ownedCommand = null; @@ -1825,7 +1827,7 @@ private static CacheInfo GetCacheInfo(Identity identity, object? exampleParamete throw new InvalidOperationException("An enumerable sequence of parameters (arrays, lists, etc) is not allowed in this context"); } info = new CacheInfo(); - if (identity.parametersType is not null) + if (identity.ParametersType is not null) { Action reader; if (exampleParameters is IDynamicParameters) @@ -1842,10 +1844,10 @@ private static CacheInfo GetCacheInfo(Identity identity, object? exampleParamete } else { - var literals = GetLiteralTokens(identity.sql); + var literals = GetLiteralTokens(identity.Sql); reader = CreateParamInfoGenerator(identity, false, true, literals); } - if ((identity.commandType is null || identity.commandType == CommandType.Text) && ShouldPassByPosition(identity.sql)) + if ((identity.CommandType is null || identity.CommandType == CommandType.Text) && ShouldPassByPosition(identity.Sql)) { var tail = reader; reader = (cmd, obj) => @@ -2517,7 +2519,7 @@ internal static IList GetLiteralTokens(string sql) /// Whether to check for duplicates. /// Whether to remove unused parameters. public static Action CreateParamInfoGenerator(Identity identity, bool checkForDuplicates, bool removeUnused) => - CreateParamInfoGenerator(identity, checkForDuplicates, removeUnused, GetLiteralTokens(identity.sql)); + CreateParamInfoGenerator(identity, checkForDuplicates, removeUnused, GetLiteralTokens(identity.Sql)); private static bool IsValueTuple(Type? type) => (type?.IsValueType == true && type.FullName?.StartsWith("System.ValueTuple`", StringComparison.Ordinal) == true) @@ -2525,7 +2527,7 @@ private static bool IsValueTuple(Type? type) => (type?.IsValueType == true internal static Action CreateParamInfoGenerator(Identity identity, bool checkForDuplicates, bool removeUnused, IList literals) { - Type type = identity.parametersType!; + Type type = identity.ParametersType!; if (IsValueTuple(type)) { @@ -2533,9 +2535,9 @@ private static bool IsValueTuple(Type? type) => (type?.IsValueType == true } bool filterParams = false; - if (removeUnused && identity.commandType.GetValueOrDefault(CommandType.Text) == CommandType.Text) + if (removeUnused && identity.CommandType.GetValueOrDefault(CommandType.Text) == CommandType.Text) { - filterParams = !smellsLikeOleDb.IsMatch(identity.sql); + filterParams = !smellsLikeOleDb.IsMatch(identity.Sql); } var dm = new DynamicMethod("ParamInfo" + Guid.NewGuid().ToString(), null, new[] { typeof(IDbCommand), typeof(object) }, type, true); @@ -2628,7 +2630,7 @@ private static bool IsValueTuple(Type? type) => (type?.IsValueType == true } if (filterParams) { - props = FilterParameters(props, identity.sql); + props = FilterParameters(props, identity.Sql); } var callOpCode = isStruct ? OpCodes.Call : OpCodes.Callvirt; @@ -2971,7 +2973,7 @@ private static int ExecuteCommand(IDbConnection cnn, ref CommandDefinition comma object? param = command.Parameters; if (param is not null) { - var identity = new Identity(command.CommandText, command.CommandType, cnn, null, param.GetType()); + var identity = new Identity(command.CommandText, command.CommandTypeDirect, cnn, null, param.GetType()); paramReader = GetCacheInfo(identity, command.Parameters, command.AddToCache).ParamReader; } @@ -3033,7 +3035,7 @@ private static DbDataReader ExecuteReaderImpl(IDbConnection cnn, ref CommandDefi // nice and simple if (param is not null) { - var identity = new Identity(command.CommandText, command.CommandType, cnn, null, param.GetType()); + var identity = new Identity(command.CommandText, command.CommandTypeDirect, cnn, null, param.GetType()); info = GetCacheInfo(identity, param, command.AddToCache); } var paramReader = info?.ParamReader; diff --git a/docs/index.md b/docs/index.md index 9ebb9efbb..25a22caf7 100644 --- a/docs/index.md +++ b/docs/index.md @@ -24,6 +24,8 @@ Note: to get the latest pre-release build, add ` -Pre` to the end of the command (note: new PRs will not be merged until they add release note wording here) +- infer command text without any whitespace as stored-procedure (#1975 via @mgravell) + ### 2.1.4 - add untyped `GridReader.ReadUnbufferedAsync` API (#1958 via @mgravell) diff --git a/tests/Dapper.Tests/ProcedureTests.cs b/tests/Dapper.Tests/ProcedureTests.cs index 1bd563045..9d76f44b9 100644 --- a/tests/Dapper.Tests/ProcedureTests.cs +++ b/tests/Dapper.Tests/ProcedureTests.cs @@ -92,6 +92,26 @@ @ErrorDescription varchar(255) OUTPUT Assert.Equal("Completed successfully", p.Get("ErrorDescription")); } + [Theory] + [InlineData(CommandType.StoredProcedure)] + [InlineData(null)] // auto + public void InferProcedure(CommandType? commandType) + { + connection.Execute("CREATE PROCEDURE #InferProcedure @id int AS BEGIN SELECT -@id END"); + var result = connection.QuerySingle("#InferProcedure", new { id = 42 }, commandType: commandType); + Assert.Equal(-42, result); + } + + [Theory] + [InlineData(CommandType.Text)] + [InlineData(null)] // auto + public void InferNotProcedure(CommandType? commandType) + { + connection.Execute("CREATE PROCEDURE #InferNotProcedure @id int AS BEGIN SELECT -@id END"); + var result = connection.QuerySingle("EXEC #InferNotProcedure @id", new { id = 42 }, commandType: commandType); + Assert.Equal(-42, result); + } + [Fact] public void SO24605346_ProcsAndStrings() { From 2837480903bb8b951de4a4eff4a0c2925d42b2cb Mon Sep 17 00:00:00 2001 From: Giorgi Dalakishvili Date: Wed, 11 Oct 2023 15:41:06 +0400 Subject: [PATCH 4/7] Add DuckDB tests (#1970) * Add DuckDB tests * Update DuckDB.NET.Data.Full version --- Directory.Packages.props | 1 + tests/Dapper.Tests/Dapper.Tests.csproj | 1 + tests/Dapper.Tests/Providers/DuckDBTests.cs | 67 +++++++++++++++++++++ 3 files changed, 69 insertions(+) create mode 100644 tests/Dapper.Tests/Providers/DuckDBTests.cs diff --git a/Directory.Packages.props b/Directory.Packages.props index e8da7cb83..06e68e627 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -1,6 +1,7 @@ + diff --git a/tests/Dapper.Tests/Dapper.Tests.csproj b/tests/Dapper.Tests/Dapper.Tests.csproj index f3007f5cc..f1e808301 100644 --- a/tests/Dapper.Tests/Dapper.Tests.csproj +++ b/tests/Dapper.Tests/Dapper.Tests.csproj @@ -15,6 +15,7 @@ + diff --git a/tests/Dapper.Tests/Providers/DuckDBTests.cs b/tests/Dapper.Tests/Providers/DuckDBTests.cs new file mode 100644 index 000000000..d06d25d6b --- /dev/null +++ b/tests/Dapper.Tests/Providers/DuckDBTests.cs @@ -0,0 +1,67 @@ +using System; +using System.Data.Common; +using DuckDB.NET.Data; +using Xunit; + +namespace Dapper.Tests +{ + public class DuckDBProvider : DatabaseProvider + { + public override DbProviderFactory Factory => DuckDBClientFactory.Instance; + public override string GetConnectionString() => "Data Source=:memory:"; + } + + public abstract class DuckDBTypeTestBase : TestBase + { + protected DuckDBConnection GetDuckDBConnection(bool open = true) + => (DuckDBConnection)(open ? Provider.GetOpenConnection() : Provider.GetClosedConnection()); + + [AttributeUsage(AttributeTargets.Method, AllowMultiple = false)] + public class FactDuckDBAttribute : FactAttribute + { + public override string? Skip + { + get { return unavailable ?? base.Skip; } + set { base.Skip = value; } + } + + private static readonly string? unavailable; + + static FactDuckDBAttribute() + { + try + { + using var _ = DatabaseProvider.Instance.GetOpenConnection(); + } + catch (Exception ex) + { + unavailable = $"DuckDB is unavailable: {ex.Message}"; + } + } + } + } + + public class DuckDBTests : DuckDBTypeTestBase + { + [FactDuckDB] + public void DuckDBNamedParameter() + { + using var connection = GetDuckDBConnection(); + + var result = connection.QueryFirst("Select $foo", new {foo = 42}); + Assert.Equal(42, result); + } + + [FactDuckDB] + public void DuckDBPositionalParameter() + { + using var connection = GetDuckDBConnection(); + + var dp = new DynamicParameters(); + dp.Add("?", 42); + + var result = connection.QueryFirst("Select ?", dp); + Assert.Equal(42, result); + } + } +} From 19193b52bb105ee2935826bd7bf26478b8ac2993 Mon Sep 17 00:00:00 2001 From: Giorgi Dalakishvili Date: Wed, 11 Oct 2023 16:10:00 +0400 Subject: [PATCH 5/7] Add a setting to turn off Ole Db "smell check" (#1974) * Add a setting to turn off Ole Db "smell check" * Rename setting, fix logic * Test for SupportLegacyParameterTokens * Fix exception type * Add release note --- Dapper/PublicAPI.Shipped.txt | 2 ++ Dapper/SqlMapper.Settings.cs | 7 ++++++ Dapper/SqlMapper.cs | 6 +++-- docs/index.md | 1 + tests/Dapper.Tests/ParameterTests.cs | 35 ++++++++++++++++++++++++++++ 5 files changed, 49 insertions(+), 2 deletions(-) diff --git a/Dapper/PublicAPI.Shipped.txt b/Dapper/PublicAPI.Shipped.txt index 39a8881e4..adaa7a517 100644 --- a/Dapper/PublicAPI.Shipped.txt +++ b/Dapper/PublicAPI.Shipped.txt @@ -303,6 +303,8 @@ static Dapper.SqlMapper.SanitizeParameterValue(object? value) -> object! static Dapper.SqlMapper.SetDbType(System.Data.IDataParameter! parameter, object! value) -> void static Dapper.SqlMapper.Settings.ApplyNullValues.get -> bool static Dapper.SqlMapper.Settings.ApplyNullValues.set -> void +static Dapper.SqlMapper.Settings.SupportLegacyParameterTokens.get -> bool +static Dapper.SqlMapper.Settings.SupportLegacyParameterTokens.set -> void static Dapper.SqlMapper.Settings.CommandTimeout.get -> int? static Dapper.SqlMapper.Settings.CommandTimeout.set -> void static Dapper.SqlMapper.Settings.FetchSize.get -> long diff --git a/Dapper/SqlMapper.Settings.cs b/Dapper/SqlMapper.Settings.cs index 192cd893d..343906b74 100644 --- a/Dapper/SqlMapper.Settings.cs +++ b/Dapper/SqlMapper.Settings.cs @@ -121,6 +121,13 @@ public static long FetchSize } } + /// + /// Indicates whether single-character parameter tokens (? etc) will be detected and used where possible; + /// this feature is not recommended and will be disabled by default in future versions; + /// where possible, prefer named parameters (@yourParam etc) or Dapper's "pseudo-positional" parameters (?yourParam? etc). + /// + public static bool SupportLegacyParameterTokens { get; set; } = true; + private static long s_FetchSize = -1; } } diff --git a/Dapper/SqlMapper.cs b/Dapper/SqlMapper.cs index b9f8b09d8..f1e761b23 100644 --- a/Dapper/SqlMapper.cs +++ b/Dapper/SqlMapper.cs @@ -2534,11 +2534,13 @@ private static bool IsValueTuple(Type? type) => (type?.IsValueType == true throw new NotSupportedException("ValueTuple should not be used for parameters - the language-level names are not available to use as parameter names, and it adds unnecessary boxing"); } - bool filterParams = false; - if (removeUnused && identity.CommandType.GetValueOrDefault(CommandType.Text) == CommandType.Text) + bool filterParams = removeUnused && identity.CommandType.GetValueOrDefault(CommandType.Text) == CommandType.Text; + + if (filterParams && Settings.SupportLegacyParameterTokens) { filterParams = !smellsLikeOleDb.IsMatch(identity.Sql); } + var dm = new DynamicMethod("ParamInfo" + Guid.NewGuid().ToString(), null, new[] { typeof(IDbCommand), typeof(object) }, type, true); var il = dm.GetILGenerator(); diff --git a/docs/index.md b/docs/index.md index 25a22caf7..36e69bcd1 100644 --- a/docs/index.md +++ b/docs/index.md @@ -25,6 +25,7 @@ Note: to get the latest pre-release build, add ` -Pre` to the end of the command (note: new PRs will not be merged until they add release note wording here) - infer command text without any whitespace as stored-procedure (#1975 via @mgravell) +- add global `SupportLegacyParameterTokens` setting to enable or disable single-character parameter tokens (#1974 via @Giorgi) ### 2.1.4 diff --git a/tests/Dapper.Tests/ParameterTests.cs b/tests/Dapper.Tests/ParameterTests.cs index cd0d0f0cc..b6408359e 100644 --- a/tests/Dapper.Tests/ParameterTests.cs +++ b/tests/Dapper.Tests/ParameterTests.cs @@ -1511,6 +1511,36 @@ public void Issue601_InternationalParameterNamesWork() [FactLongRunning] public void TestListExpansionPadding_Disabled() => TestListExpansionPadding(false); + [Theory] + [InlineData(true)] + [InlineData(false)] + public void OleDbParamFilterFails(bool legacyParameterToken) + { + SqlMapper.PurgeQueryCache(); + var oldValue = SqlMapper.Settings.SupportLegacyParameterTokens; + try + { + SqlMapper.Settings.SupportLegacyParameterTokens = legacyParameterToken; + + if (legacyParameterToken) // OLE DB parameter support enabled; can false-positive + { + Assert.Throws(() => GetValue(connection)); + } + else // OLE DB parameter support disabled; more reliable + { + Assert.Equal("this ? could be awkward", GetValue(connection)); + } + } + finally + { + SqlMapper.Settings.SupportLegacyParameterTokens = oldValue; + } + + static string GetValue(DbConnection connection) + => connection.QuerySingle("select 'this ? could be awkward'", + new TypeWithDodgyProperties()); + } + private void TestListExpansionPadding(bool enabled) { bool oldVal = SqlMapper.Settings.PadListExpansions; @@ -1706,5 +1736,10 @@ class HazNullableSqlDecimal public int Id { get; set; } public SqlDecimal? Value { get; set; } } + + class TypeWithDodgyProperties + { + public string Name => throw new NotSupportedException(); + } } } From 8d53acb4011c33a590dcb6fdb35ab6c48a2b456e Mon Sep 17 00:00:00 2001 From: Marc Gravell Date: Fri, 13 Oct 2023 07:01:56 +0100 Subject: [PATCH 6/7] the $ support added for DuckDB does not need to exacerbate the OLEDB problem (#1979) --- Dapper/SqlMapper.cs | 2 +- docs/index.md | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/Dapper/SqlMapper.cs b/Dapper/SqlMapper.cs index f1e761b23..797b833d2 100644 --- a/Dapper/SqlMapper.cs +++ b/Dapper/SqlMapper.cs @@ -2387,7 +2387,7 @@ private static IEnumerable FilterParameters(IEnumerable Date: Fri, 13 Oct 2023 07:06:15 +0100 Subject: [PATCH 7/7] NRT tweak on GetConstructorParameter --- Dapper/PublicAPI.Shipped.txt | 2 +- Dapper/SqlMapper.ITypeMap.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Dapper/PublicAPI.Shipped.txt b/Dapper/PublicAPI.Shipped.txt index adaa7a517..4c5417a6c 100644 --- a/Dapper/PublicAPI.Shipped.txt +++ b/Dapper/PublicAPI.Shipped.txt @@ -135,7 +135,7 @@ Dapper.SqlMapper.ITypeHandler.SetValue(System.Data.IDbDataParameter! parameter, Dapper.SqlMapper.ITypeMap Dapper.SqlMapper.ITypeMap.FindConstructor(string![]! names, System.Type![]! types) -> System.Reflection.ConstructorInfo? Dapper.SqlMapper.ITypeMap.FindExplicitConstructor() -> System.Reflection.ConstructorInfo? -Dapper.SqlMapper.ITypeMap.GetConstructorParameter(System.Reflection.ConstructorInfo! constructor, string! columnName) -> Dapper.SqlMapper.IMemberMap! +Dapper.SqlMapper.ITypeMap.GetConstructorParameter(System.Reflection.ConstructorInfo! constructor, string! columnName) -> Dapper.SqlMapper.IMemberMap? Dapper.SqlMapper.ITypeMap.GetMember(string! columnName) -> Dapper.SqlMapper.IMemberMap? Dapper.SqlMapper.Settings Dapper.SqlMapper.StringTypeHandler diff --git a/Dapper/SqlMapper.ITypeMap.cs b/Dapper/SqlMapper.ITypeMap.cs index 6a149b1fb..42189a688 100644 --- a/Dapper/SqlMapper.ITypeMap.cs +++ b/Dapper/SqlMapper.ITypeMap.cs @@ -33,7 +33,7 @@ public interface ITypeMap /// Constructor to resolve /// DataReader column name /// Mapping implementation - IMemberMap GetConstructorParameter(ConstructorInfo constructor, string columnName); + IMemberMap? GetConstructorParameter(ConstructorInfo constructor, string columnName); /// /// Gets member mapping for column