-
Notifications
You must be signed in to change notification settings - Fork 3.4k
Expand file tree
/
Copy pathSqlServerQueryableMethodTranslatingExpressionVisitor.cs
More file actions
1155 lines (1033 loc) · 58.8 KB
/
SqlServerQueryableMethodTranslatingExpressionVisitor.cs
File metadata and controls
1155 lines (1033 loc) · 58.8 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
using System.Diagnostics.CodeAnalysis;
using Microsoft.EntityFrameworkCore.Query.SqlExpressions;
using Microsoft.EntityFrameworkCore.SqlServer.Infrastructure.Internal;
using Microsoft.EntityFrameworkCore.SqlServer.Internal;
using Microsoft.EntityFrameworkCore.SqlServer.Metadata.Internal;
using Microsoft.EntityFrameworkCore.SqlServer.Query.Internal.SqlExpressions;
using Microsoft.EntityFrameworkCore.SqlServer.Storage.Internal;
namespace Microsoft.EntityFrameworkCore.SqlServer.Query.Internal;
/// <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
/// any release. You should only use it directly in your code with extreme caution and knowing that
/// doing so can result in application failures when updating to a new Entity Framework Core release.
/// </summary>
public class SqlServerQueryableMethodTranslatingExpressionVisitor : RelationalQueryableMethodTranslatingExpressionVisitor
{
private readonly SqlServerQueryCompilationContext _queryCompilationContext;
private readonly IRelationalTypeMappingSource _typeMappingSource;
private readonly ISqlExpressionFactory _sqlExpressionFactory;
private readonly ISqlServerSingletonOptions _sqlServerSingletonOptions;
private HashSet<ColumnExpression>? _columnsWithMultipleSetters;
/// <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
/// any release. You should only use it directly in your code with extreme caution and knowing that
/// doing so can result in application failures when updating to a new Entity Framework Core release.
/// </summary>
public SqlServerQueryableMethodTranslatingExpressionVisitor(
QueryableMethodTranslatingExpressionVisitorDependencies dependencies,
RelationalQueryableMethodTranslatingExpressionVisitorDependencies relationalDependencies,
SqlServerQueryCompilationContext queryCompilationContext,
ISqlServerSingletonOptions sqlServerSingletonOptions)
: base(dependencies, relationalDependencies, queryCompilationContext)
{
_queryCompilationContext = queryCompilationContext;
_typeMappingSource = relationalDependencies.TypeMappingSource;
_sqlExpressionFactory = relationalDependencies.SqlExpressionFactory;
_sqlServerSingletonOptions = sqlServerSingletonOptions;
}
/// <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
/// any release. You should only use it directly in your code with extreme caution and knowing that
/// doing so can result in application failures when updating to a new Entity Framework Core release.
/// </summary>
protected SqlServerQueryableMethodTranslatingExpressionVisitor(
SqlServerQueryableMethodTranslatingExpressionVisitor parentVisitor)
: base(parentVisitor)
{
_queryCompilationContext = parentVisitor._queryCompilationContext;
_typeMappingSource = parentVisitor._typeMappingSource;
_sqlExpressionFactory = parentVisitor._sqlExpressionFactory;
_sqlServerSingletonOptions = parentVisitor._sqlServerSingletonOptions;
}
/// <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
/// any release. You should only use it directly in your code with extreme caution and knowing that
/// doing so can result in application failures when updating to a new Entity Framework Core release.
/// </summary>
protected override QueryableMethodTranslatingExpressionVisitor CreateSubqueryVisitor()
=> new SqlServerQueryableMethodTranslatingExpressionVisitor(this);
/// <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
/// any release. You should only use it directly in your code with extreme caution and knowing that
/// doing so can result in application failures when updating to a new Entity Framework Core release.
/// </summary>
protected override Expression VisitMethodCall(MethodCallExpression methodCallExpression)
{
var method = methodCallExpression.Method;
if (method.DeclaringType == typeof(SqlServerQueryableExtensions)
&& Visit(methodCallExpression.Arguments[0]) is ShapedQueryExpression source)
{
switch (method.Name)
{
case nameof(SqlServerQueryableExtensions.VectorSearch)
when methodCallExpression.Arguments is
[
_, // source, translated above
UnaryExpression { NodeType: ExpressionType.Quote, Operand: LambdaExpression vectorPropertySelector },
var similarTo,
var metric
]
&& source is
{
// Since EF.Functions.VectorSearch() is an extension over DbSet directly (not IQueryable<T>), we know the query
// expression is an empty SelectExpression
QueryExpression: SelectExpression { Tables: [TableExpression table] } select,
ShaperExpression: StructuralTypeShaperExpression
{
StructuralType: IEntityType entityType,
ValueBufferExpression: ProjectionBindingExpression projectionBinding,
} entityShaper
}:
{
#pragma warning disable EF9105 // VectorSearch is experimental
if (TranslateLambdaExpression(source, vectorPropertySelector) is not ColumnExpression vectorColumn)
{
throw new InvalidOperationException(SqlServerStrings.VectorSearchRequiresColumn);
}
if (TranslateExpression(similarTo) is not { } translatedSimilarTo
|| TranslateExpression(metric, applyDefaultTypeMapping: false) is not { } translatedMetric)
{
return QueryCompilationContext.NotTranslatedExpression;
}
// Apply varchar instead of the default nvarchar to avoid the N'...' syntax
translatedMetric = _sqlExpressionFactory.ApplyTypeMapping(
translatedMetric, _typeMappingSource.FindMapping("varchar(max)"));
var vectorSearchFunction = new TableValuedFunctionExpression(
_queryCompilationContext.SqlAliasManager.GenerateTableAlias("vector_search"),
"VECTOR_SEARCH",
[
// Note that SQL Server VECTOR_SEARCH() really does accept a full table expression as its first
// argument, including a table alias which is then used to refer to the original table's columns:
// VECTOR_SEARCH([Blogs] AS b, ...)
table,
// We pass the column as a regular ColumnExpression (in SQL generation we'll only write out the name, without the table alias,
// as required by SQL Server)
vectorColumn,
translatedSimilarTo,
translatedMetric
]);
// We have the VECTOR_SEARCH() function call. Modify the SelectExpression and shaper to use it and project
// the appropriate things.
#pragma warning disable EF1001 // Internal EF Core API usage.
select.SetTables([vectorSearchFunction]);
#pragma warning restore EF1001 // Internal EF Core API usage.
var resultType = methodCallExpression.Method.ReturnType.GetSequenceType();
var entityProjection = select.GetProjection(projectionBinding);
var valueProjectionMember = new ProjectionMember().Append(resultType.GetProperty(nameof(VectorSearchResult<>.Value))!);
var distanceProjectionMember = new ProjectionMember().Append(resultType.GetProperty(nameof(VectorSearchResult<>.Distance))!);
var distanceColumn = new ColumnExpression("Distance", vectorSearchFunction.Alias, typeof(double), _typeMappingSource.FindMapping(typeof(double)), nullable: false);
select.ReplaceProjection(new Dictionary<ProjectionMember, Expression>
{
[valueProjectionMember] = entityProjection,
[distanceProjectionMember] = distanceColumn
});
// VECTOR_SEARCH() results are implicitly ordered by distance ascending; add this ordering so that
// users can compose with Take() without needing an explicit OrderBy(r => r.Distance).
select.AppendOrdering(new OrderingExpression(distanceColumn, ascending: true));
var shaper = Expression.New(
resultType.GetConstructors().Single(),
arguments:
[
entityShaper.Update(new ProjectionBindingExpression(select, valueProjectionMember, typeof(ValueBuffer))),
new ProjectionBindingExpression(select, distanceProjectionMember, typeof(double))
],
members:
[
resultType.GetProperty(nameof(VectorSearchResult<>.Value))!,
resultType.GetProperty(nameof(VectorSearchResult<>.Distance))!
]);
{
return new ShapedQueryExpression(select, shaper);
}
#pragma warning restore EF9105 // VectorSearch is experimental
}
case nameof(SqlServerQueryableExtensions.FreeTextTable) or nameof(SqlServerQueryableExtensions.ContainsTable)
when source is
{
QueryExpression: SelectExpression { Tables: [TableExpression table] } select,
ShaperExpression: StructuralTypeShaperExpression
}:
{
return TranslateFullTextTableFunction(methodCallExpression, source, select, table);
}
}
}
return base.VisitMethodCall(methodCallExpression);
}
private Expression TranslateFullTextTableFunction(
MethodCallExpression methodCallExpression,
ShapedQueryExpression source,
SelectExpression select,
TableExpression table)
{
var method = methodCallExpression.Method;
var functionName = method.Name switch
{
nameof(SqlServerQueryableExtensions.FreeTextTable) => "FREETEXTTABLE",
nameof(SqlServerQueryableExtensions.ContainsTable) => "CONTAINSTABLE",
_ => throw new UnreachableException()
};
var (columnsExpression, searchText, languageTerm, topN) = methodCallExpression.Arguments switch
{
// columnSelector overload: (source, columnSelector, searchText, languageTerm?, topN?)
[_, UnaryExpression { NodeType: ExpressionType.Quote, Operand: LambdaExpression columnSelector }, var s, var l, var t]
=> (ExtractColumnsFromSelector(columnSelector, source)
?? throw new InvalidOperationException(SqlServerStrings.InvalidColumnNameForFreeText), s, l, t),
// No column selector (all-columns overload): (source, searchText, languageTerm?, topN?)
// Use an empty array to signal "*" (all columns)
[_, var s, var l, var t] => ((Expression)Expression.NewArrayInit(typeof(ColumnExpression)), s, l, t),
_ => throw new UnreachableException()
};
if (TranslateExpression(searchText) is not { } translatedSearchText
|| TranslateExpression(languageTerm) is not { } translatedLanguageTerm
|| TranslateExpression(topN) is not { } translatedTopN)
{
return QueryCompilationContext.NotTranslatedExpression;
}
// Get the key type from the method's return type: IQueryable<FullTextSearchResult<TKey>>
var resultType = method.ReturnType.GetSequenceType();
var keyType = resultType.GetGenericArguments()[0];
List<Expression> arguments = [table, columnsExpression, translatedSearchText];
if (translatedLanguageTerm is not SqlConstantExpression { Value: null })
{
arguments.Add(translatedLanguageTerm);
}
// See note in SqlServerSqlNullabilityProcessor, where we remove the topN argument if it's a NULL parameter.
if (translatedTopN is not SqlConstantExpression { Value: null })
{
arguments.Add(translatedTopN);
}
var fullTextTableFunction = new TableValuedFunctionExpression(
_queryCompilationContext.SqlAliasManager.GenerateTableAlias(functionName.ToLowerInvariant()),
functionName,
arguments);
#pragma warning disable EF1001 // Internal EF Core API usage.
select.SetTables([fullTextTableFunction]);
#pragma warning restore EF1001 // Internal EF Core API usage.
var keyProjectionMember = new ProjectionMember().Append(resultType.GetProperty(nameof(FullTextSearchResult<int>.Key))!);
var rankProjectionMember = new ProjectionMember().Append(resultType.GetProperty(nameof(FullTextSearchResult<int>.Rank))!);
select.ReplaceProjection(new Dictionary<ProjectionMember, Expression>
{
[keyProjectionMember] = new ColumnExpression("KEY", fullTextTableFunction.Alias, keyType, _typeMappingSource.FindMapping(keyType), nullable: false),
[rankProjectionMember] = new ColumnExpression("RANK", fullTextTableFunction.Alias, typeof(int), _typeMappingSource.FindMapping(typeof(int)), nullable: false)
});
var shaper = Expression.New(
resultType.GetConstructors().Single(),
arguments:
[
new ProjectionBindingExpression(select, keyProjectionMember, keyType),
new ProjectionBindingExpression(select, rankProjectionMember, typeof(int))
],
members:
[
resultType.GetProperty(nameof(FullTextSearchResult<>.Key))!,
resultType.GetProperty(nameof(FullTextSearchResult<>.Rank))!
]);
return new ShapedQueryExpression(select, shaper);
}
private NewArrayExpression? ExtractColumnsFromSelector(LambdaExpression columnSelector, ShapedQueryExpression source)
{
var body = columnSelector.Body;
// Handle Convert node (when lambda returns object but property is a value type or different type)
if (body is UnaryExpression { NodeType: ExpressionType.Convert } convert)
{
body = convert.Operand;
}
switch (body)
{
// Single column: e => e.Property
case MemberExpression:
// Translate and return the column expression wrapped in an array
return TranslateLambdaExpression(source, columnSelector) is ColumnExpression column
? Expression.NewArrayInit(typeof(string), column)
: null;
// Multiple columns: e => new { e.Property1, e.Property2 }
case NewExpression @new:
var columns = new List<ColumnExpression>();
foreach (var argument in @new.Arguments)
{
var argBody = argument;
if (argBody is UnaryExpression { NodeType: ExpressionType.Convert } argConvert)
{
argBody = argConvert.Operand;
}
if (argBody is MemberExpression)
{
// Create a lambda for this single member to translate it
var param = columnSelector.Parameters[0];
var singleMemberLambda = Expression.Lambda(argBody, param);
if (TranslateLambdaExpression(source, singleMemberLambda) is ColumnExpression col)
{
columns.Add(col);
}
else
{
return null;
}
}
else
{
return null;
}
}
// Return a NewArrayExpression containing all the ColumnExpressions
return columns.Count > 0
? Expression.NewArrayInit(typeof(string), columns)
: null;
default:
return null;
}
}
/// <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
/// any release. You should only use it directly in your code with extreme caution and knowing that
/// doing so can result in application failures when updating to a new Entity Framework Core release.
/// </summary>
protected override Expression VisitExtension(Expression extensionExpression)
{
if (extensionExpression is TemporalQueryRootExpression queryRootExpression)
{
var selectExpression = CreateSelect(queryRootExpression.EntityType);
Func<TableExpression, TableExpressionBase> annotationApplyingFunc = queryRootExpression switch
{
TemporalAllQueryRootExpression => te => te
.AddAnnotation(SqlServerAnnotationNames.TemporalOperationType, TemporalOperationType.All),
TemporalAsOfQueryRootExpression asOf => te => te
.AddAnnotation(SqlServerAnnotationNames.TemporalOperationType, TemporalOperationType.AsOf)
.AddAnnotation(SqlServerAnnotationNames.TemporalAsOfPointInTime, asOf.PointInTime),
TemporalBetweenQueryRootExpression between => te => te
.AddAnnotation(SqlServerAnnotationNames.TemporalOperationType, TemporalOperationType.Between)
.AddAnnotation(SqlServerAnnotationNames.TemporalRangeOperationFrom, between.From)
.AddAnnotation(SqlServerAnnotationNames.TemporalRangeOperationTo, between.To),
TemporalContainedInQueryRootExpression containedIn => te => te
.AddAnnotation(SqlServerAnnotationNames.TemporalOperationType, TemporalOperationType.ContainedIn)
.AddAnnotation(SqlServerAnnotationNames.TemporalRangeOperationFrom, containedIn.From)
.AddAnnotation(SqlServerAnnotationNames.TemporalRangeOperationTo, containedIn.To),
TemporalFromToQueryRootExpression fromTo => te => te
.AddAnnotation(SqlServerAnnotationNames.TemporalOperationType, TemporalOperationType.FromTo)
.AddAnnotation(SqlServerAnnotationNames.TemporalRangeOperationFrom, fromTo.From)
.AddAnnotation(SqlServerAnnotationNames.TemporalRangeOperationTo, fromTo.To),
_ => throw new InvalidOperationException(queryRootExpression.Print()),
};
selectExpression = (SelectExpression)new TemporalAnnotationApplyingExpressionVisitor(annotationApplyingFunc)
.Visit(selectExpression);
return new ShapedQueryExpression(
selectExpression,
new RelationalStructuralTypeShaperExpression(
queryRootExpression.EntityType,
new ProjectionBindingExpression(
selectExpression,
new ProjectionMember(),
typeof(ValueBuffer)),
false));
}
return base.VisitExtension(extensionExpression);
}
/// <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
/// any release. You should only use it directly in your code with extreme caution and knowing that
/// doing so can result in application failures when updating to a new Entity Framework Core release.
/// </summary>
protected override ShapedQueryExpression? TranslatePrimitiveCollection(
SqlExpression sqlExpression,
IProperty? property,
string tableAlias)
{
if (_sqlServerSingletonOptions.EngineType == SqlServerEngineType.SqlServer
&& _sqlServerSingletonOptions.SqlServerCompatibilityLevel < 130)
{
AddTranslationErrorDetails(
SqlServerStrings.CompatibilityLevelTooLowForScalarCollections(_sqlServerSingletonOptions.SqlServerCompatibilityLevel));
return null;
}
if (_sqlServerSingletonOptions.EngineType == SqlServerEngineType.AzureSql
&& _sqlServerSingletonOptions.AzureSqlCompatibilityLevel < 130)
{
AddTranslationErrorDetails(
SqlServerStrings.CompatibilityLevelTooLowForScalarCollections(_sqlServerSingletonOptions.AzureSqlCompatibilityLevel));
return null;
}
// Generate the OPENJSON function expression, and wrap it in a SelectExpression.
// Note that where the elementTypeMapping is known (i.e. collection columns), we immediately generate OPENJSON with a WITH clause
// (i.e. with a columnInfo), which determines the type conversion to apply to the JSON elements coming out.
// For parameter collections, the element type mapping will only be inferred and applied later (see
// SqlServerInferredTypeMappingApplier below), at which point the we'll apply it to add the WITH clause.
var elementTypeMapping = (RelationalTypeMapping?)sqlExpression.TypeMapping?.ElementTypeMapping;
var openJsonExpression = elementTypeMapping is null
? new SqlServerOpenJsonExpression(tableAlias, sqlExpression)
: new SqlServerOpenJsonExpression(
tableAlias, sqlExpression,
columnInfos:
[
new SqlServerOpenJsonExpression.ColumnInfo
{
Name = "value",
TypeMapping = elementTypeMapping,
Path = []
}
]);
var elementClrType = sqlExpression.Type.GetSequenceType();
// If this is a collection property, get the element's nullability out of metadata. Otherwise, this is a parameter property, in
// which case we only have the CLR type (note that we cannot produce different SQLs based on the nullability of an *element* in
// a parameter collection - our caching mechanism only supports varying by the nullability of the parameter itself (i.e. the
// collection).
var isElementNullable = property?.GetElementType()!.IsNullable;
var keyColumnTypeMapping = _typeMappingSource.FindMapping("nvarchar(4000)")!;
#pragma warning disable EF1001 // Internal EF Core API usage.
var selectExpression = new SelectExpression(
[openJsonExpression],
new ColumnExpression(
"value",
tableAlias,
elementClrType.UnwrapNullableType(),
elementTypeMapping,
isElementNullable ?? elementClrType.IsNullableType()),
identifier:
[
(new ColumnExpression("key", tableAlias, typeof(string), keyColumnTypeMapping, nullable: false),
keyColumnTypeMapping.Comparer)
],
_queryCompilationContext.SqlAliasManager);
#pragma warning restore EF1001 // Internal EF Core API usage.
// OPENJSON doesn't guarantee the ordering of the elements coming out; when using OPENJSON without WITH, a [key] column is returned
// with the JSON array's ordering, which we can ORDER BY; this option doesn't exist with OPENJSON with WITH, unfortunately.
// However, OPENJSON with WITH has better performance, and also applies JSON-specific conversions we cannot be done otherwise
// (e.g. OPENJSON with WITH does base64 decoding for VARBINARY).
// Here we generate OPENJSON with WITH, but also add an ordering by [key] - this is a temporary invalid representation.
// In SqlServerQueryTranslationPostprocessor, we'll post-process the expression; if the ORDER BY was stripped (e.g. because of
// IN, EXISTS or a set operation), we'll just leave the OPENJSON with WITH. If not, we'll convert the OPENJSON with WITH to an
// OPENJSON without WITH.
// Note that the OPENJSON 'key' column is an nvarchar - we convert it to an int before sorting.
selectExpression.AppendOrdering(
new OrderingExpression(
_sqlExpressionFactory.Convert(
selectExpression.CreateColumnExpression(
openJsonExpression,
"key",
typeof(string),
typeMapping: _typeMappingSource.FindMapping("nvarchar(4000)"),
columnNullable: false),
typeof(int),
_typeMappingSource.FindMapping(typeof(int))),
ascending: true));
var shaperExpression = (Expression)new ProjectionBindingExpression(
selectExpression, new ProjectionMember(), elementClrType.MakeNullable());
if (shaperExpression.Type != elementClrType)
{
Check.DebugAssert(
elementClrType.MakeNullable() == shaperExpression.Type,
"expression.Type must be nullable of targetType");
shaperExpression = Expression.Convert(shaperExpression, elementClrType);
}
return new ShapedQueryExpression(selectExpression, shaperExpression);
}
/// <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
/// any release. You should only use it directly in your code with extreme caution and knowing that
/// doing so can result in application failures when updating to a new Entity Framework Core release.
/// </summary>
protected override ShapedQueryExpression TransformJsonQueryToTable(JsonQueryExpression jsonQueryExpression)
{
var structuralType = jsonQueryExpression.StructuralType;
// Calculate the table alias for the OPENJSON expression based on the last named path segment
// (or the JSON column name if there are none)
var lastNamedPathSegment = jsonQueryExpression.Path.LastOrDefault(ps => ps.PropertyName is not null);
var tableAlias =
_queryCompilationContext.SqlAliasManager.GenerateTableAlias(
lastNamedPathSegment.PropertyName ?? jsonQueryExpression.JsonColumn.Name);
// We now add all of projected entity's the properties and navigations into the OPENJSON's WITH clause. Note that navigations
// get AS JSON, which projects out the JSON sub-document for them as text, which can be further navigated into.
var columnInfos = new List<SqlServerOpenJsonExpression.ColumnInfo>();
// We're only interested in properties which actually exist in the JSON, filter out uninteresting shadow keys
// (for owned JSON entities)
foreach (var property in structuralType.GetPropertiesInHierarchy())
{
if (property.GetJsonPropertyName() is { } jsonPropertyName)
{
columnInfos.Add(
new SqlServerOpenJsonExpression.ColumnInfo
{
Name = jsonPropertyName,
TypeMapping = property.GetRelationalTypeMapping(),
Path = [new PathSegment(jsonPropertyName)],
AsJson = property.GetRelationalTypeMapping().ElementTypeMapping is not null
});
}
}
// Find the container column in the relational model to get its type mapping
// Note that we assume exactly one column with the given name mapped to the entity (despite entity splitting).
// See #38060 about improving this.
var containerColumnName = structuralType.GetContainerColumnName()!;
var containerColumn = structuralType.ContainingEntityType.GetTableMappings()
.Select(m => m.Table.FindColumn(containerColumnName))
.Single(c => c is not null)!;
var nestedJsonPropertyNames = jsonQueryExpression.StructuralType switch
{
IEntityType entityType
=> entityType.GetNavigationsInHierarchy()
.Where(n => n.ForeignKey.IsOwnership
&& n.TargetEntityType.IsMappedToJson()
&& n.ForeignKey.PrincipalToDependent == n)
.Select(n => n.TargetEntityType.GetJsonPropertyName() ?? throw new UnreachableException()),
IComplexType complexType
=> complexType.GetComplexProperties().Select(p => p.ComplexType.GetJsonPropertyName() ?? throw new UnreachableException()),
_ => throw new UnreachableException()
};
foreach (var jsonPropertyName in nestedJsonPropertyNames)
{
columnInfos.Add(
new SqlServerOpenJsonExpression.ColumnInfo
{
Name = jsonPropertyName,
TypeMapping = containerColumn.StoreTypeMapping,
Path = [new PathSegment(jsonPropertyName)],
AsJson = true
});
}
var openJsonExpression = new SqlServerOpenJsonExpression(
tableAlias, jsonQueryExpression.JsonColumn, jsonQueryExpression.Path, columnInfos);
#pragma warning disable EF1001 // Internal EF Core API usage.
var selectExpression = CreateSelect(
jsonQueryExpression,
openJsonExpression,
"key",
typeof(string),
_typeMappingSource.FindMapping("nvarchar(4000)")!);
#pragma warning restore EF1001 // Internal EF Core API usage.
// See note on OPENJSON and ordering in TranslateCollection
selectExpression.AppendOrdering(
new OrderingExpression(
_sqlExpressionFactory.Convert(
selectExpression.CreateColumnExpression(
openJsonExpression,
"key",
typeof(string),
typeMapping: _typeMappingSource.FindMapping("nvarchar(4000)"),
columnNullable: false),
typeof(int),
_typeMappingSource.FindMapping(typeof(int))),
ascending: true));
return new ShapedQueryExpression(
selectExpression,
new RelationalStructuralTypeShaperExpression(
jsonQueryExpression.StructuralType,
new ProjectionBindingExpression(
selectExpression,
new ProjectionMember(),
typeof(ValueBuffer)),
false));
}
/// <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
/// any release. You should only use it directly in your code with extreme caution and knowing that
/// doing so can result in application failures when updating to a new Entity Framework Core release.
/// </summary>
protected override ShapedQueryExpression? TranslateContains(ShapedQueryExpression source, Expression item)
{
// Attempt to translate to JSON_CONTAINS for SQL Server 2025+ (compatibility level 170+).
// JSON_CONTAINS is more efficient than IN (SELECT ... FROM OPENJSON(...)) for primitive collections.
if (_sqlServerSingletonOptions.SupportsJsonType
&& source.QueryExpression is SelectExpression
{
// Primitive collection over OPENJSON (e.g. [p].[Ints])
Tables:
[
SqlServerOpenJsonExpression
{
// JSON_CONTAINS() is only supported over json, not nvarchar
Json: { TypeMapping: SqlServerJsonTypeMapping } json,
Path: null,
ColumnInfos: [{ Name: "value" }]
}
],
Predicate: null,
GroupBy: [],
Having: null,
IsDistinct: false,
Limit: null,
Offset: null
}
&& TranslateExpression(item, applyDefaultTypeMapping: false) is { } translatedItem
// Literal untyped NULL not supported as item by JSON_CONTAINS().
// For any other nullable item, SqlServerNullabilityProcessor will add a null check around the JSON_CONTAINS call.
// TODO: reverify this once JSON_CONTAINS comes out of preview, #37715
&& translatedItem is not SqlConstantExpression { Value: null }
// Note: JSON_CONTAINS doesn't allow searching for null items within a JSON collection (returns 0)
// As a result, we only translate to JSON_CONTAINS when we know that either the item is non-nullable or the collection's elements are.
// TODO: reverify this once JSON_CONTAINS comes out of preview, #37715
&& (
translatedItem is ColumnExpression { IsNullable: false } or SqlConstantExpression { Value: not null }
|| !translatedItem.Type.IsNullableType()
|| json.Type.GetSequenceType() is var elementClrType && !elementClrType.IsNullableType()))
{
// JSON_CONTAINS returns 1 if found, 0 if not found. It's a search condition expression.
var jsonContains = _sqlExpressionFactory.Equal(
_sqlExpressionFactory.Function(
"JSON_CONTAINS",
[json, translatedItem],
nullable: true,
argumentsPropagateNullability: [true, false],
typeof(int)),
_sqlExpressionFactory.Constant(1));
#pragma warning disable EF1001 // Internal EF Core API usage.
var selectExpression = new SelectExpression(jsonContains, _queryCompilationContext.SqlAliasManager);
return source.Update(
selectExpression,
Expression.Convert(
new ProjectionBindingExpression(selectExpression, new ProjectionMember(), typeof(bool?)),
typeof(bool)));
#pragma warning restore EF1001 // Internal EF Core API usage.
}
return base.TranslateContains(source, item);
}
/// <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
/// any release. You should only use it directly in your code with extreme caution and knowing that
/// doing so can result in application failures when updating to a new Entity Framework Core release.
/// </summary>
protected override ShapedQueryExpression? TranslateElementAtOrDefault(
ShapedQueryExpression source,
Expression index,
bool returnDefault)
{
if (!returnDefault)
{
switch (source.QueryExpression)
{
// Index on parameter using a column
// Translate via JSON_VALUE() instead of via a VALUES subquery
case SelectExpression
{
Tables: [ValuesExpression { ValuesParameter: { } valuesParameter }],
Predicate: null,
GroupBy: [],
Having: null,
IsDistinct: false,
#pragma warning disable EF1001
Orderings: [{ Expression: ColumnExpression { Name: ValuesOrderingColumnName }, IsAscending: true }],
#pragma warning restore EF1001
Limit: null,
Offset: null
} selectExpression
when TranslateExpression(index) is { } translatedIndex
&& _sqlServerSingletonOptions.SupportsJsonFunctions
&& TryTranslate(selectExpression, valuesParameter, path: null, translatedIndex, out var result):
return result;
// Index on JSON array
case SelectExpression
{
Tables: [SqlServerOpenJsonExpression openJson],
Predicate: null,
GroupBy: [],
Having: null,
IsDistinct: false,
Limit: null,
Offset: null,
// We can only apply the indexing if the JSON array is ordered by its natural ordered, i.e. by the "key" column that
// we created in TranslateCollection. For example, if another ordering has been applied (e.g. by the JSON elements
// themselves), we can no longer simply index into the original array.
Orderings:
[
{
Expression: SqlUnaryExpression
{
OperatorType: ExpressionType.Convert,
Operand: ColumnExpression { Name: "key", TableAlias: var orderingTableAlias }
}
}
]
} selectExpression
when orderingTableAlias == openJson.Alias
&& TranslateExpression(index) is { } translatedIndex
&& TryTranslate(selectExpression, openJson.Json, openJson.Path, translatedIndex, out var result):
return result;
}
}
return base.TranslateElementAtOrDefault(source, index, returnDefault);
bool TryTranslate(
SelectExpression selectExpression,
SqlExpression jsonColumn,
IReadOnlyList<PathSegment>? path,
SqlExpression translatedIndex,
[NotNullWhen(true)] out ShapedQueryExpression? result)
{
// Extract the column projected out of the source, and simplify the subquery to a simple JsonScalarExpression
if (!TryGetProjection(source, selectExpression, out var projection))
{
result = null;
return false;
}
// OPENJSON's value column is an nvarchar(max); if this is a collection column whose type mapping is know, the projection
// contains a CAST node which we unwrap
var projectionColumn = projection switch
{
ColumnExpression c => c,
SqlUnaryExpression { OperatorType: ExpressionType.Convert, Operand: ColumnExpression c } => c,
_ => null
};
if (projectionColumn is null)
{
result = null;
return false;
}
// If the inner expression happens to itself be a JsonScalarExpression, simply append the paths to avoid creating
// JSON_VALUE within JSON_VALUE.
var (json, newPath) = jsonColumn is JsonScalarExpression innerJsonScalarExpression
? (innerJsonScalarExpression.Json, new List<PathSegment>(innerJsonScalarExpression.Path))
: (jsonColumn, []);
if (path is not null)
{
newPath.AddRange(path);
}
newPath.Add(new(translatedIndex));
var translation = new JsonScalarExpression(
json,
newPath,
projection.Type,
projection.TypeMapping,
projectionColumn.IsNullable);
#pragma warning disable EF1001
result = source.UpdateQueryExpression(new SelectExpression(translation, _queryCompilationContext.SqlAliasManager));
#pragma warning restore EF1001
return true;
}
}
/// <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
/// any release. You should only use it directly in your code with extreme caution and knowing that
/// doing so can result in application failures when updating to a new Entity Framework Core release.
/// </summary>
protected override ShapedQueryExpression? TranslateTake(ShapedQueryExpression source, Expression count)
{
var selectExpression = (SelectExpression)source.QueryExpression;
// When VECTOR_SEARCH() is present and an Offset has already been applied (i.e. Skip().Take()), we need to ensure that the
// generated SQL uses TOP WITH APPROXIMATE in a subquery, rather than the default OFFSET...FETCH which doesn't use the
// vector index. We push down a subquery with TOP(Skip + Take) WITH APPROXIMATE, and apply OFFSET...FETCH on the outer query.
if (selectExpression is { Offset: { } existingOffset }
&& selectExpression.Tables.Any(t => t.UnwrapJoin() is TableValuedFunctionExpression { Name: "VECTOR_SEARCH" }))
{
var translation = TranslateExpression(count);
if (translation == null)
{
return null;
}
var combinedLimit = _sqlExpressionFactory.Add(existingOffset, translation);
#pragma warning disable EF1001 // Internal EF Core API usage.
// Clear the offset so the inner subquery uses TOP(M+N) instead of OFFSET...FETCH
selectExpression.SetOffset(null);
selectExpression.SetLimit(combinedLimit);
// Push down: inner gets TOP(Skip+Take) WITH APPROXIMATE, outer is clean
selectExpression.PushdownIntoSubquery();
// Apply the original offset and take on the outer query as OFFSET...FETCH
selectExpression.ApplyOffset(existingOffset);
selectExpression.SetLimit(translation);
#pragma warning restore EF1001 // Internal EF Core API usage.
return source;
}
return base.TranslateTake(source, count);
}
/// <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
/// any release. You should only use it directly in your code with extreme caution and knowing that
/// doing so can result in application failures when updating to a new Entity Framework Core release.
/// </summary>
protected override bool IsNaturallyOrdered(SelectExpression selectExpression)
=> selectExpression switch
{
// OPENJSON rows are naturally ordered by their "key" column (array index), ascending.
// EF adds this ordering implicitly when expanding JSON arrays; treat it as natural to avoid spurious
// "Distinct after OrderBy without row limiting operator" warnings.
{
Tables: [SqlServerOpenJsonExpression openJsonExpression, ..],
Orderings:
[
{
Expression: SqlUnaryExpression
{
OperatorType: ExpressionType.Convert,
Operand: ColumnExpression { Name: "key", TableAlias: var openJsonOrderingTableAlias }
},
IsAscending: true
}
]
} when openJsonOrderingTableAlias == openJsonExpression.Alias => true,
// VECTOR_SEARCH() results are naturally ordered by Distance ascending.
// EF adds this ordering implicitly during VectorSearch translation; treat it as natural to avoid spurious
// "Distinct after OrderBy without row limiting operator" warnings.
{
Tables: [TableValuedFunctionExpression { Name: "VECTOR_SEARCH" } vectorSearchFunction, ..],
Orderings:
[
{
Expression: ColumnExpression { Name: "Distance", TableAlias: var vectorSearchOrderingTableAlias },
IsAscending: true
}
]
} when vectorSearchOrderingTableAlias == vectorSearchFunction.Alias => true,
_ => false
};
/// <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
/// any release. You should only use it directly in your code with extreme caution and knowing that
/// doing so can result in application failures when updating to a new Entity Framework Core release.
/// </summary>
protected override bool IsValidSelectExpressionForExecuteDelete(SelectExpression selectExpression)
=> selectExpression.Offset == null
&& selectExpression.GroupBy.Count == 0
&& selectExpression.Having == null
&& selectExpression.Orderings.Count == 0;
#region ExecuteUpdate
/// <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
/// any release. You should only use it directly in your code with extreme caution and knowing that
/// doing so can result in application failures when updating to a new Entity Framework Core release.
/// </summary>
protected override bool IsValidSelectExpressionForExecuteUpdate(
SelectExpression selectExpression,
TableExpressionBase table,
[NotNullWhen(true)] out TableExpression? tableExpression)
{
if (selectExpression is
{
Offset: null,
IsDistinct: false,
GroupBy: [],
Having: null,
Orderings: []
})
{
if (selectExpression.Tables.Count > 1 && table is JoinExpressionBase joinExpressionBase)
{
table = joinExpressionBase.Table;
}
if (table is TableExpression te)
{
tableExpression = te;
return true;
}
}
tableExpression = null;
return false;
}
/// <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
/// any release. You should only use it directly in your code with extreme caution and knowing that
/// doing so can result in application failures when updating to a new Entity Framework Core release.
/// </summary>
#pragma warning disable EF1001 // Internal EF Core API usage.
protected override IReadOnlyList<ColumnValueSetter> TranslateSetters(
ShapedQueryExpression source,
IReadOnlyList<ExecuteUpdateSetter> setters,
out TableExpressionBase targetTable)
{
// SQL Server 2025 introduced the modify method (https://learn.microsoft.com/sql/t-sql/data-types/json-data-type#modify-method),
// which works only with the JSON data type introduced in that same version.
// As of now, modify is only usable if a single property is being modified in the JSON document - it's impossible to modify multiple properties.
// To work around this limitation, we do a first translation pass which may generate multiple modify invocations on the same JSON column (and
// which would fail if sent to SQL Server); we then detect this case, populate _columnsWithMultipleSetters with the problematic columns, and then
// retranslate, using the less efficient JSON_MODIFY() instead for those columns.
_columnsWithMultipleSetters = new();
var translatedSetters = base.TranslateSetters(source, setters, out targetTable);
_columnsWithMultipleSetters = new(translatedSetters.GroupBy(s => s.Column).Where(g => g.Count() > 1).Select(g => g.Key));
if (_columnsWithMultipleSetters.Count > 0)
{
translatedSetters = base.TranslateSetters(source, setters, out targetTable);
}
return translatedSetters;
}
#pragma warning restore EF1001 // Internal EF Core API usage.
/// <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
/// any release. You should only use it directly in your code with extreme caution and knowing that
/// doing so can result in application failures when updating to a new Entity Framework Core release.
/// </summary>
protected override bool TrySerializeScalarToJson(
JsonScalarExpression target,
SqlExpression value,
[NotNullWhen(true)] out SqlExpression? jsonValue)
{
#pragma warning disable EF9002 // TrySerializeScalarToJson is experimental
// The base implementation handles the types natively supported in JSON (int, string, bool), as well
// as constants/parameters.
if (base.TrySerializeScalarToJson(target, value, out jsonValue))
{
return true;
}
#pragma warning restore EF9002
// geometry/geography are "user-defined types" and therefore not supported by JSON_OBJECT(), which we
// use below for serializing arbitrary relational expressions to JSON. Special-case them and serialize
// as WKT.
if (value.TypeMapping?.StoreType is "geometry" or "geography")
{
jsonValue = _sqlExpressionFactory.Function(
instance: value,
"STAsText",