From 16c9384fa2dd1beded80593dbcffcce918af82ee Mon Sep 17 00:00:00 2001 From: Marc Gravell Date: Fri, 20 Oct 2023 09:09:24 +0100 Subject: [PATCH] - expand auto-sproc detection to handle more scenarios and explicit exclusions - add net7 target - use regex generator when available (net7+) - fix #1984 --- Dapper.StrongName/Dapper.StrongName.csproj | 3 +- Dapper/CommandDefinition.cs | 20 ++++----- Dapper/CompiledRegex.cs | 45 +++++++++++++++++++ Dapper/Dapper.csproj | 2 +- Dapper/Global.cs | 4 ++ Dapper/PublicAPI/net7.0/PublicAPI.Shipped.txt | 6 +++ .../PublicAPI/net7.0/PublicAPI.Unshipped.txt | 1 + Dapper/SqlMapper.DapperRow.Descriptor.cs | 18 ++++---- Dapper/SqlMapper.cs | 15 +++---- Directory.Build.props | 2 +- appveyor.yml | 3 ++ tests/Dapper.Tests/Dapper.Tests.csproj | 2 +- tests/Dapper.Tests/ProcedureTests.cs | 22 +++++++++ 13 files changed, 110 insertions(+), 33 deletions(-) create mode 100644 Dapper/CompiledRegex.cs create mode 100644 Dapper/Global.cs create mode 100644 Dapper/PublicAPI/net7.0/PublicAPI.Shipped.txt create mode 100644 Dapper/PublicAPI/net7.0/PublicAPI.Unshipped.txt diff --git a/Dapper.StrongName/Dapper.StrongName.csproj b/Dapper.StrongName/Dapper.StrongName.csproj index dec6f7f0..ef28e32b 100644 --- a/Dapper.StrongName/Dapper.StrongName.csproj +++ b/Dapper.StrongName/Dapper.StrongName.csproj @@ -5,11 +5,12 @@ Dapper (Strong Named) A high performance Micro-ORM supporting SQL Server, MySQL, Sqlite, SqlCE, Firebird etc.. Sam Saffron;Marc Gravell;Nick Craver - net461;netstandard2.0;net5.0 + net461;netstandard2.0;net5.0;net7.0 true true enable true + $(DefineConstants);STRONG_NAME diff --git a/Dapper/CommandDefinition.cs b/Dapper/CommandDefinition.cs index f7132ed5..cc117c30 100644 --- a/Dapper/CommandDefinition.cs +++ b/Dapper/CommandDefinition.cs @@ -2,7 +2,6 @@ using System.Data; using System.Reflection; using System.Reflection.Emit; -using System.Text.RegularExpressions; using System.Threading; namespace Dapper @@ -101,17 +100,18 @@ public CommandDefinition(string commandText, object? parameters = null, IDbTrans CommandTypeDirect = commandType ?? InferCommandType(commandText); Flags = flags; CancellationToken = cancellationToken; - - static CommandType InferCommandType(string sql) - { - if (sql is null || WhitespaceChars.IsMatch(sql)) return System.Data.CommandType.Text; - return System.Data.CommandType.StoredProcedure; - } } - // if the sql contains any whitespace character (space/tab/cr/lf/etc - via unicode): 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 Regex WhitespaceChars = new(@"\s", RegexOptions.Compiled); + internal static CommandType InferCommandType(string sql) + { + // if the sql contains any whitespace character (space/tab/cr/lf/etc - via unicode), + // has operators, comments, semi-colon, or a known exception: interpret as ad-hoc; + // otherwise, simple names like "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) + + if (sql is null || CompiledRegex.WhitespaceOrReserved.IsMatch(sql)) return System.Data.CommandType.Text; + return System.Data.CommandType.StoredProcedure; + } private CommandDefinition(object? parameters) : this() { diff --git a/Dapper/CompiledRegex.cs b/Dapper/CompiledRegex.cs new file mode 100644 index 00000000..bbf857d5 --- /dev/null +++ b/Dapper/CompiledRegex.cs @@ -0,0 +1,45 @@ +using System.Diagnostics.CodeAnalysis; +using System.Text.RegularExpressions; + +namespace Dapper; + +internal static partial class CompiledRegex +{ +#if DEBUG && NET7_0_OR_GREATER // enables colorization in IDE + [StringSyntax("Regex")] +#endif + private const string + WhitespaceOrReservedPattern = @"[\s;/\-+*]|^vacuum$", + LegacyParameterPattern = @"(? LegacyParameterGen(); + internal static Regex LiteralTokens => LiteralTokensGen(); + internal static Regex PseudoPositional => PseudoPositionalGen(); + internal static Regex WhitespaceOrReserved => WhitespaceOrReservedGen(); +#else + internal static Regex LegacyParameter { get; } + = new(LegacyParameterPattern, RegexOptions.IgnoreCase | RegexOptions.Multiline | RegexOptions.CultureInvariant | RegexOptions.Compiled); + internal static Regex LiteralTokens { get; } + = new(LiteralTokensPattern, RegexOptions.IgnoreCase | RegexOptions.Multiline | RegexOptions.CultureInvariant | RegexOptions.Compiled); + internal static Regex PseudoPositional { get; } + = new(PseudoPositionalPattern, RegexOptions.IgnoreCase | RegexOptions.CultureInvariant | RegexOptions.Compiled); + internal static Regex WhitespaceOrReserved { get; } + = new(WhitespaceOrReservedPattern, RegexOptions.Compiled | RegexOptions.IgnoreCase); +#endif +} diff --git a/Dapper/Dapper.csproj b/Dapper/Dapper.csproj index cf98d5ab..8fc9c65f 100644 --- a/Dapper/Dapper.csproj +++ b/Dapper/Dapper.csproj @@ -5,7 +5,7 @@ orm;sql;micro-orm A high performance Micro-ORM supporting SQL Server, MySQL, Sqlite, SqlCE, Firebird etc.. Sam Saffron;Marc Gravell;Nick Craver - net461;netstandard2.0;net5.0 + net461;netstandard2.0;net5.0;net7.0 enable true diff --git a/Dapper/Global.cs b/Dapper/Global.cs new file mode 100644 index 00000000..f6b5f4ef --- /dev/null +++ b/Dapper/Global.cs @@ -0,0 +1,4 @@ +using System.Runtime.CompilerServices; +#if !STRONG_NAME +[assembly: InternalsVisibleTo("Dapper.Tests")] +#endif diff --git a/Dapper/PublicAPI/net7.0/PublicAPI.Shipped.txt b/Dapper/PublicAPI/net7.0/PublicAPI.Shipped.txt new file mode 100644 index 00000000..5da46e60 --- /dev/null +++ b/Dapper/PublicAPI/net7.0/PublicAPI.Shipped.txt @@ -0,0 +1,6 @@ +#nullable enable +Dapper.SqlMapper.GridReader.DisposeAsync() -> System.Threading.Tasks.ValueTask +Dapper.SqlMapper.GridReader.ReadUnbufferedAsync() -> System.Collections.Generic.IAsyncEnumerable! +Dapper.SqlMapper.GridReader.ReadUnbufferedAsync() -> System.Collections.Generic.IAsyncEnumerable! +static Dapper.SqlMapper.QueryUnbufferedAsync(this System.Data.Common.DbConnection! cnn, string! sql, object? param = null, System.Data.Common.DbTransaction? transaction = null, int? commandTimeout = null, System.Data.CommandType? commandType = null) -> System.Collections.Generic.IAsyncEnumerable! +static Dapper.SqlMapper.QueryUnbufferedAsync(this System.Data.Common.DbConnection! cnn, string! sql, object? param = null, System.Data.Common.DbTransaction? transaction = null, int? commandTimeout = null, System.Data.CommandType? commandType = null) -> System.Collections.Generic.IAsyncEnumerable! \ No newline at end of file diff --git a/Dapper/PublicAPI/net7.0/PublicAPI.Unshipped.txt b/Dapper/PublicAPI/net7.0/PublicAPI.Unshipped.txt new file mode 100644 index 00000000..91b0e1a4 --- /dev/null +++ b/Dapper/PublicAPI/net7.0/PublicAPI.Unshipped.txt @@ -0,0 +1 @@ +#nullable enable \ No newline at end of file diff --git a/Dapper/SqlMapper.DapperRow.Descriptor.cs b/Dapper/SqlMapper.DapperRow.Descriptor.cs index 11e85c02..0824c665 100644 --- a/Dapper/SqlMapper.DapperRow.Descriptor.cs +++ b/Dapper/SqlMapper.DapperRow.Descriptor.cs @@ -13,8 +13,8 @@ private sealed class DapperRowTypeDescriptionProvider : TypeDescriptionProvider { public override ICustomTypeDescriptor GetExtendedTypeDescriptor(object instance) => new DapperRowTypeDescriptor(instance); - public override ICustomTypeDescriptor GetTypeDescriptor(Type objectType, object instance) - => new DapperRowTypeDescriptor(instance); + public override ICustomTypeDescriptor GetTypeDescriptor(Type objectType, object? instance) + => new DapperRowTypeDescriptor(instance!); } //// in theory we could implement this for zero-length results to bind; would require @@ -57,7 +57,7 @@ AttributeCollection ICustomTypeDescriptor.GetAttributes() EventDescriptorCollection ICustomTypeDescriptor.GetEvents() => EventDescriptorCollection.Empty; - EventDescriptorCollection ICustomTypeDescriptor.GetEvents(Attribute[] attributes) => EventDescriptorCollection.Empty; + EventDescriptorCollection ICustomTypeDescriptor.GetEvents(Attribute[]? attributes) => EventDescriptorCollection.Empty; internal static PropertyDescriptorCollection GetProperties(DapperRow row) => GetProperties(row?.table, row); internal static PropertyDescriptorCollection GetProperties(DapperTable? table, IDictionary? row = null) @@ -75,9 +75,9 @@ internal static PropertyDescriptorCollection GetProperties(DapperTable? table, I } PropertyDescriptorCollection ICustomTypeDescriptor.GetProperties() => GetProperties(_row); - PropertyDescriptorCollection ICustomTypeDescriptor.GetProperties(Attribute[] attributes) => GetProperties(_row); + PropertyDescriptorCollection ICustomTypeDescriptor.GetProperties(Attribute[]? attributes) => GetProperties(_row); - object ICustomTypeDescriptor.GetPropertyOwner(PropertyDescriptor pd) => _row; + object ICustomTypeDescriptor.GetPropertyOwner(PropertyDescriptor? pd) => _row; } private sealed class RowBoundPropertyDescriptor : PropertyDescriptor @@ -95,10 +95,10 @@ public RowBoundPropertyDescriptor(Type type, string name, int index) : base(name public override bool ShouldSerializeValue(object component) => ((DapperRow)component).TryGetValue(_index, out _); public override Type ComponentType => typeof(DapperRow); public override Type PropertyType => _type; - public override object GetValue(object component) - => ((DapperRow)component).TryGetValue(_index, out var val) ? (val ?? DBNull.Value): DBNull.Value; - public override void SetValue(object component, object? value) - => ((DapperRow)component).SetValue(_index, value is DBNull ? null : value); + public override object GetValue(object? component) + => ((DapperRow)component!).TryGetValue(_index, out var val) ? (val ?? DBNull.Value): DBNull.Value; + public override void SetValue(object? component, object? value) + => ((DapperRow)component!).SetValue(_index, value is DBNull ? null : value); } } } diff --git a/Dapper/SqlMapper.cs b/Dapper/SqlMapper.cs index 797b833d..fb6ac88e 100644 --- a/Dapper/SqlMapper.cs +++ b/Dapper/SqlMapper.cs @@ -1865,7 +1865,7 @@ private static CacheInfo GetCacheInfo(Identity identity, object? exampleParamete private static bool ShouldPassByPosition(string sql) { - return sql?.IndexOf('?') >= 0 && pseudoPositional.IsMatch(sql); + return sql?.IndexOf('?') >= 0 && CompiledRegex.PseudoPositional.IsMatch(sql); } private static void PassByPosition(IDbCommand cmd) @@ -1882,7 +1882,7 @@ private static void PassByPosition(IDbCommand cmd) bool firstMatch = true; int index = 0; // use this to spoof names; in most pseudo-positional cases, the name is ignored, however: // for "snowflake", the name needs to be incremental i.e. "1", "2", "3" - cmd.CommandText = pseudoPositional.Replace(cmd.CommandText, match => + cmd.CommandText = CompiledRegex.PseudoPositional.Replace(cmd.CommandText, match => { string key = match.Groups[1].Value; if (!consumed.Add(key)) @@ -2386,11 +2386,6 @@ private static IEnumerable FilterParameters(IEnumerable /// Replace all literal tokens with their text form. /// @@ -2496,9 +2491,9 @@ internal static void ReplaceLiterals(IParameterLookup parameters, IDbCommand com internal static IList GetLiteralTokens(string sql) { if (string.IsNullOrEmpty(sql)) return LiteralToken.None; - if (!literalTokens.IsMatch(sql)) return LiteralToken.None; + if (!CompiledRegex.LiteralTokens.IsMatch(sql)) return LiteralToken.None; - var matches = literalTokens.Matches(sql); + var matches = CompiledRegex.LiteralTokens.Matches(sql); var found = new HashSet(StringComparer.Ordinal); var list = new List(matches.Count); foreach (Match match in matches) @@ -2538,7 +2533,7 @@ private static bool IsValueTuple(Type? type) => (type?.IsValueType == true if (filterParams && Settings.SupportLegacyParameterTokens) { - filterParams = !smellsLikeOleDb.IsMatch(identity.Sql); + filterParams = !CompiledRegex.LegacyParameter.IsMatch(identity.Sql); } var dm = new DynamicMethod("ParamInfo" + Guid.NewGuid().ToString(), null, new[] { typeof(IDbCommand), typeof(object) }, type, true); diff --git a/Directory.Build.props b/Directory.Build.props index d858cf49..e9ebe949 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -21,7 +21,7 @@ false true true - 9.0 + 11 false true readme.md diff --git a/appveyor.yml b/appveyor.yml index 122b20fa..a39d8778 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -6,6 +6,9 @@ skip_commits: files: - '**/*.md' +install: + - choco install dotnet-sdk --version 7.0.402 + environment: Appveyor: true # Postgres diff --git a/tests/Dapper.Tests/Dapper.Tests.csproj b/tests/Dapper.Tests/Dapper.Tests.csproj index f1e80830..8a003f7b 100644 --- a/tests/Dapper.Tests/Dapper.Tests.csproj +++ b/tests/Dapper.Tests/Dapper.Tests.csproj @@ -2,7 +2,7 @@ Dapper.Tests Dapper Core Test Suite - net472;net6.0 + net472;net6.0;net7.0 $(DefineConstants);MSSQLCLIENT $(NoWarn);IDE0017;IDE0034;IDE0037;IDE0039;IDE0042;IDE0044;IDE0051;IDE0052;IDE0059;IDE0060;IDE0063;IDE1006;xUnit1004;CA1806;CA1816;CA1822;CA1825;CA2208 enable diff --git a/tests/Dapper.Tests/ProcedureTests.cs b/tests/Dapper.Tests/ProcedureTests.cs index 9cc71f47..be8dbe74 100644 --- a/tests/Dapper.Tests/ProcedureTests.cs +++ b/tests/Dapper.Tests/ProcedureTests.cs @@ -317,5 +317,27 @@ public async Task Issue1986_AutoProc_Whitespace(string space) var result = await connection.QuerySingleAsync(sql); Assert.Equal(42, result); } + + [Theory] + [InlineData("foo", CommandType.StoredProcedure)] + [InlineData("foo;", CommandType.Text)] + [InlineData("foo bar", CommandType.Text)] + [InlineData("foo bar;", CommandType.Text)] + [InlineData("vacuum", CommandType.Text)] + [InlineData("vacuum;", CommandType.Text)] + [InlineData("FOO", CommandType.StoredProcedure)] + [InlineData("FOO;", CommandType.Text)] + [InlineData("FOO BAR", CommandType.Text)] + [InlineData("FOO BAR;", CommandType.Text)] + [InlineData("VACUUM", CommandType.Text)] + [InlineData("VACUUM;", CommandType.Text)] + + // comments imply text + [InlineData("foo--bar", CommandType.Text)] + [InlineData("foo/*bar*/", CommandType.Text)] + public void InferCommandType(string sql, CommandType commandType) + { + Assert.Equal(commandType, CommandDefinition.InferCommandType(sql)); + } } }