Skip to content

Commit ba34317

Browse files
authored
Prune PgTableValuedFunctionExpression's projection (#3090)
Closes #3088
1 parent 2403c06 commit ba34317

8 files changed

+117
-98
lines changed

src/EFCore.PG/Extensions/ExpressionVisitorExtensions.cs

+5-4
Original file line numberDiff line numberDiff line change
@@ -16,20 +16,21 @@ internal static class ExpressionVisitorExtensions
1616
/// <returns>
1717
/// The modified expression list, if any of the elements were modified; otherwise, returns the original expression list.
1818
/// </returns>
19-
public static IReadOnlyList<Expression> Visit(this ExpressionVisitor visitor, IReadOnlyList<Expression> nodes)
19+
public static IReadOnlyList<T> Visit<T>(this ExpressionVisitor visitor, IReadOnlyList<T> nodes)
20+
where T : Expression
2021
{
21-
Expression[]? newNodes = null;
22+
T[]? newNodes = null;
2223
for (int i = 0, n = nodes.Count; i < n; i++)
2324
{
24-
var node = visitor.Visit(nodes[i]);
25+
var node = (T)visitor.Visit(nodes[i]);
2526

2627
if (newNodes is not null)
2728
{
2829
newNodes[i] = node;
2930
}
3031
else if (!ReferenceEquals(node, nodes[i]))
3132
{
32-
newNodes = new Expression[n];
33+
newNodes = new T[n];
3334
for (var j = 0; j < i; j++)
3435
{
3536
newNodes[j] = nodes[j];

src/EFCore.PG/Query/Expressions/Internal/PgTableValuedFunctionExpression.cs

+9
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,15 @@ public override TableExpressionBase Clone(string? alias, ExpressionVisitor cloni
9696
public override PgTableValuedFunctionExpression WithAlias(string newAlias)
9797
=> new(newAlias, Name, Arguments, ColumnInfos, WithOrdinality);
9898

99+
/// <summary>
100+
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
101+
/// the same compatibility standards as public APIs. It may be changed or removed without notice in
102+
/// any release. You should only use it directly in your code with extreme caution and knowing that
103+
/// doing so can result in application failures when updating to a new Entity Framework Core release.
104+
/// </summary>
105+
public virtual PgTableValuedFunctionExpression WithColumnInfos(IReadOnlyList<ColumnInfo> columnInfos)
106+
=> new(Alias, Name, Arguments, columnInfos, WithOrdinality);
107+
99108
/// <inheritdoc />
100109
protected override void Print(ExpressionPrinter expressionPrinter)
101110
{

src/EFCore.PG/Query/Expressions/Internal/PgUnnestExpression.cs

+21-3
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,12 @@ public virtual string ColumnName
5454
/// doing so can result in application failures when updating to a new Entity Framework Core release.
5555
/// </summary>
5656
public PgUnnestExpression(string alias, SqlExpression array, string columnName, bool withOrdinality = true)
57-
: base(alias, "unnest", new[] { array }, new[] { new ColumnInfo(columnName) }, withOrdinality)
57+
: this(alias, array, new ColumnInfo(columnName), withOrdinality)
58+
{
59+
}
60+
61+
private PgUnnestExpression(string alias, SqlExpression array, ColumnInfo? columnInfo, bool withOrdinality = true)
62+
: base(alias, "unnest", new[] { array }, columnInfo is null ? null : [columnInfo.Value], withOrdinality)
5863
{
5964
}
6065

@@ -91,6 +96,19 @@ public override TableExpressionBase Clone(string? alias, ExpressionVisitor cloni
9196
=> new PgUnnestExpression(alias!, (SqlExpression)cloningExpressionVisitor.Visit(Array), ColumnName, WithOrdinality);
9297

9398
/// <inheritdoc />
94-
public override PgTableValuedFunctionExpression WithAlias(string newAlias)
95-
=> new(newAlias, Name, Arguments, ColumnInfos, WithOrdinality);
99+
public override PgUnnestExpression WithAlias(string newAlias)
100+
=> new(newAlias, Array, ColumnName, WithOrdinality);
101+
102+
/// <inheritdoc />
103+
public override PgUnnestExpression WithColumnInfos(IReadOnlyList<ColumnInfo> columnInfos)
104+
=> new(
105+
Alias,
106+
Array,
107+
columnInfos switch
108+
{
109+
[] => null,
110+
[var columnInfo] => columnInfo,
111+
_ => throw new ArgumentException()
112+
},
113+
WithOrdinality);
96114
}

src/EFCore.PG/Query/Internal/NpgsqlQueryTranslationPostprocessor.cs

+11
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@ namespace Npgsql.EntityFrameworkCore.PostgreSQL.Query.Internal;
88
/// </summary>
99
public class NpgsqlQueryTranslationPostprocessor : RelationalQueryTranslationPostprocessor
1010
{
11+
private readonly NpgsqlSqlTreePruner _pruner = new();
12+
1113
/// <summary>
1214
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
1315
/// the same compatibility standards as public APIs. It may be changed or removed without notice in
@@ -37,4 +39,13 @@ public override Expression Process(Expression query)
3739

3840
return result;
3941
}
42+
43+
/// <summary>
44+
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
45+
/// the same compatibility standards as public APIs. It may be changed or removed without notice in
46+
/// any release. You should only use it directly in your code with extreme caution and knowing that
47+
/// doing so can result in application failures when updating to a new Entity Framework Core release.
48+
/// </summary>
49+
protected override Expression Prune(Expression query)
50+
=> _pruner.Prune(query);
4051
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
using Npgsql.EntityFrameworkCore.PostgreSQL.Query.Expressions.Internal;
2+
using ColumnInfo = Npgsql.EntityFrameworkCore.PostgreSQL.Query.Expressions.Internal.PgTableValuedFunctionExpression.ColumnInfo;
3+
4+
namespace Npgsql.EntityFrameworkCore.PostgreSQL.Query.Internal;
5+
6+
/// <summary>
7+
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
8+
/// the same compatibility standards as public APIs. It may be changed or removed without notice in
9+
/// any release. You should only use it directly in your code with extreme caution and knowing that
10+
/// doing so can result in application failures when updating to a new Entity Framework Core release.
11+
/// </summary>
12+
public class NpgsqlSqlTreePruner : SqlTreePruner
13+
{
14+
/// <summary>
15+
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
16+
/// the same compatibility standards as public APIs. It may be changed or removed without notice in
17+
/// any release. You should only use it directly in your code with extreme caution and knowing that
18+
/// doing so can result in application failures when updating to a new Entity Framework Core release.
19+
/// </summary>
20+
protected override Expression VisitExtension(Expression node)
21+
{
22+
switch (node)
23+
{
24+
case PgTableValuedFunctionExpression { ColumnInfos: IReadOnlyList<ColumnInfo> columnInfos } tvf:
25+
var arguments = this.Visit(tvf.Arguments);
26+
27+
List<ColumnInfo>? newColumnInfos = null;
28+
29+
if (ReferencedColumnMap.TryGetValue(tvf.Alias, out var referencedAliases))
30+
{
31+
for (var i = 0; i < columnInfos.Count; i++)
32+
{
33+
if (referencedAliases.Contains(columnInfos[i].Name))
34+
{
35+
newColumnInfos?.Add(columnInfos[i]);
36+
}
37+
else if (newColumnInfos is null)
38+
{
39+
newColumnInfos = [];
40+
for (var j = 0; j < i; j++)
41+
{
42+
newColumnInfos.Add(columnInfos[j]);
43+
}
44+
}
45+
}
46+
}
47+
48+
return tvf
49+
.Update(arguments)
50+
.WithColumnInfos(newColumnInfos ?? columnInfos);
51+
52+
default:
53+
return base.VisitExtension(node);
54+
}
55+
}
56+
}

src/EFCore.PG/Query/Internal/NpgsqlUnnestPostprocessor.cs

+4-4
Original file line numberDiff line numberDiff line change
@@ -36,16 +36,16 @@ public class NpgsqlUnnestPostprocessor : ExpressionVisitor
3636
for (var i = 0; i < selectExpression.Tables.Count; i++)
3737
{
3838
var table = selectExpression.Tables[i];
39+
var unwrappedTable = table.UnwrapJoin();
3940

4041
// Find any unnest table which does not have any references to its ordinality column in the projection or orderings
4142
// (this is where they may appear when a column is an identifier).
42-
var unnest = table as PgUnnestExpression ?? (table as JoinExpressionBase)?.Table as PgUnnestExpression;
43-
if (unnest is not null
43+
if (unwrappedTable is PgUnnestExpression unnest
4444
&& !selectExpression.Orderings.Select(o => o.Expression)
4545
.Concat(selectExpression.Projection.Select(p => p.Expression))
4646
.Any(
47-
p => p is ColumnExpression { Name: "ordinality", } ordinalityColumn
48-
&& ordinalityColumn.TableAlias == table.Alias))
47+
p => p is ColumnExpression { Name: "ordinality" } ordinalityColumn
48+
&& ordinalityColumn.TableAlias == unwrappedTable.Alias))
4949
{
5050
if (newTables is null)
5151
{

test/EFCore.PG.FunctionalTests/Query/JsonQueryNpgsqlTest.cs

+10-86
Original file line numberDiff line numberDiff line change
@@ -1032,16 +1032,7 @@ public override async Task Json_collection_Any_with_predicate(bool async)
10321032
FROM "JsonEntitiesBasic" AS j
10331033
WHERE EXISTS (
10341034
SELECT 1
1035-
FROM ROWS FROM (jsonb_to_recordset(j."OwnedReferenceRoot" -> 'OwnedCollectionBranch') AS (
1036-
"Date" timestamp without time zone,
1037-
"Enum" integer,
1038-
"Enums" integer[],
1039-
"Fraction" numeric(18,2),
1040-
"NullableEnum" integer,
1041-
"NullableEnums" integer[],
1042-
"OwnedCollectionLeaf" jsonb,
1043-
"OwnedReferenceLeaf" jsonb
1044-
)) WITH ORDINALITY AS o
1035+
FROM ROWS FROM (jsonb_to_recordset(j."OwnedReferenceRoot" -> 'OwnedCollectionBranch') AS ("OwnedReferenceLeaf" jsonb)) WITH ORDINALITY AS o
10451036
WHERE (o."OwnedReferenceLeaf" ->> 'SomethingSomething') = 'e1_r_c1_r')
10461037
""");
10471038
}
@@ -1059,11 +1050,7 @@ SELECT o."OwnedReferenceLeaf" ->> 'SomethingSomething'
10591050
FROM ROWS FROM (jsonb_to_recordset(j."OwnedReferenceRoot" -> 'OwnedCollectionBranch') AS (
10601051
"Date" timestamp without time zone,
10611052
"Enum" integer,
1062-
"Enums" integer[],
10631053
"Fraction" numeric(18,2),
1064-
"NullableEnum" integer,
1065-
"NullableEnums" integer[],
1066-
"OwnedCollectionLeaf" jsonb,
10671054
"OwnedReferenceLeaf" jsonb
10681055
)) WITH ORDINALITY AS o
10691056
WHERE o."Enum" = -3
@@ -1083,16 +1070,7 @@ public override async Task Json_collection_Skip(bool async)
10831070
SELECT o0.c
10841071
FROM (
10851072
SELECT o."OwnedReferenceLeaf" ->> 'SomethingSomething' AS c
1086-
FROM ROWS FROM (jsonb_to_recordset(j."OwnedReferenceRoot" -> 'OwnedCollectionBranch') AS (
1087-
"Date" timestamp without time zone,
1088-
"Enum" integer,
1089-
"Enums" integer[],
1090-
"Fraction" numeric(18,2),
1091-
"NullableEnum" integer,
1092-
"NullableEnums" integer[],
1093-
"OwnedCollectionLeaf" jsonb,
1094-
"OwnedReferenceLeaf" jsonb
1095-
)) WITH ORDINALITY AS o
1073+
FROM ROWS FROM (jsonb_to_recordset(j."OwnedReferenceRoot" -> 'OwnedCollectionBranch') AS ("OwnedReferenceLeaf" jsonb)) WITH ORDINALITY AS o
10961074
OFFSET 1
10971075
) AS o0
10981076
LIMIT 1 OFFSET 0) = 'e1_r_c2_r'
@@ -1114,11 +1092,7 @@ SELECT o0.c
11141092
FROM ROWS FROM (jsonb_to_recordset(j."OwnedReferenceRoot" -> 'OwnedCollectionBranch') AS (
11151093
"Date" timestamp without time zone,
11161094
"Enum" integer,
1117-
"Enums" integer[],
11181095
"Fraction" numeric(18,2),
1119-
"NullableEnum" integer,
1120-
"NullableEnums" integer[],
1121-
"OwnedCollectionLeaf" jsonb,
11221096
"OwnedReferenceLeaf" jsonb
11231097
)) WITH ORDINALITY AS o
11241098
ORDER BY o."Date" DESC NULLS LAST
@@ -1166,14 +1140,7 @@ public override async Task Json_collection_within_collection_Count(bool async)
11661140
FROM "JsonEntitiesBasic" AS j
11671141
WHERE EXISTS (
11681142
SELECT 1
1169-
FROM ROWS FROM (jsonb_to_recordset(j."OwnedCollectionRoot") AS (
1170-
"Name" text,
1171-
"Names" text[],
1172-
"Number" integer,
1173-
"Numbers" integer[],
1174-
"OwnedCollectionBranch" jsonb,
1175-
"OwnedReferenceBranch" jsonb
1176-
)) WITH ORDINALITY AS o
1143+
FROM ROWS FROM (jsonb_to_recordset(j."OwnedCollectionRoot") AS ("OwnedCollectionBranch" jsonb)) WITH ORDINALITY AS o
11771144
WHERE (
11781145
SELECT count(*)::int
11791146
FROM ROWS FROM (jsonb_to_recordset(o."OwnedCollectionBranch") AS (
@@ -1220,11 +1187,7 @@ public override async Task Json_collection_in_projection_with_anonymous_projecti
12201187
FROM "JsonEntitiesBasic" AS j
12211188
LEFT JOIN LATERAL ROWS FROM (jsonb_to_recordset(j."OwnedCollectionRoot") AS (
12221189
"Name" text,
1223-
"Names" text[],
1224-
"Number" integer,
1225-
"Numbers" integer[],
1226-
"OwnedCollectionBranch" jsonb,
1227-
"OwnedReferenceBranch" jsonb
1190+
"Number" integer
12281191
)) WITH ORDINALITY AS o ON TRUE
12291192
ORDER BY j."Id" NULLS FIRST
12301193
""");
@@ -1242,11 +1205,7 @@ LEFT JOIN LATERAL (
12421205
SELECT o."Name", o."Number", o.ordinality
12431206
FROM ROWS FROM (jsonb_to_recordset(j."OwnedCollectionRoot") AS (
12441207
"Name" text,
1245-
"Names" text[],
1246-
"Number" integer,
1247-
"Numbers" integer[],
1248-
"OwnedCollectionBranch" jsonb,
1249-
"OwnedReferenceBranch" jsonb
1208+
"Number" integer
12501209
)) WITH ORDINALITY AS o
12511210
WHERE o."Name" = 'Foo'
12521211
) AS o0 ON TRUE
@@ -1268,9 +1227,7 @@ FROM ROWS FROM (jsonb_to_recordset(j."OwnedCollectionRoot") AS (
12681227
"Name" text,
12691228
"Names" text[],
12701229
"Number" integer,
1271-
"Numbers" integer[],
1272-
"OwnedCollectionBranch" jsonb,
1273-
"OwnedReferenceBranch" jsonb
1230+
"Numbers" integer[]
12741231
)) WITH ORDINALITY AS o
12751232
WHERE o."Name" = 'Foo'
12761233
) AS o0 ON TRUE
@@ -1312,14 +1269,7 @@ public override async Task Json_nested_collection_filter_in_projection(bool asyn
13121269
FROM "JsonEntitiesBasic" AS j
13131270
LEFT JOIN LATERAL (
13141271
SELECT o.ordinality, o1."Id", o1."Date", o1."Enum", o1."Enums", o1."Fraction", o1."NullableEnum", o1."NullableEnums", o1.c, o1.c0, o1.ordinality AS ordinality0
1315-
FROM ROWS FROM (jsonb_to_recordset(j."OwnedCollectionRoot") AS (
1316-
"Name" text,
1317-
"Names" text[],
1318-
"Number" integer,
1319-
"Numbers" integer[],
1320-
"OwnedCollectionBranch" jsonb,
1321-
"OwnedReferenceBranch" jsonb
1322-
)) WITH ORDINALITY AS o
1272+
FROM ROWS FROM (jsonb_to_recordset(j."OwnedCollectionRoot") AS ("OwnedCollectionBranch" jsonb)) WITH ORDINALITY AS o
13231273
LEFT JOIN LATERAL (
13241274
SELECT j."Id", o0."Date", o0."Enum", o0."Enums", o0."Fraction", o0."NullableEnum", o0."NullableEnums", o0."OwnedCollectionLeaf" AS c, o0."OwnedReferenceLeaf" AS c0, o0.ordinality
13251275
FROM ROWS FROM (jsonb_to_recordset(o."OwnedCollectionBranch") AS (
@@ -1349,21 +1299,12 @@ public override async Task Json_nested_collection_anonymous_projection_in_projec
13491299
FROM "JsonEntitiesBasic" AS j
13501300
LEFT JOIN LATERAL (
13511301
SELECT o.ordinality, o0."Date" AS c, o0."Enum" AS c0, o0."Enums" AS c1, o0."Fraction" AS c2, o0."OwnedReferenceLeaf" AS c3, j."Id", o0."OwnedCollectionLeaf" AS c4, o0.ordinality AS ordinality0
1352-
FROM ROWS FROM (jsonb_to_recordset(j."OwnedCollectionRoot") AS (
1353-
"Name" text,
1354-
"Names" text[],
1355-
"Number" integer,
1356-
"Numbers" integer[],
1357-
"OwnedCollectionBranch" jsonb,
1358-
"OwnedReferenceBranch" jsonb
1359-
)) WITH ORDINALITY AS o
1302+
FROM ROWS FROM (jsonb_to_recordset(j."OwnedCollectionRoot") AS ("OwnedCollectionBranch" jsonb)) WITH ORDINALITY AS o
13601303
LEFT JOIN LATERAL ROWS FROM (jsonb_to_recordset(o."OwnedCollectionBranch") AS (
13611304
"Date" timestamp without time zone,
13621305
"Enum" integer,
13631306
"Enums" integer[],
13641307
"Fraction" numeric(18,2),
1365-
"NullableEnum" integer,
1366-
"NullableEnums" integer[],
13671308
"OwnedCollectionLeaf" jsonb,
13681309
"OwnedReferenceLeaf" jsonb
13691310
)) WITH ORDINALITY AS o0 ON TRUE
@@ -1434,10 +1375,7 @@ LEFT JOIN LATERAL (
14341375
SELECT o."OwnedReferenceBranch" AS c, j."Id", o.ordinality, o."Name" AS c0
14351376
FROM ROWS FROM (jsonb_to_recordset(j."OwnedCollectionRoot") AS (
14361377
"Name" text,
1437-
"Names" text[],
14381378
"Number" integer,
1439-
"Numbers" integer[],
1440-
"OwnedCollectionBranch" jsonb,
14411379
"OwnedReferenceBranch" jsonb
14421380
)) WITH ORDINALITY AS o
14431381
ORDER BY o."Name" NULLS FIRST
@@ -1520,14 +1458,7 @@ FROM ROWS FROM (jsonb_to_recordset(j."OwnedCollectionRoot") AS (
15201458
) AS o1 ON TRUE
15211459
LEFT JOIN LATERAL (
15221460
SELECT o2.ordinality, o5."Id", o5."Date", o5."Enum", o5."Enums", o5."Fraction", o5."NullableEnum", o5."NullableEnums", o5.c, o5.c0, o5.ordinality AS ordinality0
1523-
FROM ROWS FROM (jsonb_to_recordset(j."OwnedCollectionRoot") AS (
1524-
"Name" text,
1525-
"Names" text[],
1526-
"Number" integer,
1527-
"Numbers" integer[],
1528-
"OwnedCollectionBranch" jsonb,
1529-
"OwnedReferenceBranch" jsonb
1530-
)) WITH ORDINALITY AS o2
1461+
FROM ROWS FROM (jsonb_to_recordset(j."OwnedCollectionRoot") AS ("OwnedCollectionBranch" jsonb)) WITH ORDINALITY AS o2
15311462
LEFT JOIN LATERAL (
15321463
SELECT j."Id", o3."Date", o3."Enum", o3."Enums", o3."Fraction", o3."NullableEnum", o3."NullableEnums", o3."OwnedCollectionLeaf" AS c, o3."OwnedReferenceLeaf" AS c0, o3.ordinality
15331464
FROM ROWS FROM (jsonb_to_recordset(o2."OwnedCollectionBranch") AS (
@@ -1756,14 +1687,7 @@ public override async Task Json_collection_Select_entity_in_anonymous_object_Ele
17561687
FROM "JsonEntitiesBasic" AS j
17571688
LEFT JOIN LATERAL (
17581689
SELECT o."OwnedReferenceBranch" AS c, j."Id", 1 AS c0
1759-
FROM ROWS FROM (jsonb_to_recordset(j."OwnedCollectionRoot") AS (
1760-
"Name" text,
1761-
"Names" text[],
1762-
"Number" integer,
1763-
"Numbers" integer[],
1764-
"OwnedCollectionBranch" jsonb,
1765-
"OwnedReferenceBranch" jsonb
1766-
)) WITH ORDINALITY AS o
1690+
FROM ROWS FROM (jsonb_to_recordset(j."OwnedCollectionRoot") AS ("OwnedReferenceBranch" jsonb)) WITH ORDINALITY AS o
17671691
LIMIT 1 OFFSET 0
17681692
) AS o0 ON TRUE
17691693
ORDER BY j."Id" NULLS FIRST

test/EFCore.PG.FunctionalTests/Query/PrimitiveCollectionsQueryNpgsqlTest.cs

+1-1
Original file line numberDiff line numberDiff line change
@@ -1312,7 +1312,7 @@ public override async Task Project_collection_of_ints_with_distinct(bool async)
13121312
FROM "PrimitiveCollectionsEntity" AS p
13131313
LEFT JOIN LATERAL (
13141314
SELECT DISTINCT i.value
1315-
FROM unnest(p."Ints") WITH ORDINALITY AS i(value)
1315+
FROM unnest(p."Ints") AS i(value)
13161316
) AS i0 ON TRUE
13171317
ORDER BY p."Id" NULLS FIRST
13181318
""");

0 commit comments

Comments
 (0)