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

Better support for byte arrays #228

Merged
merged 14 commits into from
Mar 13, 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
33 changes: 33 additions & 0 deletions src/EFCore.Jet/Extensions/JetDbFunctionsExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -448,5 +448,38 @@ public static class JetDbFunctionsExtensions
public static double Random(
[CanBeNull] this DbFunctions _)
=> throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(Random)));


/// <summary>
/// Returns the length of <paramref name="byteArray"/>, or the length of <paramref name="byteArray"/> <c>-1</c> in some cases. If the actual length of <paramref name="byteArray"/> is even and the last byte is
/// <c>0x00</c>, the length of <paramref name="byteArray"/> <c>-1</c> is returned. Otherwise, the actual length of <paramref name="byteArray"/> is returned.
/// </summary>
/// <remarks>
/// <para>
/// Jet SQL reads byte arrays into strings. As Jet uses Unicode, the internal length will always be a multiple of 2. If your data has an odd number of bytes, Jet internally adds a <c>0x00</c> byte to
/// the end of the array.
/// </para>
/// <para>
/// This method will test if the last byte of the array is <c>0x00</c>. If it is <c>0x00</c>, it is assumed that the last byte was added by Jet to fill the array to an even number of bytes and the internal
/// length <c>-1</c> is returned. In all other cases, the internal length is returned.
/// </para>
/// <para>
/// If the actual data length is odd, this method will always return the original length, independent of the value of the last byte of the original data.
/// <br/>
/// If the actual data length is even and the original data does not end with a <c>0x00</c> byte, this method will return the original length.
/// <br/>
/// If the actual data length is even and the original data does end with a <c>0x00</c> byte, this method will return the original length <c>-1</c>.
/// </para>
/// <para>
/// If your data will never end in <c>0x00</c> you can use this extension method safely, otherwise it is highly recommended to only use client evaluation.
/// </para>
/// </remarks>
/// <param name="_">The DbFunctions instance.</param>
/// <param name="byteArray">The `byte[]` array.</param>
/// <returns>The length of <paramref name="byteArray"/>, or the length of <paramref name="byteArray"/> <c>-1</c> in some cases.</returns>
public static int ByteArrayLength(
[CanBeNull] this DbFunctions _,
[NotNull] byte[] byteArray)
=> throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(ByteArrayLength)));
}
}
6 changes: 6 additions & 0 deletions src/EFCore.Jet/Properties/JetStrings.Designer.cs

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 3 additions & 0 deletions src/EFCore.Jet/Properties/JetStrings.resx
Original file line number Diff line number Diff line change
Expand Up @@ -264,5 +264,8 @@
</data>
<data name="QueryingIntoJsonCollectionsNotSupported" xml:space="preserve">
<value>MS Access/Jet does not support querying into JSON collections.</value>
</data>
<data name="ByteArrayLength" xml:space="preserve">
<value>Returning the exact length of a byte array is not supported by Jet. Please rewrite your query or switch to client evaluation. There is support for a 'EF.Functions.ByteArrayLength' method that will return the correct byte array length in most cases with certain exceptions. Please read its documentation carefully, before considering to use it.</value>
</data>
</root>
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,9 @@ public class JetByteArrayMethodTranslator : IMethodCallTranslator
{
private readonly ISqlExpressionFactory _sqlExpressionFactory;

private MethodInfo ByteArrayLength = typeof(JetDbFunctionsExtensions).GetRuntimeMethod(
nameof(JetDbFunctionsExtensions.ByteArrayLength),
new[] { typeof(DbFunctions), typeof(byte[]) })!;
ChrisJollyAU marked this conversation as resolved.
Show resolved Hide resolved
/// <summary>
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
/// the same compatibility standards as public APIs. It may be changed or removed without notice in
Expand All @@ -44,6 +47,40 @@ public JetByteArrayMethodTranslator(ISqlExpressionFactory sqlExpressionFactory)
IReadOnlyList<SqlExpression> arguments,
IDiagnosticsLogger<DbLoggerCategory.Query> logger)
{
if (method == ByteArrayLength)
{
var isBinaryMaxDataType = GetProviderType(arguments[1]) == "varbinary(max)" || arguments[1] is SqlParameterExpression;
SqlExpression dataLengthSqlFunction = _sqlExpressionFactory.Function(
"LENB",
new[] { arguments[1] },
nullable: true,
argumentsPropagateNullability: new[] { true },
isBinaryMaxDataType ? typeof(long) : typeof(int));

var rightval = _sqlExpressionFactory.Function(
"ASCB",
new[]
{
_sqlExpressionFactory.Function(
"RIGHTB",
new[] { arguments[1], _sqlExpressionFactory.Constant(1) },
nullable: true,
argumentsPropagateNullability: new[] { true, true, true },
typeof(byte[]))
},
nullable: true,
argumentsPropagateNullability: new[] { true },
typeof(int));

var minusOne = _sqlExpressionFactory.Subtract(dataLengthSqlFunction, _sqlExpressionFactory.Constant(1));
var whenClause = new CaseWhenClause(_sqlExpressionFactory.Equal(rightval, _sqlExpressionFactory.Constant(0)), minusOne);

dataLengthSqlFunction = _sqlExpressionFactory.Case(new[] { whenClause }, dataLengthSqlFunction);

return isBinaryMaxDataType
? _sqlExpressionFactory.Convert(dataLengthSqlFunction, typeof(int))
: dataLengthSqlFunction;
}
if (method is { IsGenericMethod: true, Name: nameof(Enumerable.Contains) }
&& arguments[0].Type == typeof(byte[]))
{
Expand All @@ -52,12 +89,30 @@ public JetByteArrayMethodTranslator(ISqlExpressionFactory sqlExpressionFactory)

var value = arguments[1] is SqlConstantExpression constantValue
? (SqlExpression)_sqlExpressionFactory.Constant(new[] { (byte)constantValue.Value! }, sourceTypeMapping)
: _sqlExpressionFactory.Convert(arguments[1], typeof(byte[]), sourceTypeMapping);
: _sqlExpressionFactory.Function(
"CHR",
new SqlExpression[] { arguments[1] },
nullable: true,
argumentsPropagateNullability: new[] { true },
typeof(string));



return _sqlExpressionFactory.GreaterThan(
_sqlExpressionFactory.Function(
"INSTRB",
new[] { _sqlExpressionFactory.Constant(1), source, value, _sqlExpressionFactory.Constant(0) },
"INSTR",
new[]
{
_sqlExpressionFactory.Constant(1),
_sqlExpressionFactory.Function(
"STRCONV",
new [] { source, _sqlExpressionFactory.Constant(64) },
nullable: true,
argumentsPropagateNullability: new[] { true, false },
typeof(string)),
value,
_sqlExpressionFactory.Constant(0)
},
nullable: true,
argumentsPropagateNullability: new[] { true, true },
typeof(int)),
Expand All @@ -67,16 +122,22 @@ public JetByteArrayMethodTranslator(ISqlExpressionFactory sqlExpressionFactory)
if (method is { IsGenericMethod: true, Name: nameof(Enumerable.First) } && method.GetParameters().Length == 1
&& arguments[0].Type == typeof(byte[]))
{
return _sqlExpressionFactory.Convert(
_sqlExpressionFactory.Function(
return _sqlExpressionFactory.Function(
"ASCB",
new[] { _sqlExpressionFactory.Function(
"MIDB",
new[] { arguments[0], _sqlExpressionFactory.Constant(1), _sqlExpressionFactory.Constant(1) },
nullable: true,
argumentsPropagateNullability: new[] { true, true, true },
typeof(byte[])),
method.ReturnType);
typeof(byte[])) },
nullable: true,
argumentsPropagateNullability: new[] { true },
typeof(int));
}

return null;
}

private static string? GetProviderType(SqlExpression expression)
=> expression.TypeMapping?.StoreType;
}
49 changes: 21 additions & 28 deletions src/EFCore.Jet/Query/Internal/JetSqlTranslatingExpressionVisitor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@
using Microsoft.EntityFrameworkCore.Query;
using Microsoft.EntityFrameworkCore.Query.SqlExpressions;
using System.Text;
using EntityFrameworkCore.Jet.Internal;
using Microsoft.EntityFrameworkCore.Diagnostics;
namespace EntityFrameworkCore.Jet.Query.Internal;

/// <summary>
Expand Down Expand Up @@ -146,18 +148,7 @@ protected override Expression VisitUnary(UnaryExpression unaryExpression)
{
return QueryCompilationContext.NotTranslatedExpression;
}

var isBinaryMaxDataType = GetProviderType(sqlExpression) == "varbinary(max)" || sqlExpression is SqlParameterExpression;
var dataLengthSqlFunction = Dependencies.SqlExpressionFactory.Function(
"DATALENGTH",
new[] { sqlExpression },
nullable: true,
argumentsPropagateNullability: new[] { true },
isBinaryMaxDataType ? typeof(long) : typeof(int));

return isBinaryMaxDataType
? Dependencies.SqlExpressionFactory.Convert(dataLengthSqlFunction, typeof(int))
: dataLengthSqlFunction;
throw new InvalidOperationException(JetStrings.ByteArrayLength);
}

