@@ -15,8 +15,8 @@ namespace Microsoft.EntityFrameworkCore.SqlServer.Query.Internal;
15
15
/// </summary>
16
16
public class SqlServerQueryTranslationPostprocessor : RelationalQueryTranslationPostprocessor
17
17
{
18
- private readonly SkipWithoutOrderByInSplitQueryVerifyingExpressionVisitor
19
- _skipWithoutOrderByInSplitQueryVerifyingExpressionVisitor = new ( ) ;
18
+ private readonly OpenJsonPostprocessor _openJsonPostprocessor ;
19
+ private readonly SkipWithoutOrderByInSplitQueryVerifier _skipWithoutOrderByInSplitQueryVerifier = new ( ) ;
20
20
21
21
/// <summary>
22
22
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
@@ -27,9 +27,11 @@ private readonly SkipWithoutOrderByInSplitQueryVerifyingExpressionVisitor
27
27
public SqlServerQueryTranslationPostprocessor (
28
28
QueryTranslationPostprocessorDependencies dependencies ,
29
29
RelationalQueryTranslationPostprocessorDependencies relationalDependencies ,
30
- QueryCompilationContext queryCompilationContext )
30
+ QueryCompilationContext queryCompilationContext ,
31
+ IRelationalTypeMappingSource typeMappingSource )
31
32
: base ( dependencies , relationalDependencies , queryCompilationContext )
32
33
{
34
+ _openJsonPostprocessor = new ( typeMappingSource , relationalDependencies . SqlExpressionFactory ) ;
33
35
}
34
36
35
37
/// <summary>
@@ -40,14 +42,15 @@ public SqlServerQueryTranslationPostprocessor(
40
42
/// </summary>
41
43
public override Expression Process ( Expression query )
42
44
{
43
- var result = base . Process ( query ) ;
45
+ query = base . Process ( query ) ;
44
46
45
- _skipWithoutOrderByInSplitQueryVerifyingExpressionVisitor . Visit ( result ) ;
47
+ query = _openJsonPostprocessor . Process ( query ) ;
48
+ _skipWithoutOrderByInSplitQueryVerifier . Visit ( query ) ;
46
49
47
- return result ;
50
+ return query ;
48
51
}
49
52
50
- private sealed class SkipWithoutOrderByInSplitQueryVerifyingExpressionVisitor : ExpressionVisitor
53
+ private sealed class SkipWithoutOrderByInSplitQueryVerifier : ExpressionVisitor
51
54
{
52
55
[ return : NotNullIfNotNull ( "expression" ) ]
53
56
public override Expression ? Visit ( Expression ? expression )
@@ -68,9 +71,7 @@ private sealed class SkipWithoutOrderByInSplitQueryVerifyingExpressionVisitor :
68
71
69
72
return relationalSplitCollectionShaperExpression ;
70
73
71
- case SelectExpression selectExpression
72
- when selectExpression . Offset != null
73
- && selectExpression . Orderings . Count == 0 :
74
+ case SelectExpression { Offset : not null , Orderings . Count : 0 } :
74
75
throw new InvalidOperationException ( SqlServerStrings . SplitQueryOffsetWithoutOrderBy ) ;
75
76
76
77
case NonQueryExpression nonQueryExpression :
@@ -81,4 +82,103 @@ private sealed class SkipWithoutOrderByInSplitQueryVerifyingExpressionVisitor :
81
82
}
82
83
}
83
84
}
85
+
86
+ /// <summary>
87
+ /// Converts <see cref="SqlServerOpenJsonExpression" /> expressions with WITH (the default) to OPENJSON without WITH when an
88
+ /// ordering still exists on the [key] column, i.e. when the ordering of the original JSON array needs to be preserved
89
+ /// (e.g. limit/offset).
90
+ /// </summary>
91
+ private sealed class OpenJsonPostprocessor : ExpressionVisitor
92
+ {
93
+ private readonly IRelationalTypeMappingSource _typeMappingSource ;
94
+ private readonly ISqlExpressionFactory _sqlExpressionFactory ;
95
+ private readonly Dictionary < ( SqlServerOpenJsonExpression , string ) , RelationalTypeMapping > _castsToApply = new ( ) ;
96
+
97
+ public OpenJsonPostprocessor ( IRelationalTypeMappingSource typeMappingSource , ISqlExpressionFactory sqlExpressionFactory )
98
+ => ( _typeMappingSource , _sqlExpressionFactory ) = ( typeMappingSource , sqlExpressionFactory ) ;
99
+
100
+ public Expression Process ( Expression expression )
101
+ {
102
+ _castsToApply . Clear ( ) ;
103
+ return Visit ( expression ) ;
104
+ }
105
+
106
+ [ return : NotNullIfNotNull ( "expression" ) ]
107
+ public override Expression ? Visit ( Expression ? expression )
108
+ {
109
+ switch ( expression )
110
+ {
111
+ case ShapedQueryExpression shapedQueryExpression :
112
+ return shapedQueryExpression . UpdateQueryExpression ( Visit ( shapedQueryExpression . QueryExpression ) ) ;
113
+
114
+ case SelectExpression
115
+ {
116
+ Tables : [ SqlServerOpenJsonExpression { ColumnInfos : not null } openJsonExpression , ..] ,
117
+ Orderings :
118
+ [
119
+ {
120
+ Expression : SqlUnaryExpression
121
+ {
122
+ OperatorType : ExpressionType . Convert ,
123
+ Operand : ColumnExpression { Name : "key" , Table : var keyColumnTable }
124
+ }
125
+ }
126
+ ]
127
+ } selectExpression
128
+ when keyColumnTable == openJsonExpression :
129
+ {
130
+ // Remove the WITH clause from the OPENJSON expression
131
+ var newOpenJsonExpression = openJsonExpression . Update (
132
+ openJsonExpression . JsonExpression ,
133
+ openJsonExpression . Path ,
134
+ columnInfos : null ) ;
135
+
136
+ var newTables = selectExpression . Tables . ToArray ( ) ;
137
+ newTables [ 0 ] = newOpenJsonExpression ;
138
+
139
+ var newSelectExpression = selectExpression . Update (
140
+ selectExpression . Projection ,
141
+ newTables ,
142
+ selectExpression . Predicate ,
143
+ selectExpression . GroupBy ,
144
+ selectExpression . Having ,
145
+ selectExpression . Orderings ,
146
+ selectExpression . Limit ,
147
+ selectExpression . Offset ) ;
148
+
149
+ // Record the OPENJSON expression and its projected column(s), along with the store type we just removed from the WITH
150
+ // clause. Then visit the select expression, adding a cast around the matching ColumnExpressions.
151
+ // TODO: Need to pass through the type mapping API for converting the JSON value (nvarchar) to the relational store type
152
+ // (e.g. datetime2), see #30677
153
+ foreach ( var column in openJsonExpression . ColumnInfos )
154
+ {
155
+ var typeMapping = _typeMappingSource . FindMapping ( column . StoreType ) ;
156
+ Check . DebugAssert (
157
+ typeMapping is not null ,
158
+ $ "Could not find mapping for store type { column . StoreType } when converting OPENJSON/WITH") ;
159
+
160
+ _castsToApply . Add ( ( newOpenJsonExpression , column . Name ) , typeMapping ) ;
161
+ }
162
+
163
+ var result = base . Visit ( newSelectExpression ) ;
164
+
165
+ foreach ( var column in openJsonExpression . ColumnInfos )
166
+ {
167
+ _castsToApply . Remove ( ( newOpenJsonExpression , column . Name ) ) ;
168
+ }
169
+
170
+ return result ;
171
+ }
172
+
173
+ case ColumnExpression { Table : SqlServerOpenJsonExpression openJsonTable , Name : var name } columnExpression
174
+ when _castsToApply . TryGetValue ( ( openJsonTable , name ) , out var typeMapping ) :
175
+ {
176
+ return _sqlExpressionFactory . Convert ( columnExpression , columnExpression . Type , typeMapping ) ;
177
+ }
178
+
179
+ default :
180
+ return base . Visit ( expression ) ;
181
+ }
182
+ }
183
+ }
84
184
}
0 commit comments