Skip to content

Commit 11dc6b2

Browse files
Copilotrenemadsen
andcommitted
Implement TransformJsonQueryToTable for MySQL JSON_TABLE support
Co-authored-by: renemadsen <76994+renemadsen@users.noreply.github.com>
1 parent bf8390b commit 11dc6b2

1 file changed

Lines changed: 121 additions & 2 deletions

File tree

src/EFCore.MySql/Query/Internal/MySqlQueryableMethodTranslatingExpressionVisitor.cs

Lines changed: 121 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22
// Licensed under the MIT. See LICENSE in the project root for license information.
33

44
using System;
5+
using System.Collections.Generic;
6+
using System.Diagnostics;
57
using System.Diagnostics.CodeAnalysis;
68
using System.Linq;
79
using System.Linq.Expressions;
@@ -210,10 +212,127 @@ protected override ShapedQueryExpression TranslateElementAtOrDefault(
210212
return base.TranslateElementAtOrDefault(source, index, returnDefault);
211213
}
212214

213-
// TODO: Implement for EF Core 7 JSON support.
214215
protected override ShapedQueryExpression TransformJsonQueryToTable(JsonQueryExpression jsonQueryExpression)
215216
{
216-
return base.TransformJsonQueryToTable(jsonQueryExpression);
217+
// Calculate the table alias for the JSON_TABLE function based on the last named path segment
218+
// (or the JSON column name if there are none)
219+
var lastNamedPathSegment = jsonQueryExpression.Path.LastOrDefault(ps => ps.PropertyName is not null);
220+
var tableAlias = _sqlAliasManager.GenerateTableAlias(
221+
lastNamedPathSegment.PropertyName ?? jsonQueryExpression.JsonColumn.Name);
222+
223+
// We now add all of the projected entity's properties and navigations into the JSON_TABLE's COLUMNS clause
224+
var columnInfos = new List<MySqlJsonTableExpression.ColumnInfo>();
225+
226+
// We're only interested in properties which actually exist in the JSON, filter out uninteresting shadow keys
227+
foreach (var property in jsonQueryExpression.StructuralType.GetPropertiesInHierarchy())
228+
{
229+
if (property.GetJsonPropertyName() is string jsonPropertyName)
230+
{
231+
var typeMapping = property.GetRelationalTypeMapping();
232+
233+
columnInfos.Add(
234+
new MySqlJsonTableExpression.ColumnInfo(
235+
Name: jsonPropertyName,
236+
TypeMapping: typeMapping,
237+
// Build path for this property: $[0]
238+
Path: [new PathSegment(_sqlExpressionFactory.Constant(0, _typeMappingSource.FindMapping(typeof(int))))],
239+
AsJson: false));
240+
}
241+
}
242+
243+
// Add navigations to owned entities mapped to JSON
244+
switch (jsonQueryExpression.StructuralType)
245+
{
246+
case IEntityType entityType:
247+
foreach (var navigation in entityType.GetNavigationsInHierarchy()
248+
.Where(n => n.ForeignKey.IsOwnership
249+
&& n.TargetEntityType.IsMappedToJson()
250+
&& n.ForeignKey.PrincipalToDependent == n))
251+
{
252+
var jsonNavigationName = navigation.TargetEntityType.GetJsonPropertyName();
253+
Check.DebugAssert(jsonNavigationName is not null, $"No JSON property name for navigation {navigation.Name}");
254+
255+
columnInfos.Add(
256+
new MySqlJsonTableExpression.ColumnInfo(
257+
Name: jsonNavigationName,
258+
TypeMapping: jsonQueryExpression.JsonColumn.TypeMapping!,
259+
Path: [new PathSegment(_sqlExpressionFactory.Constant(0, _typeMappingSource.FindMapping(typeof(int))))],
260+
AsJson: true));
261+
}
262+
break;
263+
264+
case IComplexType complexType:
265+
foreach (var complexProperty in complexType.GetComplexProperties())
266+
{
267+
var jsonPropertyName = complexProperty.ComplexType.GetJsonPropertyName();
268+
Check.DebugAssert(jsonPropertyName is not null, $"No JSON property name for complex property {complexProperty.Name}");
269+
270+
columnInfos.Add(
271+
new MySqlJsonTableExpression.ColumnInfo(
272+
Name: jsonPropertyName,
273+
TypeMapping: jsonQueryExpression.JsonColumn.TypeMapping!,
274+
Path: [new PathSegment(_sqlExpressionFactory.Constant(0, _typeMappingSource.FindMapping(typeof(int))))],
275+
AsJson: true));
276+
}
277+
break;
278+
279+
default:
280+
throw new UnreachableException();
281+
}
282+
283+
// MySQL JSON_TABLE requires the nested JSON document - it does not accept a path within a containing JSON document
284+
// in the same way as SQL Server OPENJSON. So we wrap JSON_TABLE around a JsonScalarExpression which will extract
285+
// the nested document.
286+
var jsonScalarExpression = new JsonScalarExpression(
287+
jsonQueryExpression.JsonColumn,
288+
jsonQueryExpression.Path,
289+
typeof(string),
290+
jsonQueryExpression.JsonColumn.TypeMapping,
291+
jsonQueryExpression.IsNullable);
292+
293+
// Construct the JSON_TABLE expression with the JsonScalarExpression
294+
var jsonTableExpression = new MySqlJsonTableExpression(
295+
tableAlias,
296+
jsonScalarExpression,
297+
[new PathSegment(_sqlExpressionFactory.Constant("*", RelationalTypeMapping.NullMapping))],
298+
[.. columnInfos]);
299+
300+
// Create the key column for ordering (JSON_TABLE returns a 'key' column for array elements)
301+
var keyColumnTypeMapping = _typeMappingSource.FindMapping(typeof(uint))!;
302+
303+
#pragma warning disable EF1001 // Internal EF Core API usage.
304+
var selectExpression = new SelectExpression(
305+
[jsonTableExpression],
306+
new ColumnExpression(
307+
"key",
308+
tableAlias,
309+
typeof(uint),
310+
keyColumnTypeMapping,
311+
nullable: false),
312+
identifier: [(new ColumnExpression("key", tableAlias, typeof(uint), keyColumnTypeMapping, nullable: false), keyColumnTypeMapping.Comparer)],
313+
_sqlAliasManager);
314+
#pragma warning restore EF1001 // Internal EF Core API usage.
315+
316+
// Add ordering by the key column to maintain array order
317+
selectExpression.AppendOrdering(
318+
new OrderingExpression(
319+
selectExpression.CreateColumnExpression(
320+
jsonTableExpression,
321+
"key",
322+
typeof(uint),
323+
typeMapping: keyColumnTypeMapping,
324+
columnNullable: false),
325+
ascending: true));
326+
327+
return new ShapedQueryExpression(
328+
selectExpression,
329+
new RelationalStructuralTypeShaperExpression(
330+
jsonQueryExpression.StructuralType,
331+
new ProjectionBindingExpression(
332+
selectExpression,
333+
new ProjectionMember(),
334+
typeof(ValueBuffer)),
335+
false));
217336
}
218337

219338
protected override ShapedQueryExpression TranslatePrimitiveCollection(SqlExpression sqlExpression, IProperty property, string tableAlias)

0 commit comments

Comments
 (0)