Skip to content

Commit c0b3711

Browse files
authored
Vary array containment translation based on indexes (#3611)
Closes #3546
1 parent 620b8cf commit c0b3711

File tree

6 files changed

+119
-55
lines changed

6 files changed

+119
-55
lines changed

src/EFCore.PG/Metadata/Conventions/NpgsqlRuntimeModelConvention.cs

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -103,7 +103,6 @@ protected override void ProcessIndexAnnotations(
103103

104104
if (!runtime)
105105
{
106-
annotations.Remove(NpgsqlAnnotationNames.IndexMethod);
107106
annotations.Remove(NpgsqlAnnotationNames.IndexOperators);
108107
annotations.Remove(NpgsqlAnnotationNames.IndexSortOrder);
109108
annotations.Remove(NpgsqlAnnotationNames.IndexNullSortOrder);

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

Lines changed: 34 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -597,59 +597,62 @@ protected override ShapedQueryExpression TransformJsonQueryToTable(JsonQueryExpr
597597
{
598598
(translatedItem, array) = _sqlExpressionFactory.ApplyTypeMappingsOnItemAndArray(translatedItem, array);
599599

600-
// When the array is a column, we translate Contains to array @> ARRAY[item]. GIN indexes on array are used, but null
601-
// semantics is impossible without preventing index use.
602-
switch (array)
600+
// We special-case null constant item and use array_position instead, since it does
601+
// nulls correctly (but doesn't use indexes).
602+
// TODO: Better just translate to ANY and handle in nullability processing?
603+
// TODO: once lambda-based caching is implemented, move this to NpgsqlSqlNullabilityProcessor
604+
// (https://github.com/dotnet/efcore/issues/17598) and do for parameters as well.
605+
if (translatedItem is SqlConstantExpression { Value: null })
603606
{
604-
case ColumnExpression:
605-
if (translatedItem is SqlConstantExpression { Value: null })
606-
{
607-
// We special-case null constant item and use array_position instead, since it does
608-
// nulls correctly (but doesn't use indexes)
609-
// TODO: once lambda-based caching is implemented, move this to NpgsqlSqlNullabilityProcessor
610-
// (https://github.com/dotnet/efcore/issues/17598) and do for parameters as well.
611-
return BuildSimplifiedShapedQuery(
612-
source,
613-
_sqlExpressionFactory.IsNotNull(
614-
_sqlExpressionFactory.Function(
615-
"array_position",
616-
[array, translatedItem],
617-
nullable: true,
618-
argumentsPropagateNullability: FalseArrays[2],
619-
typeof(int))));
620-
}
607+
return BuildSimplifiedShapedQuery(
608+
source,
609+
_sqlExpressionFactory.IsNotNull(
610+
_sqlExpressionFactory.Function(
611+
"array_position",
612+
[array, translatedItem],
613+
nullable: true,
614+
argumentsPropagateNullability: FalseArrays[2],
615+
typeof(int))));
616+
}
621617

622-
return BuildSimplifiedShapedQuery(
618+
return array switch
619+
{
620+
// For array columns which have a GIN index, we translate to array containment (with @>) which uses that index.
621+
ColumnExpression { Column: IColumn column }
622+
when column.Table.Indexes
623+
.Any(i =>
624+
i.Columns.Count > 0
625+
&& i.Columns[0] == column
626+
&& i.MappedIndexes.Any(mi => mi.GetMethod()?.Equals("GIN", StringComparison.OrdinalIgnoreCase) == true))
627+
=> BuildSimplifiedShapedQuery(
623628
source,
624629
_sqlExpressionFactory.Contains(
625630
array,
626-
_sqlExpressionFactory.NewArrayOrConstant([translatedItem], array.Type, array.TypeMapping)));
631+
_sqlExpressionFactory.NewArrayOrConstant([translatedItem], array.Type, array.TypeMapping))),
627632

628633
// For constant arrays (new[] { 1, 2, 3 }) or inline arrays (new[] { 1, param, 3 }), don't do anything PG-specific for since
629634
// the general EF Core mechanism is fine for that case: item IN (1, 2, 3).
630-
case SqlConstantExpression or PgNewArrayExpression:
631-
break;
635+
SqlConstantExpression or PgNewArrayExpression
636+
=> base.TranslateContains(source, item),
632637

633638
// Similar to ParameterExpression below, but when a bare subquery is present inside ANY(), PostgreSQL just compares
634639
// against each of its resulting rows (just like IN). To "extract" the array result of the scalar subquery, we need
635640
// to add an explicit cast (see #1803).
636-
case ScalarSubqueryExpression subqueryExpression:
637-
return BuildSimplifiedShapedQuery(
641+
ScalarSubqueryExpression subqueryExpression
642+
=> BuildSimplifiedShapedQuery(
638643
source,
639644
_sqlExpressionFactory.Any(
640645
translatedItem,
641646
_sqlExpressionFactory.Convert(
642647
subqueryExpression, subqueryExpression.Type, subqueryExpression.TypeMapping),
643-
PgAnyOperatorType.Equal));
648+
PgAnyOperatorType.Equal)),
644649

645650
// For ParameterExpression, and for all other cases - e.g. array returned from some function -
646651
// translate to e.SomeText = ANY (@p). This is superior to the general solution which will expand
647652
// parameters to constants, since non-PG SQL does not support arrays.
648653
// Note that this will allow indexes on the item to be used.
649-
default:
650-
return BuildSimplifiedShapedQuery(
651-
source, _sqlExpressionFactory.Any(translatedItem, array, PgAnyOperatorType.Equal));
652-
}
654+
_ => BuildSimplifiedShapedQuery(source, _sqlExpressionFactory.Any(translatedItem, array, PgAnyOperatorType.Equal))
655+
};
653656
}
654657

655658
return base.TranslateContains(source, item);

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

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -168,7 +168,7 @@ public override async Task Array_column_Any_equality_operator(bool async)
168168
"""
169169
SELECT s."Id", s."ArrayContainerEntityId", s."ArrayOfStringConvertedToDelimitedString", s."Byte", s."ByteArray", s."Bytea", s."EnumConvertedToInt", s."EnumConvertedToString", s."IList", s."IntArray", s."IntList", s."ListOfStringConvertedToDelimitedString", s."NonNullableText", s."NullableEnumConvertedToString", s."NullableEnumConvertedToStringWithNonNullableLambda", s."NullableIntArray", s."NullableIntList", s."NullableStringArray", s."NullableStringList", s."NullableText", s."StringArray", s."StringList", s."ValueConvertedArrayOfEnum", s."ValueConvertedListOfEnum", s."Varchar10", s."Varchar15"
170170
FROM "SomeEntities" AS s
171-
WHERE s."StringArray" @> ARRAY['3']::text[]
171+
WHERE '3' = ANY (s."StringArray")
172172
""");
173173
}
174174

