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

public static int ByteArrayLength(
[CanBeNull] this DbFunctions _,
[NotNull] byte[] expression)
=> 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>Jet SQL reads byte arrays into strings. As Jet uses unicode, the length returned will always be a multiple of 2. If your data is even number of bytes the result will be correct. If your data is an odd number of bytes, the last byte will be a 0. The result is calculated by checking for a 0 at the last byte and if so reducing the length by 1. If your data is meant to end in a 0 this may lead to the calculated length being 1 short. To opt in to this behaviour, use the 'EF.Functions.ByteArrayLength' method. Alternatively, rewrite your query to use the data in client side.</value>
ChrisJollyAU marked this conversation as resolved.
Show resolved Hide resolved
</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