Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

expand auto-sproc detection to handle more scenarios and explicit exclusions #1989

Merged
merged 1 commit into from
Oct 20, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion Dapper.StrongName/Dapper.StrongName.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,12 @@
<Title>Dapper (Strong Named)</Title>
<Description>A high performance Micro-ORM supporting SQL Server, MySQL, Sqlite, SqlCE, Firebird etc..</Description>
<Authors>Sam Saffron;Marc Gravell;Nick Craver</Authors>
<TargetFrameworks>net461;netstandard2.0;net5.0</TargetFrameworks>
<TargetFrameworks>net461;netstandard2.0;net5.0;net7.0</TargetFrameworks>
<SignAssembly>true</SignAssembly>
<PublicSign Condition=" '$(OS)' != 'Windows_NT' ">true</PublicSign>
<Nullable>enable</Nullable>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
<DefineConstants>$(DefineConstants);STRONG_NAME</DefineConstants>
</PropertyGroup>
<ItemGroup>
<Folder Include="Properties\" />
Expand Down
20 changes: 10 additions & 10 deletions Dapper/CommandDefinition.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@
using System.Data;
using System.Reflection;
using System.Reflection.Emit;
using System.Text.RegularExpressions;
using System.Threading;

namespace Dapper
Expand Down Expand Up @@ -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()
{
Expand Down
45 changes: 45 additions & 0 deletions Dapper/CompiledRegex.cs
Original file line number Diff line number Diff line change
@@ -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 = @"(?<![\p{L}\p{N}@_])[?@:](?![\p{L}\p{N}@_])", // look for ? / @ / : *by itself* - see SupportLegacyParameterTokens
LiteralTokensPattern = @"(?<![\p{L}\p{N}_])\{=([\p{L}\p{N}_]+)\}", // look for {=abc} to inject member abc as a literal
PseudoPositionalPattern = @"\?([\p{L}_][\p{L}\p{N}_]*)\?"; // look for ?abc? for the purpose of subst back to ? using member abc


#if NET7_0_OR_GREATER // use regex code generator (this doesn't work for down-level, even if you define the attribute manually)
[GeneratedRegex(LegacyParameterPattern, RegexOptions.IgnoreCase | RegexOptions.Multiline | RegexOptions.CultureInvariant)]
private static partial Regex LegacyParameterGen();

[GeneratedRegex(LiteralTokensPattern, RegexOptions.IgnoreCase | RegexOptions.Multiline | RegexOptions.CultureInvariant)]
private static partial Regex LiteralTokensGen();

[GeneratedRegex(PseudoPositionalPattern, RegexOptions.IgnoreCase | RegexOptions.Multiline | RegexOptions.CultureInvariant)]
private static partial Regex PseudoPositionalGen();

[GeneratedRegex(WhitespaceOrReservedPattern, RegexOptions.IgnoreCase, "en-US")]
private static partial Regex WhitespaceOrReservedGen();

internal static Regex LegacyParameter => 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
}
2 changes: 1 addition & 1 deletion Dapper/Dapper.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
<PackageTags>orm;sql;micro-orm</PackageTags>
<Description>A high performance Micro-ORM supporting SQL Server, MySQL, Sqlite, SqlCE, Firebird etc..</Description>
<Authors>Sam Saffron;Marc Gravell;Nick Craver</Authors>
<TargetFrameworks>net461;netstandard2.0;net5.0</TargetFrameworks>
<TargetFrameworks>net461;netstandard2.0;net5.0;net7.0</TargetFrameworks>
<Nullable>enable</Nullable>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
</PropertyGroup>
Expand Down
4 changes: 4 additions & 0 deletions Dapper/Global.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
using System.Runtime.CompilerServices;
#if !STRONG_NAME
[assembly: InternalsVisibleTo("Dapper.Tests")]
#endif
6 changes: 6 additions & 0 deletions Dapper/PublicAPI/net7.0/PublicAPI.Shipped.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
#nullable enable
Dapper.SqlMapper.GridReader.DisposeAsync() -> System.Threading.Tasks.ValueTask
Dapper.SqlMapper.GridReader.ReadUnbufferedAsync() -> System.Collections.Generic.IAsyncEnumerable<dynamic!>!
Dapper.SqlMapper.GridReader.ReadUnbufferedAsync<T>() -> System.Collections.Generic.IAsyncEnumerable<T>!
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<dynamic!>!
static Dapper.SqlMapper.QueryUnbufferedAsync<T>(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<T>!
1 change: 1 addition & 0 deletions Dapper/PublicAPI/net7.0/PublicAPI.Unshipped.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
#nullable enable
18 changes: 9 additions & 9 deletions Dapper/SqlMapper.DapperRow.Descriptor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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<string,object?>? row = null)
Expand All @@ -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
Expand All @@ -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);
}
}
}
Expand Down
15 changes: 5 additions & 10 deletions Dapper/SqlMapper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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))
Expand Down Expand Up @@ -2386,11 +2386,6 @@ private static IEnumerable<PropertyInfo> FilterParameters(IEnumerable<PropertyIn
return list;
}

