Skip to content

Commit e17a1e9

Browse files
Copilotrenemadsen
andcommitted
Use JSON_EXTRACT instead of JsonScalarExpression for JSON_TABLE source
Co-authored-by: renemadsen <76994+renemadsen@users.noreply.github.com>
1 parent e6be0f2 commit e17a1e9

1 file changed

Lines changed: 134 additions & 3 deletions

File tree

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

Lines changed: 134 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -214,9 +214,140 @@ protected override ShapedQueryExpression TranslateElementAtOrDefault(
214214

215215
protected override ShapedQueryExpression TransformJsonQueryToTable(JsonQueryExpression jsonQueryExpression)
216216
{
217-
// Call the base implementation which will handle the transformation using the default approach
218-
// This should work now that we have proper JSON_TABLE support
219-
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+
var jsonTypeMapping = jsonQueryExpression.JsonColumn.TypeMapping!;
224+
225+
// We now add all of the projected entity's properties and navigations into the JSON_TABLE's COLUMNS clause
226+
var columnInfos = new List<MySqlJsonTableExpression.ColumnInfo>();
227+
228+
// We're only interested in properties which actually exist in the JSON, filter out uninteresting shadow keys
229+
foreach (var property in jsonQueryExpression.StructuralType.GetPropertiesInHierarchy())
230+
{
231+
if (property.GetJsonPropertyName() is string jsonPropertyName)
232+
{
233+
columnInfos.Add(
234+
new MySqlJsonTableExpression.ColumnInfo(
235+
Name: jsonPropertyName,
236+
TypeMapping: property.GetRelationalTypeMapping(),
237+
// Path for JSON_TABLE: $[0] to access array element properties
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: jsonTypeMapping,
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: jsonTypeMapping,
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+
// For MySQL JSON_TABLE, we need to extract the nested JSON document using JSON_EXTRACT
284+
// Build the full path by combining the JSON column path with the query expression path
285+
var fullPath = jsonQueryExpression.Path.ToList();
286+
287+
// Create a JSON_EXTRACT expression to get the nested JSON document
288+
// JSON_EXTRACT returns the JSON fragment as-is (with quotes for strings)
289+
SqlExpression jsonSource;
290+
if (fullPath.Count > 0)
291+
{
292+
// Build JSON path string like $.path.to.array
293+
var pathBuilder = new System.Text.StringBuilder("$");
294+
foreach (var segment in fullPath)
295+
{
296+
if (segment.PropertyName is not null)
297+
{
298+
pathBuilder.Append('.').Append(segment.PropertyName);
299+
}
300+
else if (segment.ArrayIndex is SqlConstantExpression { Value: int index })
301+
{
302+
pathBuilder.Append('[').Append(index).Append(']');
303+
}
304+
}
305+
306+
// Use JSON_EXTRACT to get the nested JSON document
307+
jsonSource = _sqlExpressionFactory.Function(
308+
"JSON_EXTRACT",
309+
[jsonQueryExpression.JsonColumn, _sqlExpressionFactory.Constant(pathBuilder.ToString())],
310+
nullable: true,
311+
argumentsPropagateNullability: [true, true],
312+
typeof(string),
313+
jsonTypeMapping);
314+
}
315+
else
316+
{
317+
// No path, use the JSON column directly
318+
jsonSource = jsonQueryExpression.JsonColumn;
319+
}
320+
321+
// Construct the JSON_TABLE expression with column definitions
322+
var jsonTableExpression = new MySqlJsonTableExpression(
323+
tableAlias,
324+
jsonSource,
325+
// Path to iterate over array elements: $[*]
326+
[new PathSegment(_sqlExpressionFactory.Constant("*", RelationalTypeMapping.NullMapping))],
327+
[.. columnInfos]);
328+
329+
// MySQL JSON_TABLE returns a 'key' column for array ordering (similar to PostgreSQL's ordinality)
330+
var keyColumnTypeMapping = _typeMappingSource.FindMapping(typeof(uint))!;
331+
332+
#pragma warning disable EF1001 // Internal EF Core API usage.
333+
// Use CreateSelect helper method (from base class) to create the SelectExpression
334+
var selectExpression = CreateSelect(
335+
jsonQueryExpression,
336+
jsonTableExpression,
337+
"key",
338+
typeof(uint),
339+
keyColumnTypeMapping);
340+
#pragma warning restore EF1001 // Internal EF Core API usage.
341+
342+
return new ShapedQueryExpression(
343+
selectExpression,
344+
new RelationalStructuralTypeShaperExpression(
345+
jsonQueryExpression.StructuralType,
346+
new ProjectionBindingExpression(
347+
selectExpression,
348+
new ProjectionMember(),
349+
typeof(ValueBuffer)),
350+
false));
220351
}
221352

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

0 commit comments

Comments
 (0)