Skip to content

Commit 933bc8d

Browse files
authored
Add ordering by ordinality column for primitive collections (#3209)
Fixes #3207
1 parent 3b71c73 commit 933bc8d

File tree

4 files changed

+238
-230
lines changed

4 files changed

+238
-230
lines changed
Lines changed: 158 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,158 @@
1+
using System.Diagnostics.CodeAnalysis;
2+
using Npgsql.EntityFrameworkCore.PostgreSQL.Query.Expressions.Internal;
3+
using Npgsql.EntityFrameworkCore.PostgreSQL.Storage.Internal.Mapping;
4+
5+
namespace Npgsql.EntityFrameworkCore.PostgreSQL.Extensions.Internal;
6+
7+
/// <summary>
8+
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
9+
/// the same compatibility standards as public APIs. It may be changed or removed without notice in
10+
/// any release. You should only use it directly in your code with extreme caution and knowing that
11+
/// doing so can result in application failures when updating to a new Entity Framework Core release.
12+
/// </summary>
13+
public static class NpgsqlShapedQueryExpressionExtensions
14+
{
15+
/// <summary>
16+
/// If the given <paramref name="source" /> wraps an array-returning expression without any additional clauses (e.g. filter,
17+
/// ordering...), returns that expression.
18+
/// </summary>
19+
/// <remarks>
20+
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
21+
/// the same compatibility standards as public APIs. It may be changed or removed without notice in
22+
/// any release. You should only use it directly in your code with extreme caution and knowing that
23+
/// doing so can result in application failures when updating to a new Entity Framework Core release.
24+
/// </remarks>
25+
public static bool TryExtractArray(
26+
this ShapedQueryExpression source,
27+
[NotNullWhen(true)] out SqlExpression? array,
28+
bool ignoreOrderings = false,
29+
bool ignorePredicate = false)
30+
=> TryExtractArray(source, out array, out _, ignoreOrderings, ignorePredicate);
31+
32+
/// <summary>
33+
/// If the given <paramref name="source" /> wraps an array-returning expression without any additional clauses (e.g. filter,
34+
/// ordering...), returns that expression.
35+
/// </summary>
36+
/// <remarks>
37+
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
38+
/// the same compatibility standards as public APIs. It may be changed or removed without notice in
39+
/// any release. You should only use it directly in your code with extreme caution and knowing that
40+
/// doing so can result in application failures when updating to a new Entity Framework Core release.
41+
/// </remarks>
42+
public static bool TryExtractArray(
43+
this ShapedQueryExpression source,
44+
[NotNullWhen(true)] out SqlExpression? array,
45+
[NotNullWhen(true)] out ColumnExpression? projectedColumn,
46+
bool ignoreOrderings = false,
47+
bool ignorePredicate = false)
48+
{
49+
if (source.QueryExpression is SelectExpression
50+
{
51+
Tables: [PgUnnestExpression { Array: var a } unnest],
52+
GroupBy: [],
53+
Having: null,
54+
IsDistinct: false,
55+
Limit: null,
56+
Offset: null
57+
} select
58+
&& (ignorePredicate || select.Predicate is null)
59+
// We can only apply the indexing if the JSON array is ordered by its natural ordered, i.e. by the "ordinality" column that
60+
// we created in TranslatePrimitiveCollection. For example, if another ordering has been applied (e.g. by the array elements
61+
// themselves), we can no longer simply index into the original array.
62+
&& (ignoreOrderings
63+
|| select.Orderings is []
64+
|| (select.Orderings is [{ Expression: ColumnExpression { Name: "ordinality", TableAlias: var orderingTableAlias } }]
65+
&& orderingTableAlias == unnest.Alias))
66+
&& IsPostgresArray(a)
67+
&& TryGetProjectedColumn(source, out var column))
68+
{
69+
array = a;
70+
projectedColumn = column;
71+
return true;
72+
}
73+
74+
array = null;
75+
projectedColumn = null;
76+
return false;
77+
}
78+
79+
/// <summary>
80+
/// If the given <paramref name="source" /> wraps a <see cref="ValuesExpression" /> without any additional clauses (e.g. filter,
81+
/// ordering...), converts that to a <see cref="NewArrayExpression" /> and returns that.
82+
/// </summary>
83+
/// <remarks>
84+
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
85+
/// the same compatibility standards as public APIs. It may be changed or removed without notice in
86+
/// any release. You should only use it directly in your code with extreme caution and knowing that
87+
/// doing so can result in application failures when updating to a new Entity Framework Core release.
88+
/// </remarks>
89+
public static bool TryConvertValuesToArray(
90+
this ShapedQueryExpression source,
91+
[NotNullWhen(true)] out SqlExpression? array,
92+
bool ignoreOrderings = false,
93+
bool ignorePredicate = false)
94+
{
95+
if (source.QueryExpression is SelectExpression
96+
{
97+
Tables: [ValuesExpression { ColumnNames: ["_ord", "Value"], RowValues.Count: > 0 } valuesExpression],
98+
GroupBy: [],
99+
Having: null,
100+
IsDistinct: false,
101+
Limit: null,
102+
Offset: null
103+
} select
104+
&& (ignorePredicate || select.Predicate is null)
105+
&& (ignoreOrderings || select.Orderings is []))
106+
{
107+
var elements = new SqlExpression[valuesExpression.RowValues.Count];
108+
109+
for (var i = 0; i < elements.Length; i++)
110+
{
111+
// Skip the first column (_ord) and copy the second (Value)
112+
elements[i] = valuesExpression.RowValues[i].Values[1];
113+
}
114+
115+
array = new PgNewArrayExpression(elements, valuesExpression.RowValues[0].Values[1].Type.MakeArrayType(), typeMapping: null);
116+
return true;
117+
}
118+
119+
array = null;
120+
return false;
121+
}
122+
123+
/// <summary>
124+
/// Checks whether the given expression maps to a PostgreSQL array, as opposed to a multirange type.
125+
/// </summary>
126+
private static bool IsPostgresArray(SqlExpression expression)
127+
=> expression switch
128+
{
129+
{ TypeMapping: NpgsqlArrayTypeMapping } => true,
130+
{ TypeMapping: NpgsqlMultirangeTypeMapping } => false,
131+
{ Type: var type } when type.IsMultirange() => false,
132+
_ => true
133+
};
134+
135+
private static bool TryGetProjectedColumn(
136+
ShapedQueryExpression shapedQueryExpression,
137+
[NotNullWhen(true)] out ColumnExpression? projectedColumn)
138+
{
139+
var shaperExpression = shapedQueryExpression.ShaperExpression;
140+
if (shaperExpression is UnaryExpression { NodeType: ExpressionType.Convert } unaryExpression
141+
&& unaryExpression.Operand.Type.IsNullableType()
142+
&& unaryExpression.Operand.Type.UnwrapNullableType() == unaryExpression.Type)
143+
{
144+
shaperExpression = unaryExpression.Operand;
145+
}
146+
147+
if (shaperExpression is ProjectionBindingExpression projectionBindingExpression
148+
&& shapedQueryExpression.QueryExpression is SelectExpression selectExpression
149+
&& selectExpression.GetProjection(projectionBindingExpression) is ColumnExpression c)
150+
{
151+
projectedColumn = c;
152+
return true;
153+
}
154+
155+
projectedColumn = null;
156+
return false;
157+
}
158+
}

0 commit comments

Comments
 (0)