// look for ? / @ / : *by itself*
private static readonly Regex smellsLikeOleDb = new(@"(?<![\p{L}\p{N}@_])[?@:](?![\p{L}\p{N}@_])", RegexOptions.IgnoreCase | RegexOptions.Multiline | RegexOptions.CultureInvariant | RegexOptions.Compiled),
literalTokens = new(@"(?<![\p{L}\p{N}_])\{=([\p{L}\p{N}_]+)\}", RegexOptions.IgnoreCase | RegexOptions.Multiline | RegexOptions.CultureInvariant | RegexOptions.Compiled),
pseudoPositional = new(@"\?([\p{L}_][\p{L}\p{N}_]*)\?", RegexOptions.IgnoreCase | RegexOptions.CultureInvariant | RegexOptions.Compiled);

/// <summary>
/// Replace all literal tokens with their text form.
/// </summary>
Expand Down Expand Up @@ -2496,9 +2491,9 @@ internal static void ReplaceLiterals(IParameterLookup parameters, IDbCommand com
internal static IList<LiteralToken> 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<string>(StringComparer.Ordinal);
var list = new List<LiteralToken>(matches.Count);
foreach (Match match in matches)
Expand Down Expand Up @@ -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);
Expand Down
2 changes: 1 addition & 1 deletion Directory.Build.props
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@
<IncludeSymbols>false</IncludeSymbols>
<ManagePackageVersionsCentrally>true</ManagePackageVersionsCentrally>
<PublishRepositoryUrl>true</PublishRepositoryUrl>
<LangVersion>9.0</LangVersion>
<LangVersion>11</LangVersion>
<CheckEolTargetFramework>false</CheckEolTargetFramework>
<SuppressNETCoreSdkPreviewMessage>true</SuppressNETCoreSdkPreviewMessage>
<PackageReadmeFile>readme.md</PackageReadmeFile>
Expand Down
3 changes: 3 additions & 0 deletions appveyor.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@ skip_commits:
files:
- '**/*.md'

install:
- choco install dotnet-sdk --version 7.0.402

environment:
Appveyor: true
# Postgres
Expand Down
2 changes: 1 addition & 1 deletion tests/Dapper.Tests/Dapper.Tests.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
<PropertyGroup>
<AssemblyName>Dapper.Tests</AssemblyName>
<Description>Dapper Core Test Suite</Description>
<TargetFrameworks>net472;net6.0</TargetFrameworks>
<TargetFrameworks>net472;net6.0;net7.0</TargetFrameworks>
<DefineConstants>$(DefineConstants);MSSQLCLIENT</DefineConstants>
<NoWarn>$(NoWarn);IDE0017;IDE0034;IDE0037;IDE0039;IDE0042;IDE0044;IDE0051;IDE0052;IDE0059;IDE0060;IDE0063;IDE1006;xUnit1004;CA1806;CA1816;CA1822;CA1825;CA2208</NoWarn>
<Nullable>enable</Nullable>
Expand Down
22 changes: 22 additions & 0 deletions tests/Dapper.Tests/ProcedureTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -317,5 +317,27 @@ public async Task Issue1986_AutoProc_Whitespace(string space)
var result = await connection.QuerySingleAsync<int>(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));
}
}
}