|
2 | 2 | // Licensed under the MIT. See LICENSE in the project root for license information. |
3 | 3 |
|
4 | 4 | using System; |
| 5 | +using System.Collections.Generic; |
| 6 | +using System.Diagnostics; |
5 | 7 | using System.Diagnostics.CodeAnalysis; |
6 | 8 | using System.Linq; |
7 | 9 | using System.Linq.Expressions; |
@@ -210,10 +212,127 @@ protected override ShapedQueryExpression TranslateElementAtOrDefault( |
210 | 212 | return base.TranslateElementAtOrDefault(source, index, returnDefault); |
211 | 213 | } |
212 | 214 |
|
213 | | - // TODO: Implement for EF Core 7 JSON support. |
214 | 215 | protected override ShapedQueryExpression TransformJsonQueryToTable(JsonQueryExpression jsonQueryExpression) |
215 | 216 | { |
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)); |
217 | 336 | } |
218 | 337 |
|
219 | 338 | protected override ShapedQueryExpression TranslatePrimitiveCollection(SqlExpression sqlExpression, IProperty property, string tableAlias) |
|
0 commit comments