@@ -180,7 +180,7 @@ public override async Task Array_column_Any_Equals(bool async)
180180
"""
181181
SELECT s."Id", s."ArrayContainerEntityId", s."ArrayOfStringConvertedToDelimitedString", s."Byte", s."ByteArray", s."Bytea", s."EnumConvertedToInt", s."EnumConvertedToString", s."IList", s."IntArray", s."IntList", s."ListOfStringConvertedToDelimitedString", s."NonNullableText", s."NullableEnumConvertedToString", s."NullableEnumConvertedToStringWithNonNullableLambda", s."NullableIntArray", s."NullableIntList", s."NullableStringArray", s."NullableStringList", s."NullableText", s."StringArray", s."StringList", s."ValueConvertedArrayOfEnum", s."ValueConvertedListOfEnum", s."Varchar10", s."Varchar15"
182182
FROM "SomeEntities" AS s
183-
WHERE s."StringArray" @> ARRAY['3']::text[]
183+
WHERE '3' = ANY (s."StringArray")
184184
""");
185185
}
186186

@@ -192,7 +192,7 @@ public override async Task Array_column_Contains_literal_item(bool async)
192192
"""
193193
SELECT s."Id", s."ArrayContainerEntityId", s."ArrayOfStringConvertedToDelimitedString", s."Byte", s."ByteArray", s."Bytea", s."EnumConvertedToInt", s."EnumConvertedToString", s."IList", s."IntArray", s."IntList", s."ListOfStringConvertedToDelimitedString", s."NonNullableText", s."NullableEnumConvertedToString", s."NullableEnumConvertedToStringWithNonNullableLambda", s."NullableIntArray", s."NullableIntList", s."NullableStringArray", s."NullableStringList", s."NullableText", s."StringArray", s."StringList", s."ValueConvertedArrayOfEnum", s."ValueConvertedListOfEnum", s."Varchar10", s."Varchar15"
194194
FROM "SomeEntities" AS s
195-
WHERE s."IntArray" @> ARRAY[3]::integer[]
195+
WHERE 3 = ANY (s."IntArray")
196196
""");
197197
}
198198

