Skip to content

Commit

Permalink
- expand auto-sproc detection to handle more scenarios and explicit e…
Browse files Browse the repository at this point in the history
…xclusions

- add net7 target
- use regex generator when available (net7+)
- fix #1984
  • Loading branch information
mgravell committed Oct 20, 2023
1 parent cdadfa6 commit 16c9384
Show file tree
Hide file tree
Showing 13 changed files with 110 additions and 33 deletions.
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));
}
}
}

0 comments on commit 16c9384

Please sign in to comment.