Skip to content

Commit

Permalink
Better support for byte arrays (#228)
Browse files Browse the repository at this point in the history
* fix array index and array length for byte arrays

* [GitHub Actions] Update green tests.

* Fix Contains with byte array. and fix getting length if an odd number of bytes

* fix array index and array length for byte arrays

* Fix Contains with byte array. and fix getting length if an odd number of bytes

* Enforce an optin methodology using EF.Functions for the byte array length due to certain situations with unicode strings

* Split error message and details into 2 parts
  • Loading branch information
ChrisJollyAU committed Mar 13, 2024
1 parent 2e47826 commit 41dab6c
Show file tree
Hide file tree
Showing 9 changed files with 492 additions and 162 deletions.
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[]) })!;
/// <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

0 comments on commit 41dab6c

Please sign in to comment.