@@ -206,7 +206,7 @@ public override async Task Array_column_Contains_parameter_item(bool async)
206206
207207
SELECT s."Id", s."ArrayContainerEntityId", s."ArrayOfStringConvertedToDelimitedString", s."Byte", s."ByteArray", s."Bytea", s."EnumConvertedToInt", s."EnumConvertedToString", s."IList", s."IntArray", s."IntList", s."ListOfStringConvertedToDelimitedString", s."NonNullableText", s."NullableEnumConvertedToString", s."NullableEnumConvertedToStringWithNonNullableLambda", s."NullableIntArray", s."NullableIntList", s."NullableStringArray", s."NullableStringList", s."NullableText", s."StringArray", s."StringList", s."ValueConvertedArrayOfEnum", s."ValueConvertedListOfEnum", s."Varchar10", s."Varchar15"
208208
FROM "SomeEntities" AS s
209-
WHERE s."IntArray" @> ARRAY[@p]::integer[]
209+
WHERE @p = ANY (s."IntArray")
210210
""");
211211
}
212212

@@ -218,7 +218,7 @@ public override async Task Array_column_Contains_column_item(bool async)
218218
"""
219219
SELECT s."Id", s."ArrayContainerEntityId", s."ArrayOfStringConvertedToDelimitedString", s."Byte", s."ByteArray", s."Bytea", s."EnumConvertedToInt", s."EnumConvertedToString", s."IList", s."IntArray", s."IntList", s."ListOfStringConvertedToDelimitedString", s."NonNullableText", s."NullableEnumConvertedToString", s."NullableEnumConvertedToStringWithNonNullableLambda", s."NullableIntArray", s."NullableIntList", s."NullableStringArray", s."NullableStringList", s."NullableText", s."StringArray", s."StringList", s."ValueConvertedArrayOfEnum", s."ValueConvertedListOfEnum", s."Varchar10", s."Varchar15"
220220
FROM "SomeEntities" AS s
221-
WHERE s."IntArray" @> ARRAY[s."Id" + 2]::integer[]
221+
WHERE s."Id" + 2 = ANY (s."IntArray")
222222
""");
223223
}
224224

@@ -250,7 +250,7 @@ public override void Array_column_Contains_null_parameter_does_not_work()
250250
"""
251251
SELECT count(*)::int
252252
FROM "SomeEntities" AS s
253-
WHERE s."StringArray" @> ARRAY[NULL]::text[]
253+
WHERE NULL = ANY (s."StringArray") OR (NULL IS NULL AND array_position(s."StringArray", NULL) IS NOT NULL)
254254
""");
255255
}
256256

@@ -262,7 +262,7 @@ public override async Task Nullable_array_column_Contains_literal_item(bool asyn
262262
"""
263263
SELECT s."Id", s."ArrayContainerEntityId", s."ArrayOfStringConvertedToDelimitedString", s."Byte", s."ByteArray", s."Bytea", s."EnumConvertedToInt", s."EnumConvertedToString", s."IList", s."IntArray", s."IntList", s."ListOfStringConvertedToDelimitedString", s."NonNullableText", s."NullableEnumConvertedToString", s."NullableEnumConvertedToStringWithNonNullableLambda", s."NullableIntArray", s."NullableIntList", s."NullableStringArray", s."NullableStringList", s."NullableText", s."StringArray", s."StringList", s."ValueConvertedArrayOfEnum", s."ValueConvertedListOfEnum", s."Varchar10", s."Varchar15"
264264
FROM "SomeEntities" AS s
265-
WHERE s."NullableIntArray" @> ARRAY[3]::integer[]
265+
WHERE 3 = ANY (s."NullableIntArray")
266266
""");
267267
}
268268

@@ -525,7 +525,7 @@ await AssertQuery(
525525
526526
SELECT s."Id", s."ArrayContainerEntityId", s."ArrayOfStringConvertedToDelimitedString", s."Byte", s."ByteArray", s."Bytea", s."EnumConvertedToInt", s."EnumConvertedToString", s."IList", s."IntArray", s."IntList", s."ListOfStringConvertedToDelimitedString", s."NonNullableText", s."NullableEnumConvertedToString", s."NullableEnumConvertedToStringWithNonNullableLambda", s."NullableIntArray", s."NullableIntList", s."NullableStringArray", s."NullableStringList", s."NullableText", s."StringArray", s."StringList", s."ValueConvertedArrayOfEnum", s."ValueConvertedListOfEnum", s."Varchar10", s."Varchar15"
527527
FROM "SomeEntities" AS s
528-
WHERE s."ValueConvertedArrayOfEnum" @> ARRAY[@item]::text[]
528+
WHERE @item = ANY (s."ValueConvertedArrayOfEnum")
529529
""");
530530
}
531531

