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

Remove string.Split allocation from parameter append hot path. #135

Merged
merged 1 commit into from
Jul 20, 2024
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
40 changes: 40 additions & 0 deletions src/Weasel.Core/CommandBuilderBase.cs
Original file line number Diff line number Diff line change
Expand Up @@ -302,6 +302,7 @@
return AppendWithParameters(text, separator);
}

#if NET6_0 || NET7_0
/// <summary>
/// Append a SQL string with user defined placeholder characters for new parameters, and returns an
/// array of the newly created parameters
Expand All @@ -327,6 +328,45 @@

return parameters;
}
#else

/// <summary>
/// Append a SQL string with user defined placeholder characters for new parameters, and returns an
/// array of the newly created parameters
/// </summary>
/// <param name="text"></param>
/// <param name="separator"></param>
/// <returns></returns>
public TParameter[] AppendWithParameters(string text, char separator)
{
var span = text.AsSpan();

var parameters = new TParameter[span.Count(separator)];

var enumerator = span.Split(separator);

var pos = 0;
foreach (var range in enumerator)
{
if (range.Start.Value == 0)
{
// append the first part of the SQL string
_sql.Append(span[range]);
continue;
}

// Just need a placeholder parameter type and value
var parameter = AddParameter(DBNull.Value, _provider.StringParameterType);
parameters[pos] = parameter;
_sql.Append(_parameterPrefix);
_sql.Append(parameter.ParameterName);
_sql.Append(span[range]);
pos++;
}

return parameters;
}
#endif
}

