Skip to content

Commit 41dab6c

Browse files
authored
Better support for byte arrays (#228)
* 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
1 parent 2e47826 commit 41dab6c

File tree

9 files changed

+492
-162
lines changed

9 files changed

+492
-162
lines changed

src/EFCore.Jet/Extensions/JetDbFunctionsExtensions.cs

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -448,5 +448,38 @@ public static bool IsDate(
448448
public static double Random(
449449
[CanBeNull] this DbFunctions _)
450450
=> throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(Random)));
451+
452+
453+
/// <summary>
454+
/// 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
455+
/// <c>0x00</c>, the length of <paramref name="byteArray"/> <c>-1</c> is returned. Otherwise, the actual length of <paramref name="byteArray"/> is returned.
456+
/// </summary>
457+
/// <remarks>
458+
/// <para>
459+
/// 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
460+
/// the end of the array.
461+
/// </para>
462+
/// <para>
463+
/// 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
464+
/// length <c>-1</c> is returned. In all other cases, the internal length is returned.
465+
/// </para>
466+
/// <para>
467+
/// 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.
468+
/// <br/>
469+
/// 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.
470+
/// <br/>
471+
/// 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>.
472+
/// </para>
473+
/// <para>
474+
/// 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.
475+
/// </para>
476+
/// </remarks>
477+
/// <param name="_">The DbFunctions instance.</param>
478+
/// <param name="byteArray">The `byte[]` array.</param>
479+
/// <returns>The length of <paramref name="byteArray"/>, or the length of <paramref name="byteArray"/> <c>-1</c> in some cases.</returns>
480+
public static int ByteArrayLength(
481+
[CanBeNull] this DbFunctions _,
482+
[NotNull] byte[] byteArray)
483+
=> throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(ByteArrayLength)));
451484
}
452485
}

src/EFCore.Jet/Properties/JetStrings.Designer.cs

Lines changed: 6 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/EFCore.Jet/Properties/JetStrings.resx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -264,5 +264,8 @@
264264
</data>
265265
<data name="QueryingIntoJsonCollectionsNotSupported" xml:space="preserve">
266266
<value>MS Access/Jet does not support querying into JSON collections.</value>
267+
</data>
268+
<data name="ByteArrayLength" xml:space="preserve">
269+
<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>
267270
</data>
268271
</root>

src/EFCore.Jet/Query/ExpressionTranslators/Internal/JetByteArrayMethodTranslator.cs