return base.VisitUnary(unaryExpression);
Expand Down Expand Up @@ -417,23 +408,25 @@ private Expression TranslateByteArrayElementAccess(Expression array, Expression
var visitedIndex = Visit(index);

return visitedArray is SqlExpression sqlArray
&& visitedIndex is SqlExpression sqlIndex
? Dependencies.SqlExpressionFactory.Convert(
Dependencies.SqlExpressionFactory.Function(
"MID",
new[]
{
sqlArray,
Dependencies.SqlExpressionFactory.Add(
Dependencies.SqlExpressionFactory.ApplyDefaultTypeMapping(sqlIndex),
Dependencies.SqlExpressionFactory.Constant(1)),
Dependencies.SqlExpressionFactory.Constant(1)
},
&& visitedIndex is SqlExpression sqlIndex
? Dependencies.SqlExpressionFactory.Function(
"ASCB",
new[] { Dependencies.SqlExpressionFactory.Function(
"MIDB",
new[] {
sqlArray,
Dependencies.SqlExpressionFactory.Add(
Dependencies.SqlExpressionFactory.ApplyDefaultTypeMapping(sqlIndex),
Dependencies.SqlExpressionFactory.Constant(1)),
Dependencies.SqlExpressionFactory.Constant(1) },
nullable: true,
argumentsPropagateNullability: new[] { true, true, true },
typeof(byte[])) },
nullable: true,
argumentsPropagateNullability: new[] { true, true, true },
typeof(byte[])),
resultType)
: QueryCompilationContext.NotTranslatedExpression;
argumentsPropagateNullability: new[] { true },
typeof(int))

: QueryCompilationContext.NotTranslatedExpression;
}

private static string? GetProviderType(SqlExpression expression)
Expand Down
Loading
Loading