// Note: Those methods are intentionally not written as extension methods
Expand Down Expand Up @@ -429,7 +469,7 @@
/// <param name="ct"></param>
/// <typeparam name="T"></typeparam>
/// <returns></returns>
public static async Task<IReadOnlyList<T>> FetchListAsync<T, TCommand>(

Check warning on line 472 in src/Weasel.Core/CommandBuilderBase.cs

View workflow job for this annotation

GitHub Actions / Postgres ionx/postgres-plv8:12.2 Case Sensitive false

Type parameter 'TCommand' has no matching typeparam tag in the XML comment on 'CommandBuilderExtensions.FetchListAsync<T, TCommand>(DbConnection, ICommandBuilder<TCommand>, Func<DbDataReader, CancellationToken, Task<T>>, DbTransaction?, CancellationToken)' (but other type parameters do)

Check warning on line 472 in src/Weasel.Core/CommandBuilderBase.cs

View workflow job for this annotation

GitHub Actions / Postgres ionx/postgres-plv8:12.2 Case Sensitive false

Type parameter 'TCommand' has no matching typeparam tag in the XML comment on 'CommandBuilderExtensions.FetchListAsync<T, TCommand>(DbConnection, ICommandBuilder<TCommand>, Func<DbDataReader, CancellationToken, Task<T>>, DbTransaction?, CancellationToken)' (but other type parameters do)

Check warning on line 472 in src/Weasel.Core/CommandBuilderBase.cs

View workflow job for this annotation

GitHub Actions / Postgres ionx/postgres-plv8:12.2 Case Sensitive false

Type parameter 'TCommand' has no matching typeparam tag in the XML comment on 'CommandBuilderExtensions.FetchListAsync<T, TCommand>(DbConnection, ICommandBuilder<TCommand>, Func<DbDataReader, CancellationToken, Task<T>>, DbTransaction?, CancellationToken)' (but other type parameters do)

Check warning on line 472 in src/Weasel.Core/CommandBuilderBase.cs

View workflow job for this annotation

GitHub Actions / MSSQL mcr.microsoft.com/mssql/server:2019-latest

Type parameter 'TCommand' has no matching typeparam tag in the XML comment on 'CommandBuilderExtensions.FetchListAsync<T, TCommand>(DbConnection, ICommandBuilder<TCommand>, Func<DbDataReader, CancellationToken, Task<T>>, DbTransaction?, CancellationToken)' (but other type parameters do)

Check warning on line 472 in src/Weasel.Core/CommandBuilderBase.cs

View workflow job for this annotation

GitHub Actions / MSSQL mcr.microsoft.com/mssql/server:2019-latest

Type parameter 'TCommand' has no matching typeparam tag in the XML comment on 'CommandBuilderExtensions.FetchListAsync<T, TCommand>(DbConnection, ICommandBuilder<TCommand>, Func<DbDataReader, CancellationToken, Task<T>>, DbTransaction?, CancellationToken)' (but other type parameters do)

Check warning on line 472 in src/Weasel.Core/CommandBuilderBase.cs

View workflow job for this annotation

GitHub Actions / MSSQL mcr.microsoft.com/mssql/server:2019-latest

Type parameter 'TCommand' has no matching typeparam tag in the XML comment on 'CommandBuilderExtensions.FetchListAsync<T, TCommand>(DbConnection, ICommandBuilder<TCommand>, Func<DbDataReader, CancellationToken, Task<T>>, DbTransaction?, CancellationToken)' (but other type parameters do)

Check warning on line 472 in src/Weasel.Core/CommandBuilderBase.cs

View workflow job for this annotation

GitHub Actions / MSSQL mcr.microsoft.com/mssql/server:2022-latest

Type parameter 'TCommand' has no matching typeparam tag in the XML comment on 'CommandBuilderExtensions.FetchListAsync<T, TCommand>(DbConnection, ICommandBuilder<TCommand>, Func<DbDataReader, CancellationToken, Task<T>>, DbTransaction?, CancellationToken)' (but other type parameters do)

Check warning on line 472 in src/Weasel.Core/CommandBuilderBase.cs

View workflow job for this annotation

GitHub Actions / MSSQL mcr.microsoft.com/mssql/server:2022-latest

Type parameter 'TCommand' has no matching typeparam tag in the XML comment on 'CommandBuilderExtensions.FetchListAsync<T, TCommand>(DbConnection, ICommandBuilder<TCommand>, Func<DbDataReader, CancellationToken, Task<T>>, DbTransaction?, CancellationToken)' (but other type parameters do)

Check warning on line 472 in src/Weasel.Core/CommandBuilderBase.cs

View workflow job for this annotation

GitHub Actions / MSSQL mcr.microsoft.com/mssql/server:2022-latest

Type parameter 'TCommand' has no matching typeparam tag in the XML comment on 'CommandBuilderExtensions.FetchListAsync<T, TCommand>(DbConnection, ICommandBuilder<TCommand>, Func<DbDataReader, CancellationToken, Task<T>>, DbTransaction?, CancellationToken)' (but other type parameters do)

Check warning on line 472 in src/Weasel.Core/CommandBuilderBase.cs

View workflow job for this annotation

GitHub Actions / Postgres ionx/postgres-plv8:12.2 Case Sensitive true

Type parameter 'TCommand' has no matching typeparam tag in the XML comment on 'CommandBuilderExtensions.FetchListAsync<T, TCommand>(DbConnection, ICommandBuilder<TCommand>, Func<DbDataReader, CancellationToken, Task<T>>, DbTransaction?, CancellationToken)' (but other type parameters do)

Check warning on line 472 in src/Weasel.Core/CommandBuilderBase.cs

View workflow job for this annotation

GitHub Actions / Postgres ionx/postgres-plv8:12.2 Case Sensitive true

Type parameter 'TCommand' has no matching typeparam tag in the XML comment on 'CommandBuilderExtensions.FetchListAsync<T, TCommand>(DbConnection, ICommandBuilder<TCommand>, Func<DbDataReader, CancellationToken, Task<T>>, DbTransaction?, CancellationToken)' (but other type parameters do)

Check warning on line 472 in src/Weasel.Core/CommandBuilderBase.cs

View workflow job for this annotation

GitHub Actions / Postgres ionx/postgres-plv8:12.2 Case Sensitive true

Type parameter 'TCommand' has no matching typeparam tag in the XML comment on 'CommandBuilderExtensions.FetchListAsync<T, TCommand>(DbConnection, ICommandBuilder<TCommand>, Func<DbDataReader, CancellationToken, Task<T>>, DbTransaction?, CancellationToken)' (but other type parameters do)

Check warning on line 472 in src/Weasel.Core/CommandBuilderBase.cs

View workflow job for this annotation

GitHub Actions / Postgres postgres:15.3-alpine Case Sensitive false

Type parameter 'TCommand' has no matching typeparam tag in the XML comment on 'CommandBuilderExtensions.FetchListAsync<T, TCommand>(DbConnection, ICommandBuilder<TCommand>, Func<DbDataReader, CancellationToken, Task<T>>, DbTransaction?, CancellationToken)' (but other type parameters do)

Check warning on line 472 in src/Weasel.Core/CommandBuilderBase.cs

View workflow job for this annotation

GitHub Actions / Postgres postgres:15.3-alpine Case Sensitive false

Type parameter 'TCommand' has no matching typeparam tag in the XML comment on 'CommandBuilderExtensions.FetchListAsync<T, TCommand>(DbConnection, ICommandBuilder<TCommand>, Func<DbDataReader, CancellationToken, Task<T>>, DbTransaction?, CancellationToken)' (but other type parameters do)

Check warning on line 472 in src/Weasel.Core/CommandBuilderBase.cs

View workflow job for this annotation

GitHub Actions / Postgres postgres:15.3-alpine Case Sensitive false

Type parameter 'TCommand' has no matching typeparam tag in the XML comment on 'CommandBuilderExtensions.FetchListAsync<T, TCommand>(DbConnection, ICommandBuilder<TCommand>, Func<DbDataReader, CancellationToken, Task<T>>, DbTransaction?, CancellationToken)' (but other type parameters do)

Check warning on line 472 in src/Weasel.Core/CommandBuilderBase.cs

View workflow job for this annotation

GitHub Actions / Postgres postgres:15.3-alpine Case Sensitive true

Type parameter 'TCommand' has no matching typeparam tag in the XML comment on 'CommandBuilderExtensions.FetchListAsync<T, TCommand>(DbConnection, ICommandBuilder<TCommand>, Func<DbDataReader, CancellationToken, Task<T>>, DbTransaction?, CancellationToken)' (but other type parameters do)

Check warning on line 472 in src/Weasel.Core/CommandBuilderBase.cs

View workflow job for this annotation

GitHub Actions / Postgres postgres:15.3-alpine Case Sensitive true

Type parameter 'TCommand' has no matching typeparam tag in the XML comment on 'CommandBuilderExtensions.FetchListAsync<T, TCommand>(DbConnection, ICommandBuilder<TCommand>, Func<DbDataReader, CancellationToken, Task<T>>, DbTransaction?, CancellationToken)' (but other type parameters do)

Check warning on line 472 in src/Weasel.Core/CommandBuilderBase.cs

View workflow job for this annotation

GitHub Actions / Postgres postgres:15.3-alpine Case Sensitive true

Type parameter 'TCommand' has no matching typeparam tag in the XML comment on 'CommandBuilderExtensions.FetchListAsync<T, TCommand>(DbConnection, ICommandBuilder<TCommand>, Func<DbDataReader, CancellationToken, Task<T>>, DbTransaction?, CancellationToken)' (but other type parameters do)
DbConnection connection,
ICommandBuilder<TCommand> commandBuilder,
Func<DbDataReader, CancellationToken, Task<T>> transform,
Expand Down
227 changes: 227 additions & 0 deletions src/Weasel.Core/SpanSplitEnumerator.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,227 @@
using System.Buffers;
using System.Runtime.CompilerServices;

namespace Weasel.Core;

// Vendored from .NET 9 Preview. Requires APIs available in .NET 8+.

#if NET8_0

internal static class SpanSplitExtensions
{
/// <summary>
/// Returns a type that allows for enumeration of each element within a split span
/// using the provided separator character.
/// </summary>
/// <typeparam name="T">The type of the elements.</typeparam>
/// <param name="source">The source span to be enumerated.</param>
/// <param name="separator">The separator character to be used to split the provided span.</param>
/// <returns>Returns a <see cref="SpanSplitEnumerator{T}"/>.</returns>
public static SpanSplitEnumerator<T> Split<T>(this ReadOnlySpan<T> source, T separator) where T : IEquatable<T> =>
new SpanSplitEnumerator<T>(source, separator);

/// <summary>
/// Returns a type that allows for enumeration of each element within a split span
/// using the provided separator span.
/// </summary>
/// <typeparam name="T">The type of the elements.</typeparam>
/// <param name="source">The source span to be enumerated.</param>
/// <param name="separator">The separator span to be used to split the provided span.</param>
/// <returns>Returns a <see cref="SpanSplitEnumerator{T}"/>.</returns>
public static SpanSplitEnumerator<T> Split<T>(this ReadOnlySpan<T> source, ReadOnlySpan<T> separator)
where T : IEquatable<T> =>
new SpanSplitEnumerator<T>(source, separator, treatAsSingleSeparator: true);

/// <summary>
/// Returns a type that allows for enumeration of each element within a split span
/// using the provided <see cref="SpanSplitEnumerator{T}"/>.
/// </summary>
/// <typeparam name="T">The type of the elements.</typeparam>
/// <param name="source">The source span to be enumerated.</param>
/// <param name="separators">The <see cref="SpanSplitEnumerator{T}"/> to be used to split the provided span.</param>
/// <returns>Returns a <see cref="SpanSplitEnumerator{T}"/>.</returns>
/// <remarks>
/// Unlike <see cref="SplitAny{T}(ReadOnlySpan{T}, ReadOnlySpan{T})"/>, the <paramref name="separators"/> is not checked for being empty.
/// An empty <paramref name="separators"/> will result in no separators being found, regardless of the type of <typeparamref name="T"/>,
/// whereas <see cref="SplitAny{T}(ReadOnlySpan{T}, ReadOnlySpan{T})"/> will use all Unicode whitespace characters as separators if <paramref name="separators"/> is
/// empty and <typeparamref name="T"/> is <see cref="char"/>.
/// </remarks>
public static SpanSplitEnumerator<T> SplitAny<T>(this ReadOnlySpan<T> source, SearchValues<T> separators)
where T : IEquatable<T> =>
new SpanSplitEnumerator<T>(source, separators);
}

internal static class SpanSplitConstants
{
/// <summary>A <see cref="SearchValues{Char}"/> for all of the Unicode whitespace characters</summary>
public static readonly SearchValues<char> WhiteSpaceChars =
SearchValues.Create(
"\t\n\v\f\r\u0020\u0085\u00a0\u1680\u2000\u2001\u2002\u2003\u2004\u2005\u2006\u2007\u2008\u2009\u200a\u2028\u2029\u202f\u205f\u3000");
}

internal ref struct SpanSplitEnumerator<T> where T : IEquatable<T>
{
/// <summary>The input span being split.</summary>
private readonly ReadOnlySpan<T> _span;

/// <summary>A single separator to use when <see cref="_splitMode"/> is <see cref="SpanSplitEnumeratorMode.SingleElement"/>.</summary>
private readonly T _separator = default!;

/// <summary>
/// A separator span to use when <see cref="_splitMode"/> is <see cref="SpanSplitEnumeratorMode.Sequence"/> (in which case
/// it's treated as a single separator) or <see cref="SpanSplitEnumeratorMode.Any"/> (in which case it's treated as a set of separators).
/// </summary>
private readonly ReadOnlySpan<T> _separatorBuffer;

/// <summary>A set of separators to use when <see cref="_splitMode"/> is <see cref="SpanSplitEnumeratorMode.SearchValues"/>.</summary>
private readonly SearchValues<T> _searchValues = default!;

/// <summary>Mode that dictates how the instance was configured and how its fields should be used in <see cref="MoveNext"/>.</summary>
private SpanSplitEnumeratorMode _splitMode;

/// <summary>The inclusive starting index in <see cref="_span"/> of the current range.</summary>
private int _startCurrent = 0;

/// <summary>The exclusive ending index in <see cref="_span"/> of the current range.</summary>
private int _endCurrent = 0;

/// <summary>The index in <see cref="_span"/> from which the next separator search should start.</summary>
private int _startNext = 0;

/// <summary>Gets an enumerator that allows for iteration over the split span.</summary>
/// <returns>Returns a <see cref="SpanSplitEnumerator{T}"/> that can be used to iterate over the split span.</returns>
public SpanSplitEnumerator<T> GetEnumerator() => this;

/// <summary>Gets the current element of the enumeration.</summary>
/// <returns>Returns a <see cref="Range"/> instance that indicates the bounds of the current element withing the source span.</returns>
public Range Current => new Range(_startCurrent, _endCurrent);

/// <summary>Initializes the enumerator for <see cref="SpanSplitEnumeratorMode.SearchValues"/>.</summary>
internal SpanSplitEnumerator(ReadOnlySpan<T> span, SearchValues<T> searchValues)
{
_span = span;
_splitMode = SpanSplitEnumeratorMode.SearchValues;
_searchValues = searchValues;
}

/// <summary>Initializes the enumerator for <see cref="SpanSplitEnumeratorMode.Any"/>.</summary>
/// <remarks>
/// If <paramref name="separators"/> is empty and <typeparamref name="T"/> is <see cref="char"/>, as an optimization
/// it will instead use <see cref="SpanSplitEnumeratorMode.SearchValues"/> with a cached <see cref="SearchValues{Char}"/>
/// for all whitespace characters.
/// </remarks>
internal SpanSplitEnumerator(ReadOnlySpan<T> span, ReadOnlySpan<T> separators)
{
_span = span;
if (typeof(T) == typeof(char) && separators.Length == 0)
{
_searchValues = Unsafe.As<SearchValues<T>>(SpanSplitConstants.WhiteSpaceChars);
_splitMode = SpanSplitEnumeratorMode.SearchValues;
}
else
{
_separatorBuffer = separators;
_splitMode = SpanSplitEnumeratorMode.Any;
}
}

/// <summary>Initializes the enumerator for <see cref="SpanSplitEnumeratorMode.Sequence"/> (or <see cref="SpanSplitEnumeratorMode.EmptySequence"/> if the separator is empty).</summary>
/// <remarks><paramref name="treatAsSingleSeparator"/> must be true.</remarks>
internal SpanSplitEnumerator(ReadOnlySpan<T> span, ReadOnlySpan<T> separator, bool treatAsSingleSeparator)
{
_span = span;
_separatorBuffer = separator;
_splitMode = separator.Length == 0 ? SpanSplitEnumeratorMode.EmptySequence : SpanSplitEnumeratorMode.Sequence;
}

/// <summary>Initializes the enumerator for <see cref="SpanSplitEnumeratorMode.SingleElement"/>.</summary>
internal SpanSplitEnumerator(ReadOnlySpan<T> span, T separator)
{
_span = span;
_separator = separator;
_splitMode = SpanSplitEnumeratorMode.SingleElement;
}

/// <summary>
/// Advances the enumerator to the next element of the enumeration.
/// </summary>
/// <returns><see langword="true"/> if the enumerator was successfully advanced to the next element; <see langword="false"/> if the enumerator has passed the end of the enumeration.</returns>
public bool MoveNext()
{
// Search for the next separator index.
int separatorIndex, separatorLength;
switch (_splitMode)
{
case SpanSplitEnumeratorMode.None:
return false;

case SpanSplitEnumeratorMode.SingleElement:
separatorIndex = _span.Slice(_startNext).IndexOf(_separator);
separatorLength = 1;
break;

case SpanSplitEnumeratorMode.Any:
separatorIndex = _span.Slice(_startNext).IndexOfAny(_separatorBuffer);
separatorLength = 1;
break;

case SpanSplitEnumeratorMode.Sequence:
separatorIndex = _span.Slice(_startNext).IndexOf(_separatorBuffer);
separatorLength = _separatorBuffer.Length;
break;

case SpanSplitEnumeratorMode.EmptySequence:
separatorIndex = -1;
separatorLength = 1;
break;

default:
separatorIndex = _span.Slice(_startNext).IndexOfAny(_searchValues);
separatorLength = 1;
break;
}

_startCurrent = _startNext;
if (separatorIndex >= 0)
{
_endCurrent = _startCurrent + separatorIndex;
_startNext = _endCurrent + separatorLength;
}
else
{
_startNext = _endCurrent = _span.Length;

// Set _splitMode to None so that subsequent MoveNext calls will return false.
_splitMode = SpanSplitEnumeratorMode.None;
}

return true;
}
}

/// <summary>Indicates in which mode <see cref="SpanSplitEnumerator{T}"/> is operating, with regards to how it should interpret its state.</summary>
internal enum SpanSplitEnumeratorMode
{
/// <summary>Either a default <see cref="SpanSplitEnumerator{T}"/> was used, or the enumerator has finished enumerating and there's no more work to do.</summary>
None = 0,

/// <summary>A single T separator was provided.</summary>
SingleElement,

/// <summary>A span of separators was provided, each of which should be treated independently.</summary>
Any,

/// <summary>The separator is a span of elements to be treated as a single sequence.</summary>
Sequence,

/// <summary>The separator is an empty sequence, such that no splits should be performed.</summary>
EmptySequence,

/// <summary>
/// A <see cref="SearchValues{Char}"/> was provided and should behave the same as with <see cref="Any"/> but with the separators in the <see cref="SearchValues"/>
/// instance instead of in a <see cref="ReadOnlySpan{Char}"/>.
/// </summary>
SearchValues
}

#endif
30 changes: 30 additions & 0 deletions src/Weasel.Postgresql/BatchBuilder.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
using System.Text;
using Npgsql;
using NpgsqlTypes;
using Weasel.Core;

namespace Weasel.Postgresql;

Expand Down Expand Up @@ -108,6 +109,7 @@ public NpgsqlParameter[] AppendWithParameters(string text)
return AppendWithParameters(text, '?');
}

#if NET6_0 || NET7_0
public NpgsqlParameter[] AppendWithParameters(string text, char placeholder)
{
var split = text.Split(placeholder);
Expand All @@ -124,7 +126,35 @@ public NpgsqlParameter[] AppendWithParameters(string text, char placeholder)

return parameters;
}
#else
public NpgsqlParameter[] AppendWithParameters(string text, char separator)
{
var span = text.AsSpan();

var parameters = new NpgsqlParameter[span.Count(separator)];

var enumerator = span.Split(separator);

var pos = 0;
foreach (var range in enumerator)
{
if (range.Start.Value == 0)
{
// append the first part of the SQL string
_builder.Append(span[range]);
continue;
}

// Just need a placeholder parameter type and value
var parameter = AppendParameter(DBNull.Value, NpgsqlDbType.Text);
parameters[pos] = parameter;
_builder.Append(span[range]);
pos++;
}

return parameters;
}
#endif
public void StartNewCommand()
{
if (_current != null)
Expand Down
Loading