-
Notifications
You must be signed in to change notification settings - Fork 257
Expand file tree
/
Copy pathNpgsqlShapedQueryExpressionExtensions.cs
More file actions
245 lines (228 loc) · 11 KB
/
NpgsqlShapedQueryExpressionExtensions.cs
File metadata and controls
245 lines (228 loc) · 11 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
using System.Diagnostics.CodeAnalysis;
using Npgsql.EntityFrameworkCore.PostgreSQL.Query.Expressions.Internal;
using Npgsql.EntityFrameworkCore.PostgreSQL.Storage.Internal.Mapping;
namespace Npgsql.EntityFrameworkCore.PostgreSQL.Extensions.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 static class NpgsqlShapedQueryExpressionExtensions
{
/// <summary>
/// If the given <paramref name="source" /> wraps an array-returning expression without any additional clauses (e.g. filter,
/// ordering...), returns that expression.
/// </summary>
/// <remarks>
/// 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.
/// </remarks>
public static bool TryExtractArray(
this ShapedQueryExpression source,
[NotNullWhen(true)] out SqlExpression? array,
bool ignoreOrderings = false,
bool ignorePredicate = false)
=> TryExtractArray(source, out array, out _, ignoreOrderings, ignorePredicate);
/// <summary>
/// If the given <paramref name="source" /> wraps an array-returning expression without any additional clauses (e.g. filter,
/// ordering...), returns that expression.
/// </summary>
/// <remarks>
/// 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.
/// </remarks>
public static bool TryExtractArray(
this ShapedQueryExpression source,
[NotNullWhen(true)] out SqlExpression? array,
[NotNullWhen(true)] out ColumnExpression? projectedColumn,
bool ignoreOrderings = false,
bool ignorePredicate = false)
{
if (source.QueryExpression is SelectExpression
{
Tables: [PgUnnestExpression { Array: var a } unnest],
GroupBy: [],
Having: null,
IsDistinct: false,
Limit: null,
Offset: null
} select
&& (ignorePredicate || select.Predicate is null)
// We can only apply the indexing if the array is ordered by its natural ordered, i.e. by the "ordinality" column that
// we created in TranslatePrimitiveCollection. For example, if another ordering has been applied (e.g. by the array elements
// themselves), we can no longer simply index into the original array.
&& (ignoreOrderings
|| select.Orderings is []
|| (select.Orderings is [{ Expression: ColumnExpression { Name: "ordinality", TableAlias: var orderingTableAlias } }]
&& orderingTableAlias == unnest.Alias))
&& IsPostgresArray(a)
&& TryGetProjectedColumn(source, out var column))
{
array = a;
projectedColumn = column;
return true;
}
array = null;
projectedColumn = null;
return false;
}
/// <summary>
/// If the given <paramref name="source" /> wraps a JSON-array-returning expression without any additional clauses (e.g. filter,
/// ordering...), returns that expression.
/// </summary>
/// <remarks>
/// 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.
/// </remarks>
public static bool TryExtractJsonArray(
this ShapedQueryExpression source,
[NotNullWhen(true)] out SqlExpression? jsonArray,
[NotNullWhen(true)] out SqlExpression? projectedElement,
out bool isElementNullable,
bool ignoreOrderings = false,
bool ignorePredicate = false)
{
if (source.QueryExpression is SelectExpression
{
Tables:
[
TableValuedFunctionExpression
{
Name: "jsonb_array_elements_text" or "json_array_elements_text",
Arguments: [SqlExpression json]
} tvf
],
GroupBy: [],
Having: null,
IsDistinct: false,
Limit: null,
Offset: null
} select
&& (ignorePredicate || select.Predicate is null)
// We can only apply the indexing if the array is ordered by its natural ordered, i.e. by the "ordinality" column that
// we created in TranslatePrimitiveCollection. For example, if another ordering has been applied (e.g. by the array elements
// themselves), we can no longer simply index into the original array.
&& (ignoreOrderings
|| select.Orderings is []
|| (select.Orderings is [{ Expression: ColumnExpression { Name: "ordinality", TableAlias: var orderingTableAlias } }]
&& orderingTableAlias == tvf.Alias))
&& TryGetScalarProjection(source, out var projectedScalar))
{
jsonArray = json;
// The projected ColumnExpression is wrapped in a Convert to apply the element type mapping - unless it happens to be text.
switch (projectedScalar)
{
case SqlUnaryExpression
{
OperatorType: ExpressionType.Convert,
Operand: ColumnExpression { IsNullable: var isNullable }
} convert:
projectedElement = convert;
isElementNullable = isNullable;
return true;
case ColumnExpression { IsNullable: var isNullable } column:
projectedElement = column;
isElementNullable = isNullable;
return true;
default:
throw new UnreachableException();
}
}
jsonArray = null;
projectedElement = null;
isElementNullable = false;
return false;
}
/// <summary>
/// If the given <paramref name="source" /> wraps a <see cref="ValuesExpression" /> without any additional clauses (e.g. filter,
/// ordering...), converts that to a <see cref="NewArrayExpression" /> and returns that.
/// </summary>
/// <remarks>
/// 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.
/// </remarks>
public static bool TryConvertToArray(
this ShapedQueryExpression source,
[NotNullWhen(true)] out SqlExpression? array,
bool ignoreOrderings = false,
bool ignorePredicate = false)
{
if (source.QueryExpression is SelectExpression
{
Tables: [ValuesExpression { ColumnNames: ["_ord", "Value"], RowValues.Count: > 0 } valuesExpression],
GroupBy: [],
Having: null,
IsDistinct: false,
Limit: null,
Offset: null
} select
&& (ignorePredicate || select.Predicate is null)
&& (ignoreOrderings || select.Orderings is []))
{
var elements = new SqlExpression[valuesExpression.RowValues.Count];
for (var i = 0; i < elements.Length; i++)
{
// Skip the first column (_ord) and copy the second (Value)
elements[i] = valuesExpression.RowValues[i].Values[1];
}
array = new PgNewArrayExpression(elements, valuesExpression.RowValues[0].Values[1].Type.MakeArrayType(), typeMapping: null);
return true;
}
array = null;
return false;
}
/// <summary>
/// Checks whether the given expression maps to a PostgreSQL array, as opposed to a multirange type.
/// </summary>
private static bool IsPostgresArray(SqlExpression expression)
=> expression switch
{
{ TypeMapping: NpgsqlArrayTypeMapping } => true,
{ TypeMapping: NpgsqlMultirangeTypeMapping } => false,
{ TypeMapping: NpgsqlJsonTypeMapping } => false,
{ Type: var type } when type.IsMultirange() => false,
_ => true
};
private static bool TryGetProjectedColumn(
ShapedQueryExpression shapedQueryExpression,
[NotNullWhen(true)] out ColumnExpression? projectedColumn)
{
if (TryGetScalarProjection(shapedQueryExpression, out var scalar) && scalar is ColumnExpression column)
{
projectedColumn = column;
return true;
}
projectedColumn = null;
return false;
}
private static bool TryGetScalarProjection(
ShapedQueryExpression shapedQueryExpression,
[NotNullWhen(true)] out SqlExpression? projectedScalar)
{
var shaperExpression = shapedQueryExpression.ShaperExpression;
if (shaperExpression is UnaryExpression { NodeType: ExpressionType.Convert } unaryExpression
&& unaryExpression.Operand.Type.IsNullableType()
&& unaryExpression.Operand.Type.UnwrapNullableType() == unaryExpression.Type)
{
shaperExpression = unaryExpression.Operand;
}
if (shaperExpression is ProjectionBindingExpression projectionBindingExpression
&& shapedQueryExpression.QueryExpression is SelectExpression selectExpression
&& selectExpression.GetProjection(projectionBindingExpression) is SqlExpression scalar)
{
projectedScalar = scalar;
return true;
}
projectedScalar = null;
return false;
}
}