@@ -539,7 +539,7 @@ await AssertQuery(
539539
"""
540540
SELECT s."Id", s."ArrayContainerEntityId", s."ArrayOfStringConvertedToDelimitedString", s."Byte", s."ByteArray", s."Bytea", s."EnumConvertedToInt", s."EnumConvertedToString", s."IList", s."IntArray", s."IntList", s."ListOfStringConvertedToDelimitedString", s."NonNullableText", s."NullableEnumConvertedToString", s."NullableEnumConvertedToStringWithNonNullableLambda", s."NullableIntArray", s."NullableIntList", s."NullableStringArray", s."NullableStringList", s."NullableText", s."StringArray", s."StringList", s."ValueConvertedArrayOfEnum", s."ValueConvertedListOfEnum", s."Varchar10", s."Varchar15"
541541
FROM "SomeEntities" AS s
542-
WHERE s."ValueConvertedArrayOfEnum" @> ARRAY['Eight']::text[]
542+
WHERE 'Eight' = ANY (s."ValueConvertedArrayOfEnum")
543543
""");
544544
}
545545

@@ -587,7 +587,7 @@ public override async Task IList_column_contains_constant(bool async)
587587
"""
588588
SELECT s."Id", s."ArrayContainerEntityId", s."ArrayOfStringConvertedToDelimitedString", s."Byte", s."ByteArray", s."Bytea", s."EnumConvertedToInt", s."EnumConvertedToString", s."IList", s."IntArray", s."IntList", s."ListOfStringConvertedToDelimitedString", s."NonNullableText", s."NullableEnumConvertedToString", s."NullableEnumConvertedToStringWithNonNullableLambda", s."NullableIntArray", s."NullableIntList", s."NullableStringArray", s."NullableStringList", s."NullableText", s."StringArray", s."StringList", s."ValueConvertedArrayOfEnum", s."ValueConvertedListOfEnum", s."Varchar10", s."Varchar15"
589589
FROM "SomeEntities" AS s
590-
WHERE s."IList" @> ARRAY[10]::integer[]
590+
WHERE 10 = ANY (s."IList")
591591
""");
592592
}
593593

0 commit comments

Comments
 (0)