Lines changed: 68 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,9 @@ public class JetByteArrayMethodTranslator : IMethodCallTranslator
2121
{
2222
private readonly ISqlExpressionFactory _sqlExpressionFactory;
2323

24+
private MethodInfo ByteArrayLength = typeof(JetDbFunctionsExtensions).GetRuntimeMethod(
25+
nameof(JetDbFunctionsExtensions.ByteArrayLength),
26+
new[] { typeof(DbFunctions), typeof(byte[]) })!;
2427
/// <summary>
2528
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
2629
/// the same compatibility standards as public APIs. It may be changed or removed without notice in
@@ -44,6 +47,40 @@ public JetByteArrayMethodTranslator(ISqlExpressionFactory sqlExpressionFactory)
4447
IReadOnlyList<SqlExpression> arguments,
4548
IDiagnosticsLogger<DbLoggerCategory.Query> logger)
4649
{
50+
if (method == ByteArrayLength)
51+
{
52+
var isBinaryMaxDataType = GetProviderType(arguments[1]) == "varbinary(max)" || arguments[1] is SqlParameterExpression;
53+
SqlExpression dataLengthSqlFunction = _sqlExpressionFactory.Function(
54+
"LENB",
55+
new[] { arguments[1] },
56+
nullable: true,
57+
argumentsPropagateNullability: new[] { true },
58+
isBinaryMaxDataType ? typeof(long) : typeof(int));
59+
60+
var rightval = _sqlExpressionFactory.Function(
61+
"ASCB",
62+
new[]
63+
{
64+
_sqlExpressionFactory.Function(
65+
"RIGHTB",
66+
new[] { arguments[1], _sqlExpressionFactory.Constant(1) },
67+
nullable: true,
68+
argumentsPropagateNullability: new[] { true, true, true },
69+
typeof(byte[]))
70+
},
71+
nullable: true,
72+
argumentsPropagateNullability: new[] { true },
73+
typeof(int));
74+
75+
var minusOne = _sqlExpressionFactory.Subtract(dataLengthSqlFunction, _sqlExpressionFactory.Constant(1));
76+
var whenClause = new CaseWhenClause(_sqlExpressionFactory.Equal(rightval, _sqlExpressionFactory.Constant(0)), minusOne);
77+
78+
dataLengthSqlFunction = _sqlExpressionFactory.Case(new[] { whenClause }, dataLengthSqlFunction);
79+
80+
return isBinaryMaxDataType
81+
? _sqlExpressionFactory.Convert(dataLengthSqlFunction, typeof(int))
82+
: dataLengthSqlFunction;
83+
}
4784
if (method is { IsGenericMethod: true, Name: nameof(Enumerable.Contains) }
4885
&& arguments[0].Type == typeof(byte[]))
4986
{
@@ -52,12 +89,30 @@ public JetByteArrayMethodTranslator(ISqlExpressionFactory sqlExpressionFactory)
5289

5390
var value = arguments[1] is SqlConstantExpression constantValue
5491
? (SqlExpression)_sqlExpressionFactory.Constant(new[] { (byte)constantValue.Value! }, sourceTypeMapping)
55-
: _sqlExpressionFactory.Convert(arguments[1], typeof(byte[]), sourceTypeMapping);
92+
: _sqlExpressionFactory.Function(
93+
"CHR",
94+
new SqlExpression[] { arguments[1] },
95+
nullable: true,
96+
argumentsPropagateNullability: new[] { true },
97+
typeof(string));
98+
99+
56100

57101
return _sqlExpressionFactory.GreaterThan(
58102
_sqlExpressionFactory.Function(
59-
"INSTRB",
60-
new[] { _sqlExpressionFactory.Constant(1), source, value, _sqlExpressionFactory.Constant(0) },
103+
"INSTR",
104+
new[]
105+
{
106+
_sqlExpressionFactory.Constant(1),
107+
_sqlExpressionFactory.Function(
108+
"STRCONV",
109+
new [] { source, _sqlExpressionFactory.Constant(64) },
110+
nullable: true,
111+
argumentsPropagateNullability: new[] { true, false },
112+
typeof(string)),
113+
value,
114+
_sqlExpressionFactory.Constant(0)
115+
},
61116
nullable: true,
62117
argumentsPropagateNullability: new[] { true, true },
63118
typeof(int)),
@@ -67,16 +122,22 @@ public JetByteArrayMethodTranslator(ISqlExpressionFactory sqlExpressionFactory)
67122
if (method is { IsGenericMethod: true, Name: nameof(Enumerable.First) } && method.GetParameters().Length == 1
68123
&& arguments[0].Type == typeof(byte[]))
69124
{
70-
return _sqlExpressionFactory.Convert(
71-
_sqlExpressionFactory.Function(
125+
return _sqlExpressionFactory.Function(
126+
"ASCB",
127+
new[] { _sqlExpressionFactory.Function(
72128
"MIDB",
73129
new[] { arguments[0], _sqlExpressionFactory.Constant(1), _sqlExpressionFactory.Constant(1) },
74130
nullable: true,
75131
argumentsPropagateNullability: new[] { true, true, true },
76-
typeof(byte[])),
77-
method.ReturnType);
132+
typeof(byte[])) },
133+
nullable: true,
134+
argumentsPropagateNullability: new[] { true },
135+
typeof(int));
78136
}
79137

80138
return null;
81139
}
140+
141+
private static string? GetProviderType(SqlExpression expression)
142+
=> expression.TypeMapping?.StoreType;
82143
}

src/EFCore.Jet/Query/Internal/JetSqlTranslatingExpressionVisitor.cs

Lines changed: 21 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@
1212
using Microsoft.EntityFrameworkCore.Query;
1313
using Microsoft.EntityFrameworkCore.Query.SqlExpressions;
1414
using System.Text;
15+
using EntityFrameworkCore.Jet.Internal;
16+
using Microsoft.EntityFrameworkCore.Diagnostics;
1517
namespace EntityFrameworkCore.Jet.Query.Internal;
1618

1719
/// <summary>
@@ -146,18 +148,7 @@ protected override Expression VisitUnary(UnaryExpression unaryExpression)
146148
{
147149
return QueryCompilationContext.NotTranslatedExpression;
148150
}
149-
150-
var isBinaryMaxDataType = GetProviderType(sqlExpression) == "varbinary(max)" || sqlExpression is SqlParameterExpression;
151-
var dataLengthSqlFunction = Dependencies.SqlExpressionFactory.Function(
152-
"DATALENGTH",
153-
new[] { sqlExpression },
154-
nullable: true,
155-
argumentsPropagateNullability: new[] { true },
156-
isBinaryMaxDataType ? typeof(long) : typeof(int));
157-
158-
return isBinaryMaxDataType
159-
? Dependencies.SqlExpressionFactory.Convert(dataLengthSqlFunction, typeof(int))
160-
: dataLengthSqlFunction;
151+
throw new InvalidOperationException(JetStrings.ByteArrayLength);
161152
}
162153

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

419410
return visitedArray is SqlExpression sqlArray
420-
&& visitedIndex is SqlExpression sqlIndex
421-
? Dependencies.SqlExpressionFactory.Convert(
422-
Dependencies.SqlExpressionFactory.Function(
423-
"MID",
424-
new[]
425-
{
426-
sqlArray,
427-
Dependencies.SqlExpressionFactory.Add(
428-
Dependencies.SqlExpressionFactory.ApplyDefaultTypeMapping(sqlIndex),
429-
Dependencies.SqlExpressionFactory.Constant(1)),
430-
Dependencies.SqlExpressionFactory.Constant(1)
431-
},
411+
&& visitedIndex is SqlExpression sqlIndex
412+
? Dependencies.SqlExpressionFactory.Function(
413+
"ASCB",
414+
new[] { Dependencies.SqlExpressionFactory.Function(
415+
"MIDB",
416+
new[] {
417+
sqlArray,
418+
Dependencies.SqlExpressionFactory.Add(
419+
Dependencies.SqlExpressionFactory.ApplyDefaultTypeMapping(sqlIndex),
420+
Dependencies.SqlExpressionFactory.Constant(1)),
421+
Dependencies.SqlExpressionFactory.Constant(1) },
422+
nullable: true,
423+
argumentsPropagateNullability: new[] { true, true, true },
424+
typeof(byte[])) },
432425
nullable: true,
433-
argumentsPropagateNullability: new[] { true, true, true },
434-
typeof(byte[])),
435-
resultType)
436-
: QueryCompilationContext.NotTranslatedExpression;
426+
argumentsPropagateNullability: new[] { true },
427+
typeof(int))
428+
429+
: QueryCompilationContext.NotTranslatedExpression;
437430
}
438431

439432
private static string? GetProviderType(SqlExpression expression)

0 commit comments

Comments
 (0)