From 7cd18ecb26de0c1e2d7285fd223ab6f81abf0671 Mon Sep 17 00:00:00 2001 From: Shay Rojansky Date: Thu, 7 Oct 2021 00:11:01 +0200 Subject: [PATCH] Array translation improvements And redo array/list tests Closes #2026 --- EFCore.PG.sln.DotSettings | 1 + .../Internal/NpgsqlArrayTranslator.cs | 144 +- .../NpgsqlSqlTranslatingExpressionVisitor.cs | 11 +- .../Query/NpgsqlSqlExpressionFactory.cs | 26 +- .../Query/ArrayArrayQueryTest.cs | 685 +++++++ .../Query/ArrayListQueryTest.cs | 777 ++++++++ .../Query/ArrayQueryFixture.cs | 88 + .../Query/ArrayQueryTest.cs | 1628 ++++------------- .../TestModels/Array/ArrayContainerEntity.cs | 12 + .../TestModels/Array/ArrayEntity.cs | 27 + .../TestModels/Array/ArrayQueryContext.cs | 44 + .../TestModels/Array/ArrayQueryData.cs | 87 + .../TestModels/Array/SomeEnum.cs | 12 + 13 files changed, 2199 insertions(+), 1343 deletions(-) create mode 100644 test/EFCore.PG.FunctionalTests/Query/ArrayArrayQueryTest.cs create mode 100644 test/EFCore.PG.FunctionalTests/Query/ArrayListQueryTest.cs create mode 100644 test/EFCore.PG.FunctionalTests/Query/ArrayQueryFixture.cs create mode 100644 test/EFCore.PG.FunctionalTests/TestModels/Array/ArrayContainerEntity.cs create mode 100644 test/EFCore.PG.FunctionalTests/TestModels/Array/ArrayEntity.cs create mode 100644 test/EFCore.PG.FunctionalTests/TestModels/Array/ArrayQueryContext.cs create mode 100644 test/EFCore.PG.FunctionalTests/TestModels/Array/ArrayQueryData.cs create mode 100644 test/EFCore.PG.FunctionalTests/TestModels/Array/SomeEnum.cs diff --git a/EFCore.PG.sln.DotSettings b/EFCore.PG.sln.DotSettings index 11acce2f9..f5b5ef67a 100644 --- a/EFCore.PG.sln.DotSettings +++ b/EFCore.PG.sln.DotSettings @@ -214,6 +214,7 @@ True True True + True True True True diff --git a/src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlArrayTranslator.cs b/src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlArrayTranslator.cs index 062371240..b1b85fac3 100644 --- a/src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlArrayTranslator.cs +++ b/src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlArrayTranslator.cs @@ -22,17 +22,57 @@ namespace Npgsql.EntityFrameworkCore.PostgreSQL.Query.ExpressionTranslators.Inte /// public class NpgsqlArrayTranslator : IMethodCallTranslator, IMemberTranslator { - private static readonly MethodInfo SequenceEqual = + #region Methods + + private static readonly MethodInfo Array_IndexOf1 = + typeof(Array).GetTypeInfo().GetMethods(BindingFlags.Public | BindingFlags.Static | BindingFlags.DeclaredOnly) + .Single(m => m.Name == nameof(Array.IndexOf) && m.IsGenericMethod && m.GetParameters().Length == 2); + + private static readonly MethodInfo Array_IndexOf2 = + typeof(Array).GetTypeInfo().GetMethods(BindingFlags.Public | BindingFlags.Static | BindingFlags.DeclaredOnly) + .Single(m => m.Name == nameof(Array.IndexOf) && m.IsGenericMethod && m.GetParameters().Length == 3); + + private static readonly MethodInfo Enumerable_Append = typeof(Enumerable).GetTypeInfo().GetMethods(BindingFlags.Public | BindingFlags.Static | BindingFlags.DeclaredOnly) - .Single(m => m.Name == nameof(Enumerable.SequenceEqual) && m.GetParameters().Length == 2); + .Single(m => m.Name == nameof(Enumerable.Append) && m.GetParameters().Length == 2); + + private static readonly MethodInfo Enumerable_AnyWithoutPredicate = + typeof(Enumerable).GetTypeInfo().GetMethods(BindingFlags.Public | BindingFlags.Static | BindingFlags.DeclaredOnly) + .Single(mi => mi.Name == nameof(Enumerable.Any) && mi.GetParameters().Length == 1); - private static readonly MethodInfo EnumerableContains = + private static readonly MethodInfo Enumerable_Concat = + typeof(Enumerable).GetTypeInfo().GetMethods(BindingFlags.Public | BindingFlags.Static | BindingFlags.DeclaredOnly) + .Single(m => m.Name == nameof(Enumerable.Concat) && m.GetParameters().Length == 2); + + private static readonly MethodInfo Enumerable_Contains = typeof(Enumerable).GetTypeInfo().GetMethods(BindingFlags.Public | BindingFlags.Static | BindingFlags.DeclaredOnly) .Single(m => m.Name == nameof(Enumerable.Contains) && m.GetParameters().Length == 2); - private static readonly MethodInfo EnumerableAnyWithoutPredicate = + private static readonly MethodInfo Enumerable_SequenceEqual = typeof(Enumerable).GetTypeInfo().GetMethods(BindingFlags.Public | BindingFlags.Static | BindingFlags.DeclaredOnly) - .Single(mi => mi.Name == nameof(Enumerable.Any) && mi.GetParameters().Length == 1); + .Single(m => m.Name == nameof(Enumerable.SequenceEqual) && m.GetParameters().Length == 2); + + private static readonly MethodInfo String_Join1 = + typeof(string).GetMethod(nameof(string.Join), new[] { typeof(string), typeof(object[]) })!; + + private static readonly MethodInfo String_Join2 = + typeof(string).GetMethod(nameof(string.Join), new[] { typeof(string), typeof(string[]) })!; + + private static readonly MethodInfo String_Join3 = + typeof(string).GetMethod(nameof(string.Join), new[] { typeof(char), typeof(object[]) })!; + + private static readonly MethodInfo String_Join4 = + typeof(string).GetMethod(nameof(string.Join), new[] { typeof(char), typeof(string[]) })!; + + private static readonly MethodInfo String_Join_generic1 = + typeof(string).GetTypeInfo().GetMethods(BindingFlags.Public | BindingFlags.Static | BindingFlags.DeclaredOnly) + .Single(m => m.Name == nameof(string.Join) && m.IsGenericMethod && m.GetParameters().Length == 2 && m.GetParameters()[0].ParameterType == typeof(string)); + + private static readonly MethodInfo String_Join_generic2 = + typeof(string).GetTypeInfo().GetMethods(BindingFlags.Public | BindingFlags.Static | BindingFlags.DeclaredOnly) + .Single(m => m.Name == nameof(string.Join) && m.IsGenericMethod && m.GetParameters().Length == 2 && m.GetParameters()[0].ParameterType == typeof(char)); + + #endregion Methods private readonly NpgsqlSqlExpressionFactory _sqlExpressionFactory; private readonly NpgsqlJsonPocoTranslator _jsonPocoTranslator; @@ -72,7 +112,7 @@ public NpgsqlArrayTranslator( if (instance is null && arguments.Count > 0 && arguments[0].Type.IsArrayOrGenericList() && !IsMappedToNonArray(arguments[0])) { // Extension method over an array or list - if (method.IsClosedFormOf(SequenceEqual) && arguments[1].Type.IsArray) + if (method.IsClosedFormOf(Enumerable_SequenceEqual) && arguments[1].Type.IsArray) { return _sqlExpressionFactory.Equal(arguments[0], arguments[1]); } @@ -80,6 +120,23 @@ public NpgsqlArrayTranslator( return TranslateCommon(arguments[0], arguments.Slice(1)); } + if (method.DeclaringType == typeof(string) + && (method == String_Join1 + || method == String_Join2 + || method == String_Join3 + || method == String_Join4 + || method.IsClosedFormOf(String_Join_generic1) + || method.IsClosedFormOf(String_Join_generic2)) + && !IsMappedToNonArray(arguments[0])) + { + return _sqlExpressionFactory.Function( + "array_to_string", + new[] { arguments[1], arguments[0], _sqlExpressionFactory.Constant("") }, + nullable: true, + argumentsPropagateNullability: TrueArrays[3], + typeof(string)); + } + // Not an array/list return null; @@ -92,7 +149,7 @@ static bool IsMappedToNonArray(SqlExpression arrayOrList) SqlExpression? TranslateCommon(SqlExpression arrayOrList, IReadOnlyList arguments) { // Predicate-less Any - translate to a simple length check. - if (method.IsClosedFormOf(EnumerableAnyWithoutPredicate)) + if (method.IsClosedFormOf(Enumerable_AnyWithoutPredicate)) { return _sqlExpressionFactory.GreaterThan( _jsonPocoTranslator.TranslateArrayLength(arrayOrList) @@ -109,7 +166,7 @@ static bool IsMappedToNonArray(SqlExpression arrayOrList) // is pattern-matched in AllAnyToContainsRewritingExpressionVisitor, which transforms it to // new[] { "a", "b", "c" }.Contains(e.Some Text). - if ((method.IsClosedFormOf(EnumerableContains) + if ((method.IsClosedFormOf(Enumerable_Contains) || method.Name == nameof(List.Contains) && method.DeclaringType.IsGenericList() @@ -176,6 +233,77 @@ arrayOrList.TypeMapping is NpgsqlArrayTypeMapping or null // Note: we also translate .Where(e => new[] { "a", "b", "c" }.Any(p => EF.Functions.Like(e.SomeText, p))) // to LIKE ANY (...). See NpgsqlSqlTranslatingExpressionVisitor.VisitArrayMethodCall. + if (method.IsClosedFormOf(Enumerable_Append)) + { + var (item, array) = _sqlExpressionFactory.ApplyTypeMappingsOnItemAndArray(arguments[0], arrayOrList); + + return _sqlExpressionFactory.Function( + "array_append", + new[] { array, item }, + nullable: true, + TrueArrays[2], + arrayOrList.Type, + arrayOrList.TypeMapping); + } + + if (method.IsClosedFormOf(Enumerable_Concat)) + { + var inferredMapping = ExpressionExtensions.InferTypeMapping(arrayOrList, arguments[0]); + + return _sqlExpressionFactory.Function( + "array_cat", + new[] + { + _sqlExpressionFactory.ApplyTypeMapping(arrayOrList, inferredMapping), + _sqlExpressionFactory.ApplyTypeMapping(arguments[0], inferredMapping) + }, + nullable: true, + TrueArrays[2], + arrayOrList.Type, + inferredMapping); + } + + if (method.IsClosedFormOf(Array_IndexOf1) + || + method.Name == nameof(List.IndexOf) + && method.DeclaringType.IsGenericList() + && method.GetParameters().Length == 1) + { + var (item, array) = _sqlExpressionFactory.ApplyTypeMappingsOnItemAndArray(arguments[0], arrayOrList); + + return _sqlExpressionFactory.Coalesce( + _sqlExpressionFactory.Subtract( + _sqlExpressionFactory.Function( + "array_position", + new[] { array, item }, + nullable: true, + TrueArrays[2], + arrayOrList.Type), + _sqlExpressionFactory.Constant(1)), + _sqlExpressionFactory.Constant(-1)); + } + + if (method.IsClosedFormOf(Array_IndexOf2) + || + method.Name == nameof(List.IndexOf) + && method.DeclaringType.IsGenericList() + && method.GetParameters().Length == 2) + { + var (item, array) = _sqlExpressionFactory.ApplyTypeMappingsOnItemAndArray(arguments[0], arrayOrList); + var startIndex = _sqlExpressionFactory.GenerateOneBasedIndexExpression(arguments[1]); + + return _sqlExpressionFactory.Coalesce( + _sqlExpressionFactory.Subtract( + _sqlExpressionFactory.Function( + "array_position", + new[] { array, item, startIndex }, + nullable: true, + TrueArrays[3], + arrayOrList.Type), + _sqlExpressionFactory.Constant(1)), + _sqlExpressionFactory.Constant(-1)); + } + return null; } } diff --git a/src/EFCore.PG/Query/Internal/NpgsqlSqlTranslatingExpressionVisitor.cs b/src/EFCore.PG/Query/Internal/NpgsqlSqlTranslatingExpressionVisitor.cs index 8fe3c5547..2b53f912c 100644 --- a/src/EFCore.PG/Query/Internal/NpgsqlSqlTranslatingExpressionVisitor.cs +++ b/src/EFCore.PG/Query/Internal/NpgsqlSqlTranslatingExpressionVisitor.cs @@ -415,7 +415,7 @@ protected override Expression VisitBinary(BinaryExpression binaryExpression) // Try translating ArrayIndex inside json column _jsonPocoTranslator.TranslateMemberAccess(sqlLeft!, sqlRight!, binaryExpression.Type) ?? // Other types should be subscriptable - but PostgreSQL arrays are 1-based, so adjust the index. - _sqlExpressionFactory.ArrayIndex(sqlLeft!, GenerateOneBasedIndexExpression(sqlRight!)); + _sqlExpressionFactory.ArrayIndex(sqlLeft!, _sqlExpressionFactory.GenerateOneBasedIndexExpression(sqlRight!)); } return base.VisitBinary(binaryExpression); @@ -509,15 +509,6 @@ bool TryTranslateArguments(out SqlExpression[] sqlArguments) } } - /// - /// PostgreSQL array indexing is 1-based. If the index happens to be a constant, - /// just increment it. Otherwise, append a +1 in the SQL. - /// - private SqlExpression GenerateOneBasedIndexExpression(SqlExpression expression) - => expression is SqlConstantExpression constant - ? _sqlExpressionFactory.Constant(Convert.ToInt32(constant.Value) + 1, constant.TypeMapping) - : _sqlExpressionFactory.Add(expression, _sqlExpressionFactory.Constant(1)); - #region Copied from RelationalSqlTranslatingExpressionVisitor private static Expression TryRemoveImplicitConvert(Expression expression) diff --git a/src/EFCore.PG/Query/NpgsqlSqlExpressionFactory.cs b/src/EFCore.PG/Query/NpgsqlSqlExpressionFactory.cs index 63cac5f67..d010a7376 100644 --- a/src/EFCore.PG/Query/NpgsqlSqlExpressionFactory.cs +++ b/src/EFCore.PG/Query/NpgsqlSqlExpressionFactory.cs @@ -410,7 +410,9 @@ private SqlExpression ApplyTypeMappingOnAll(PostgresAllExpression postgresAllExp return new PostgresAllExpression(item, array, postgresAllExpression.OperatorType, _boolTypeMapping); } - private (SqlExpression, SqlExpression) ApplyTypeMappingsOnItemAndArray(SqlExpression itemExpression, SqlExpression arrayExpression) + public virtual (SqlExpression, SqlExpression) ApplyTypeMappingsOnItemAndArray( + SqlExpression itemExpression, + SqlExpression arrayExpression) { // Attempt type inference either from the operand to the array or the other way around var arrayMapping = (NpgsqlArrayTypeMapping?)arrayExpression.TypeMapping; @@ -464,9 +466,15 @@ private SqlExpression ApplyTypeMappingOnAll(PostgresAllExpression postgresAllExp private SqlExpression ApplyTypeMappingOnArrayIndex( PostgresArrayIndexExpression postgresArrayIndexExpression, RelationalTypeMapping? typeMapping) - => new PostgresArrayIndexExpression( - // TODO: Infer the array's mapping from the element - ApplyDefaultTypeMapping(postgresArrayIndexExpression.Array), + { + // If a (non-null) type mapping is being applied, it's to the element being indexed. + // Infer the array's mapping from that. + var (_, array) = typeMapping is not null + ? ApplyTypeMappingsOnItemAndArray(Constant(null, typeMapping), postgresArrayIndexExpression.Array) + : (null, ApplyDefaultTypeMapping(postgresArrayIndexExpression.Array)); + + return new PostgresArrayIndexExpression( + array, ApplyDefaultTypeMapping(postgresArrayIndexExpression.Index), postgresArrayIndexExpression.Type, // If the array has a type mapping (i.e. column), prefer that just like we prefer column mappings in general @@ -474,6 +482,7 @@ postgresArrayIndexExpression.Array.TypeMapping is NpgsqlArrayTypeMapping arrayMa ? arrayMapping.ElementMapping : typeMapping ?? (RelationalTypeMapping?)_typeMappingSource.FindMapping(postgresArrayIndexExpression.Type, Dependencies.Model)); + } private SqlExpression ApplyTypeMappingOnILike(PostgresILikeExpression ilikeExpression) { @@ -749,5 +758,14 @@ private SqlExpression ApplyTypeMappingOnPostgresNewArray( newExpressions ?? postgresNewArrayExpression.Expressions, postgresNewArrayExpression.Type, arrayTypeMapping); } + + /// + /// PostgreSQL array indexing is 1-based. If the index happens to be a constant, + /// just increment it. Otherwise, append a +1 in the SQL. + /// + public virtual SqlExpression GenerateOneBasedIndexExpression(SqlExpression expression) + => expression is SqlConstantExpression constant + ? Constant(System.Convert.ToInt32(constant.Value) + 1, constant.TypeMapping) + : Add(expression, Constant(1)); } } diff --git a/test/EFCore.PG.FunctionalTests/Query/ArrayArrayQueryTest.cs b/test/EFCore.PG.FunctionalTests/Query/ArrayArrayQueryTest.cs new file mode 100644 index 000000000..596437c5a --- /dev/null +++ b/test/EFCore.PG.FunctionalTests/Query/ArrayArrayQueryTest.cs @@ -0,0 +1,685 @@ +using System; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.EntityFrameworkCore; +using Npgsql.EntityFrameworkCore.PostgreSQL.TestModels.Array; +using Xunit; +using Xunit.Abstractions; + +namespace Npgsql.EntityFrameworkCore.PostgreSQL.Query +{ + public class ArrayArrayQueryTest : ArrayQueryTest + { + public ArrayArrayQueryTest(ArrayArrayQueryFixture fixture, ITestOutputHelper testOutputHelper) + : base(fixture, testOutputHelper) + { + } + + #region Indexers + + public override async Task Index_with_constant(bool async) + { + await base.Index_with_constant(async); + + AssertSql( + @"SELECT s.""Id"", s.""ArrayContainerEntityId"", s.""Byte"", s.""ByteArray"", s.""Bytea"", s.""IntArray"", s.""IntList"", s.""NonNullableText"", s.""NullableIntArray"", s.""NullableIntList"", s.""NullableStringArray"", s.""NullableStringList"", s.""NullableText"", s.""StringArray"", s.""StringList"", s.""ValueConvertedArray"", s.""ValueConvertedList"", s.""ValueConvertedScalar"" +FROM ""SomeEntities"" AS s +WHERE s.""IntArray""[1] = 3"); + } + + public override async Task Index_with_parameter(bool async) + { + await base.Index_with_parameter(async); + + AssertSql( + @"@__x_0='0' + +SELECT s.""Id"", s.""ArrayContainerEntityId"", s.""Byte"", s.""ByteArray"", s.""Bytea"", s.""IntArray"", s.""IntList"", s.""NonNullableText"", s.""NullableIntArray"", s.""NullableIntList"", s.""NullableStringArray"", s.""NullableStringList"", s.""NullableText"", s.""StringArray"", s.""StringList"", s.""ValueConvertedArray"", s.""ValueConvertedList"", s.""ValueConvertedScalar"" +FROM ""SomeEntities"" AS s +WHERE s.""IntArray""[@__x_0 + 1] = 3"); + } + + public override async Task Nullable_index_with_constant(bool async) + { + await base.Nullable_index_with_constant(async); + + AssertSql( + @"SELECT s.""Id"", s.""ArrayContainerEntityId"", s.""Byte"", s.""ByteArray"", s.""Bytea"", s.""IntArray"", s.""IntList"", s.""NonNullableText"", s.""NullableIntArray"", s.""NullableIntList"", s.""NullableStringArray"", s.""NullableStringList"", s.""NullableText"", s.""StringArray"", s.""StringList"", s.""ValueConvertedArray"", s.""ValueConvertedList"", s.""ValueConvertedScalar"" +FROM ""SomeEntities"" AS s +WHERE s.""NullableIntArray""[1] = 3"); + } + + public override async Task Nullable_value_array_index_compare_to_null(bool async) + { + await base.Nullable_value_array_index_compare_to_null(async); + + AssertSql( + @"SELECT s.""Id"", s.""ArrayContainerEntityId"", s.""Byte"", s.""ByteArray"", s.""Bytea"", s.""IntArray"", s.""IntList"", s.""NonNullableText"", s.""NullableIntArray"", s.""NullableIntList"", s.""NullableStringArray"", s.""NullableStringList"", s.""NullableText"", s.""StringArray"", s.""StringList"", s.""ValueConvertedArray"", s.""ValueConvertedList"", s.""ValueConvertedScalar"" +FROM ""SomeEntities"" AS s +WHERE (s.""NullableIntArray""[3] IS NULL)"); + } + + public override async Task Non_nullable_value_array_index_compare_to_null(bool async) + { + await base.Non_nullable_value_array_index_compare_to_null(async); + + AssertSql( + @"SELECT s.""Id"", s.""ArrayContainerEntityId"", s.""Byte"", s.""ByteArray"", s.""Bytea"", s.""IntArray"", s.""IntList"", s.""NonNullableText"", s.""NullableIntArray"", s.""NullableIntList"", s.""NullableStringArray"", s.""NullableStringList"", s.""NullableText"", s.""StringArray"", s.""StringList"", s.""ValueConvertedArray"", s.""ValueConvertedList"", s.""ValueConvertedScalar"" +FROM ""SomeEntities"" AS s +WHERE FALSE"); + } + + public override async Task Nullable_reference_array_index_compare_to_null(bool async) + { + await base.Nullable_reference_array_index_compare_to_null(async); + + AssertSql( + @"SELECT s.""Id"", s.""ArrayContainerEntityId"", s.""Byte"", s.""ByteArray"", s.""Bytea"", s.""IntArray"", s.""IntList"", s.""NonNullableText"", s.""NullableIntArray"", s.""NullableIntList"", s.""NullableStringArray"", s.""NullableStringList"", s.""NullableText"", s.""StringArray"", s.""StringList"", s.""ValueConvertedArray"", s.""ValueConvertedList"", s.""ValueConvertedScalar"" +FROM ""SomeEntities"" AS s +WHERE (s.""NullableStringArray""[3] IS NULL)"); + } + + public override async Task Non_nullable_reference_array_index_compare_to_null(bool async) + { + await base.Non_nullable_reference_array_index_compare_to_null(async); + + AssertSql( + @"SELECT s.""Id"", s.""ArrayContainerEntityId"", s.""Byte"", s.""ByteArray"", s.""Bytea"", s.""IntArray"", s.""IntList"", s.""NonNullableText"", s.""NullableIntArray"", s.""NullableIntList"", s.""NullableStringArray"", s.""NullableStringList"", s.""NullableText"", s.""StringArray"", s.""StringList"", s.""ValueConvertedArray"", s.""ValueConvertedList"", s.""ValueConvertedScalar"" +FROM ""SomeEntities"" AS s +WHERE FALSE"); + } + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public async Task Index_bytea_with_constant(bool async) + { + await AssertQuery( + async, + ss => ss.Set().Where(e => e.Bytea[0] == 3), + entryCount: 1); + + AssertSql( + @"SELECT s.""Id"", s.""ArrayContainerEntityId"", s.""Byte"", s.""ByteArray"", s.""Bytea"", s.""IntArray"", s.""IntList"", s.""NonNullableText"", s.""NullableIntArray"", s.""NullableIntList"", s.""NullableStringArray"", s.""NullableStringList"", s.""NullableText"", s.""StringArray"", s.""StringList"", s.""ValueConvertedArray"", s.""ValueConvertedList"", s.""ValueConvertedScalar"" +FROM ""SomeEntities"" AS s +WHERE get_byte(s.""Bytea"", 0) = 3"); + } + + #endregion + + #region SequenceEqual + + public override async Task SequenceEqual_with_parameter(bool async) + { + await base.SequenceEqual_with_parameter(async); + + AssertSql( + @"@__arr_0={ '3', '4' } (DbType = Object) + +SELECT s.""Id"", s.""ArrayContainerEntityId"", s.""Byte"", s.""ByteArray"", s.""Bytea"", s.""IntArray"", s.""IntList"", s.""NonNullableText"", s.""NullableIntArray"", s.""NullableIntList"", s.""NullableStringArray"", s.""NullableStringList"", s.""NullableText"", s.""StringArray"", s.""StringList"", s.""ValueConvertedArray"", s.""ValueConvertedList"", s.""ValueConvertedScalar"" +FROM ""SomeEntities"" AS s +WHERE s.""IntArray"" = @__arr_0"); + } + + public override async Task SequenceEqual_with_array_literal(bool async) + { + await base.SequenceEqual_with_array_literal(async); + + AssertSql( + @"SELECT s.""Id"", s.""ArrayContainerEntityId"", s.""Byte"", s.""ByteArray"", s.""Bytea"", s.""IntArray"", s.""IntList"", s.""NonNullableText"", s.""NullableIntArray"", s.""NullableIntList"", s.""NullableStringArray"", s.""NullableStringList"", s.""NullableText"", s.""StringArray"", s.""StringList"", s.""ValueConvertedArray"", s.""ValueConvertedList"", s.""ValueConvertedScalar"" +FROM ""SomeEntities"" AS s +WHERE s.""IntArray"" = ARRAY[3,4]::integer[]"); + } + + public override async Task SequenceEqual_over_nullable_with_parameter(bool async) + { + await base.SequenceEqual_over_nullable_with_parameter(async); + + AssertSql( + @"@__arr_0={ '3', '4', NULL } (DbType = Object) + +SELECT s.""Id"", s.""ArrayContainerEntityId"", s.""Byte"", s.""ByteArray"", s.""Bytea"", s.""IntArray"", s.""IntList"", s.""NonNullableText"", s.""NullableIntArray"", s.""NullableIntList"", s.""NullableStringArray"", s.""NullableStringList"", s.""NullableText"", s.""StringArray"", s.""StringList"", s.""ValueConvertedArray"", s.""ValueConvertedList"", s.""ValueConvertedScalar"" +FROM ""SomeEntities"" AS s +WHERE s.""NullableIntArray"" = @__arr_0"); + } + + #endregion SequenceEqual + + #region Containment + + public override async Task Array_column_Any_equality_operator(bool async) + { + await base.Array_column_Any_equality_operator(async); + + AssertSql( + @"SELECT s.""Id"", s.""ArrayContainerEntityId"", s.""Byte"", s.""ByteArray"", s.""Bytea"", s.""IntArray"", s.""IntList"", s.""NonNullableText"", s.""NullableIntArray"", s.""NullableIntList"", s.""NullableStringArray"", s.""NullableStringList"", s.""NullableText"", s.""StringArray"", s.""StringList"", s.""ValueConvertedArray"", s.""ValueConvertedList"", s.""ValueConvertedScalar"" +FROM ""SomeEntities"" AS s +WHERE s.""StringArray"" @> ARRAY['3']::text[]"); + } + + public override async Task Array_column_Any_Equals(bool async) + { + await base.Array_column_Any_Equals(async); + + AssertSql( + @"SELECT s.""Id"", s.""ArrayContainerEntityId"", s.""Byte"", s.""ByteArray"", s.""Bytea"", s.""IntArray"", s.""IntList"", s.""NonNullableText"", s.""NullableIntArray"", s.""NullableIntList"", s.""NullableStringArray"", s.""NullableStringList"", s.""NullableText"", s.""StringArray"", s.""StringList"", s.""ValueConvertedArray"", s.""ValueConvertedList"", s.""ValueConvertedScalar"" +FROM ""SomeEntities"" AS s +WHERE s.""StringArray"" @> ARRAY['3']::text[]"); + } + + public override async Task Array_column_Contains_literal_item(bool async) + { + await base.Array_column_Contains_literal_item(async); + + AssertSql( + @"SELECT s.""Id"", s.""ArrayContainerEntityId"", s.""Byte"", s.""ByteArray"", s.""Bytea"", s.""IntArray"", s.""IntList"", s.""NonNullableText"", s.""NullableIntArray"", s.""NullableIntList"", s.""NullableStringArray"", s.""NullableStringList"", s.""NullableText"", s.""StringArray"", s.""StringList"", s.""ValueConvertedArray"", s.""ValueConvertedList"", s.""ValueConvertedScalar"" +FROM ""SomeEntities"" AS s +WHERE s.""IntArray"" @> ARRAY[3]::integer[]"); + } + + public override async Task Array_column_Contains_parameter_item(bool async) + { + await base.Array_column_Contains_parameter_item(async); + + AssertSql( + @"@__p_0='3' + +SELECT s.""Id"", s.""ArrayContainerEntityId"", s.""Byte"", s.""ByteArray"", s.""Bytea"", s.""IntArray"", s.""IntList"", s.""NonNullableText"", s.""NullableIntArray"", s.""NullableIntList"", s.""NullableStringArray"", s.""NullableStringList"", s.""NullableText"", s.""StringArray"", s.""StringList"", s.""ValueConvertedArray"", s.""ValueConvertedList"", s.""ValueConvertedScalar"" +FROM ""SomeEntities"" AS s +WHERE s.""IntArray"" @> ARRAY[@__p_0]::integer[]"); + } + + public override async Task Array_column_Contains_column_item(bool async) + { + await base.Array_column_Contains_column_item(async); + + AssertSql( + @"SELECT s.""Id"", s.""ArrayContainerEntityId"", s.""Byte"", s.""ByteArray"", s.""Bytea"", s.""IntArray"", s.""IntList"", s.""NonNullableText"", s.""NullableIntArray"", s.""NullableIntList"", s.""NullableStringArray"", s.""NullableStringList"", s.""NullableText"", s.""StringArray"", s.""StringList"", s.""ValueConvertedArray"", s.""ValueConvertedList"", s.""ValueConvertedScalar"" +FROM ""SomeEntities"" AS s +WHERE s.""IntArray"" @> ARRAY[s.""Id"" + 2]::integer[]"); + } + + public override async Task Array_column_Contains_null_constant(bool async) + { + await base.Array_column_Contains_null_constant(async); + + AssertSql( + @"SELECT s.""Id"", s.""ArrayContainerEntityId"", s.""Byte"", s.""ByteArray"", s.""Bytea"", s.""IntArray"", s.""IntList"", s.""NonNullableText"", s.""NullableIntArray"", s.""NullableIntList"", s.""NullableStringArray"", s.""NullableStringList"", s.""NullableText"", s.""StringArray"", s.""StringList"", s.""ValueConvertedArray"", s.""ValueConvertedList"", s.""ValueConvertedScalar"" +FROM ""SomeEntities"" AS s +WHERE (array_position(s.""NullableStringArray"", NULL) IS NOT NULL)"); + } + + public override void Array_column_Contains_null_parameter_does_not_work() + { + using var ctx = CreateContext(); + + string p = null; + + // We incorrectly miss arrays containing non-constant nulls, because detecting those + // would prevent index use. + Assert.Equal( + 0, + ctx.SomeEntities.Count(e => e.StringArray.Contains(p))); + + AssertSql( + @"SELECT COUNT(*)::INT +FROM ""SomeEntities"" AS s +WHERE s.""StringArray"" @> ARRAY[NULL]::text[]"); + } + + public override async Task Nullable_array_column_Contains_literal_item(bool async) + { + await base.Nullable_array_column_Contains_literal_item(async); + + AssertSql( + @"SELECT s.""Id"", s.""ArrayContainerEntityId"", s.""Byte"", s.""ByteArray"", s.""Bytea"", s.""IntArray"", s.""IntList"", s.""NonNullableText"", s.""NullableIntArray"", s.""NullableIntList"", s.""NullableStringArray"", s.""NullableStringList"", s.""NullableText"", s.""StringArray"", s.""StringList"", s.""ValueConvertedArray"", s.""ValueConvertedList"", s.""ValueConvertedScalar"" +FROM ""SomeEntities"" AS s +WHERE s.""NullableIntArray"" @> ARRAY[3]::integer[]"); + } + + public override async Task Array_constant_Contains_column(bool async) + { + await AssertQuery( + async, + ss => ss.Set().Where(e => new[] { "foo", "xxx" }.Contains(e.NullableText)), + entryCount: 1); + + AssertSql( + @"SELECT s.""Id"", s.""ArrayContainerEntityId"", s.""Byte"", s.""ByteArray"", s.""Bytea"", s.""IntArray"", s.""IntList"", s.""NonNullableText"", s.""NullableIntArray"", s.""NullableIntList"", s.""NullableStringArray"", s.""NullableStringList"", s.""NullableText"", s.""StringArray"", s.""StringList"", s.""ValueConvertedArray"", s.""ValueConvertedList"", s.""ValueConvertedScalar"" +FROM ""SomeEntities"" AS s +WHERE s.""NullableText"" IN ('foo', 'xxx')"); + } + + public override async Task Array_param_Contains_nullable_column(bool async) + { + var array = new[] { "foo", "xxx" }; + + await AssertQuery( + async, + ss => ss.Set().Where(e => array.Contains(e.NullableText)), + entryCount: 1); + + AssertSql( + @"@__array_0={ 'foo', 'xxx' } (DbType = Object) + +SELECT s.""Id"", s.""ArrayContainerEntityId"", s.""Byte"", s.""ByteArray"", s.""Bytea"", s.""IntArray"", s.""IntList"", s.""NonNullableText"", s.""NullableIntArray"", s.""NullableIntList"", s.""NullableStringArray"", s.""NullableStringList"", s.""NullableText"", s.""StringArray"", s.""StringList"", s.""ValueConvertedArray"", s.""ValueConvertedList"", s.""ValueConvertedScalar"" +FROM ""SomeEntities"" AS s +WHERE s.""NullableText"" = ANY (@__array_0) OR ((s.""NullableText"" IS NULL) AND (array_position(@__array_0, NULL) IS NOT NULL))"); + } + + public override async Task Array_param_Contains_non_nullable_column(bool async) + { + var array = new[] { 1 }; + + await AssertQuery( + async, + ss => ss.Set().Where(e => array.Contains(e.Id)), + entryCount: 1); + + AssertSql( + @"@__array_0={ '1' } (DbType = Object) + +SELECT s.""Id"", s.""ArrayContainerEntityId"", s.""Byte"", s.""ByteArray"", s.""Bytea"", s.""IntArray"", s.""IntList"", s.""NonNullableText"", s.""NullableIntArray"", s.""NullableIntList"", s.""NullableStringArray"", s.""NullableStringList"", s.""NullableText"", s.""StringArray"", s.""StringList"", s.""ValueConvertedArray"", s.""ValueConvertedList"", s.""ValueConvertedScalar"" +FROM ""SomeEntities"" AS s +WHERE s.""Id"" = ANY (@__array_0)"); + } + + public override void Array_param_with_null_Contains_non_nullable_not_found() + { + using var ctx = CreateContext(); + + var array = new[] { "unknown1", "unknown2", null }; + + Assert.Equal(0, ctx.SomeEntities.Count(e => array.Contains(e.NonNullableText))); + + AssertSql( + @"@__array_0={ 'unknown1', 'unknown2', NULL } (DbType = Object) + +SELECT COUNT(*)::INT +FROM ""SomeEntities"" AS s +WHERE s.""NonNullableText"" = ANY (@__array_0)"); + } + + public override void Array_param_with_null_Contains_non_nullable_not_found_negated() + { + using var ctx = CreateContext(); + + var array = new[] { "unknown1", "unknown2", null }; + + Assert.Equal(2, ctx.SomeEntities.Count(e => !array.Contains(e.NonNullableText))); + + AssertSql( + @"@__array_0={ 'unknown1', 'unknown2', NULL } (DbType = Object) + +SELECT COUNT(*)::INT +FROM ""SomeEntities"" AS s +WHERE NOT (s.""NonNullableText"" = ANY (@__array_0) AND (s.""NonNullableText"" = ANY (@__array_0) IS NOT NULL))"); + } + + public override void Array_param_with_null_Contains_nullable_not_found() + { + using var ctx = CreateContext(); + + var array = new[] { "unknown1", "unknown2", null }; + + Assert.Equal(0, ctx.SomeEntities.Count(e => array.Contains(e.NullableText))); + + AssertSql( + @"@__array_0={ 'unknown1', 'unknown2', NULL } (DbType = Object) + +SELECT COUNT(*)::INT +FROM ""SomeEntities"" AS s +WHERE s.""NullableText"" = ANY (@__array_0) OR ((s.""NullableText"" IS NULL) AND (array_position(@__array_0, NULL) IS NOT NULL))"); + } + + public override void Array_param_with_null_Contains_nullable_not_found_negated() + { + using var ctx = CreateContext(); + + var array = new[] { "unknown1", "unknown2", null }; + + Assert.Equal(2, ctx.SomeEntities.Count(e => !array.Contains(e.NullableText))); + + AssertSql( + @"@__array_0={ 'unknown1', 'unknown2', NULL } (DbType = Object) + +SELECT COUNT(*)::INT +FROM ""SomeEntities"" AS s +WHERE NOT (s.""NullableText"" = ANY (@__array_0) AND (s.""NullableText"" = ANY (@__array_0) IS NOT NULL)) AND ((s.""NullableText"" IS NOT NULL) OR (array_position(@__array_0, NULL) IS NULL))"); + } + + public override async Task Byte_array_parameter_contains_column(bool async) + { + var values = new byte[] { 20 }; + + await AssertQuery( + async, + ss => ss.Set().Where(e => values.Contains(e.Byte)), + entryCount: 1); + + // Note: EF Core prints the parameter as a bytea, but it's actually a smallint[] (otherwise ANY would fail) + AssertSql( + @"@__values_0='0x14' (DbType = Object) + +SELECT s.""Id"", s.""ArrayContainerEntityId"", s.""Byte"", s.""ByteArray"", s.""Bytea"", s.""IntArray"", s.""IntList"", s.""NonNullableText"", s.""NullableIntArray"", s.""NullableIntList"", s.""NullableStringArray"", s.""NullableStringList"", s.""NullableText"", s.""StringArray"", s.""StringList"", s.""ValueConvertedArray"", s.""ValueConvertedList"", s.""ValueConvertedScalar"" +FROM ""SomeEntities"" AS s +WHERE s.""Byte"" = ANY (@__values_0)"); + } + + public override async Task Array_param_Contains_value_converted_column(bool async) + { + var array = new[] { SomeEnum.Two, SomeEnum.Three }; + + await AssertQuery( + async, + ss => ss.Set().Where(e => array.Contains(e.ValueConvertedScalar)), + entryCount: 1); + + AssertSql( + @"@__array_0={ '-2', '-3' } (DbType = Object) + +SELECT s.""Id"", s.""ArrayContainerEntityId"", s.""Byte"", s.""ByteArray"", s.""Bytea"", s.""IntArray"", s.""IntList"", s.""NonNullableText"", s.""NullableIntArray"", s.""NullableIntList"", s.""NullableStringArray"", s.""NullableStringList"", s.""NullableText"", s.""StringArray"", s.""StringList"", s.""ValueConvertedArray"", s.""ValueConvertedList"", s.""ValueConvertedScalar"" +FROM ""SomeEntities"" AS s +WHERE s.""ValueConvertedScalar"" = ANY (@__array_0)"); + } + + public override async Task Array_column_Contains_value_converted_param(bool async) + { + await base.Array_column_Contains_value_converted_param(async); + + AssertSql( + @"@__item_0='-8' + +SELECT s.""Id"", s.""ArrayContainerEntityId"", s.""Byte"", s.""ByteArray"", s.""Bytea"", s.""IntArray"", s.""IntList"", s.""NonNullableText"", s.""NullableIntArray"", s.""NullableIntList"", s.""NullableStringArray"", s.""NullableStringList"", s.""NullableText"", s.""StringArray"", s.""StringList"", s.""ValueConvertedArray"", s.""ValueConvertedList"", s.""ValueConvertedScalar"" +FROM ""SomeEntities"" AS s +WHERE s.""ValueConvertedArray"" @> ARRAY[@__item_0]::integer[]"); + } + + public override async Task Array_param_Contains_value_converted_array_column(bool async) + { + var p = new[] { SomeEnum.Eight, SomeEnum.Nine }; + + await AssertQuery( + async, + ss => ss.Set().Where(e => e.ValueConvertedArray.All(x => p.Contains(x))), + entryCount: 1); + + AssertSql( + @"@__p_0={ '-8', '-9' } (DbType = Object) + +SELECT s.""Id"", s.""ArrayContainerEntityId"", s.""Byte"", s.""ByteArray"", s.""Bytea"", s.""IntArray"", s.""IntList"", s.""NonNullableText"", s.""NullableIntArray"", s.""NullableIntList"", s.""NullableStringArray"", s.""NullableStringList"", s.""NullableText"", s.""StringArray"", s.""StringList"", s.""ValueConvertedArray"", s.""ValueConvertedList"", s.""ValueConvertedScalar"" +FROM ""SomeEntities"" AS s +WHERE s.""ValueConvertedArray"" <@ @__p_0"); + } + + public override async Task Array_column_Contains_in_scalar_subquery(bool async) + { + await base.Array_column_Contains_in_scalar_subquery(async); + + AssertSql( + @"SELECT s.""Id"" +FROM ""SomeEntityContainers"" AS s +WHERE 3 = ANY (( + SELECT s0.""NullableIntArray"" + FROM ""SomeEntities"" AS s0 + WHERE s.""Id"" = s0.""ArrayContainerEntityId"" + ORDER BY s0.""Id"" NULLS FIRST + LIMIT 1)::integer[])"); + } + + #endregion Containment + + #region Length/Count + + public override async Task Array_Length(bool async) + { + await AssertQuery( + async, + ss => ss.Set().Where(e => e.IntArray.Length == 2), + entryCount: 1); + + AssertSql( + @"SELECT s.""Id"", s.""ArrayContainerEntityId"", s.""Byte"", s.""ByteArray"", s.""Bytea"", s.""IntArray"", s.""IntList"", s.""NonNullableText"", s.""NullableIntArray"", s.""NullableIntList"", s.""NullableStringArray"", s.""NullableStringList"", s.""NullableText"", s.""StringArray"", s.""StringList"", s.""ValueConvertedArray"", s.""ValueConvertedList"", s.""ValueConvertedScalar"" +FROM ""SomeEntities"" AS s +WHERE cardinality(s.""IntArray"") = 2"); + } + + public override async Task Nullable_array_Length(bool async) + { + await AssertQuery( + async, + ss => ss.Set().Where(e => e.NullableIntArray.Length == 3), + entryCount: 1); + + AssertSql( + @"SELECT s.""Id"", s.""ArrayContainerEntityId"", s.""Byte"", s.""ByteArray"", s.""Bytea"", s.""IntArray"", s.""IntList"", s.""NonNullableText"", s.""NullableIntArray"", s.""NullableIntList"", s.""NullableStringArray"", s.""NullableStringList"", s.""NullableText"", s.""StringArray"", s.""StringList"", s.""ValueConvertedArray"", s.""ValueConvertedList"", s.""ValueConvertedScalar"" +FROM ""SomeEntities"" AS s +WHERE cardinality(s.""NullableIntArray"") = 3"); + } + + public override async Task Array_Length_on_EF_Property(bool async) + { + await AssertQuery( + async, + ss => ss.Set().Where(e => EF.Property(e, nameof(ArrayEntity.IntArray)).Length == 2), + entryCount: 1); + + AssertSql( + @"SELECT s.""Id"", s.""ArrayContainerEntityId"", s.""Byte"", s.""ByteArray"", s.""Bytea"", s.""IntArray"", s.""IntList"", s.""NonNullableText"", s.""NullableIntArray"", s.""NullableIntList"", s.""NullableStringArray"", s.""NullableStringList"", s.""NullableText"", s.""StringArray"", s.""StringList"", s.""ValueConvertedArray"", s.""ValueConvertedList"", s.""ValueConvertedScalar"" +FROM ""SomeEntities"" AS s +WHERE cardinality(s.""IntArray"") = 2"); + } + + #endregion Length/Count + + #region Any/All + + public override async Task Any_no_predicate(bool async) + { + await base.Any_no_predicate(async); + + AssertSql( + @"SELECT s.""Id"", s.""ArrayContainerEntityId"", s.""Byte"", s.""ByteArray"", s.""Bytea"", s.""IntArray"", s.""IntList"", s.""NonNullableText"", s.""NullableIntArray"", s.""NullableIntList"", s.""NullableStringArray"", s.""NullableStringList"", s.""NullableText"", s.""StringArray"", s.""StringList"", s.""ValueConvertedArray"", s.""ValueConvertedList"", s.""ValueConvertedScalar"" +FROM ""SomeEntities"" AS s +WHERE cardinality(s.""IntArray"") > 0"); + } + + public override async Task Any_like(bool async) + { + await AssertQuery( + async, + ss => ss.Set() + .Where(e => new[] { "a%", "b%", "c%" }.Any(p => EF.Functions.Like(e.NullableText, p))), + ss => ss.Set() + .Where(e => new[] { "a", "b", "c" }.Any(p => e.NullableText.StartsWith(p, StringComparison.Ordinal))), + entryCount: 1); + + AssertSql( + @"SELECT s.""Id"", s.""ArrayContainerEntityId"", s.""Byte"", s.""ByteArray"", s.""Bytea"", s.""IntArray"", s.""IntList"", s.""NonNullableText"", s.""NullableIntArray"", s.""NullableIntList"", s.""NullableStringArray"", s.""NullableStringList"", s.""NullableText"", s.""StringArray"", s.""StringList"", s.""ValueConvertedArray"", s.""ValueConvertedList"", s.""ValueConvertedScalar"" +FROM ""SomeEntities"" AS s +WHERE s.""NullableText"" LIKE ANY (ARRAY['a%','b%','c%']::text[])"); + } + + public override async Task Any_ilike(bool async) + { + await AssertQuery( + async, + ss => ss.Set() + .Where(e => new[] { "a%", "b%", "c%" }.Any(p => EF.Functions.ILike(e.NullableText, p))), + ss => ss.Set() + .Where(e => new[] { "a", "b", "c" }.Any(p => e.NullableText.StartsWith(p, StringComparison.OrdinalIgnoreCase))), + entryCount: 1); + + AssertSql( + @"SELECT s.""Id"", s.""ArrayContainerEntityId"", s.""Byte"", s.""ByteArray"", s.""Bytea"", s.""IntArray"", s.""IntList"", s.""NonNullableText"", s.""NullableIntArray"", s.""NullableIntList"", s.""NullableStringArray"", s.""NullableStringList"", s.""NullableText"", s.""StringArray"", s.""StringList"", s.""ValueConvertedArray"", s.""ValueConvertedList"", s.""ValueConvertedScalar"" +FROM ""SomeEntities"" AS s +WHERE s.""NullableText"" ILIKE ANY (ARRAY['a%','b%','c%']::text[])"); + } + + public override async Task Any_like_anonymous(bool async) + { + using var ctx = CreateContext(); + + var patternsActual = new[] { "a%", "b%", "c%" }; + var patternsExpected = new[] { "a", "b", "c" }; + + await AssertQuery( + async, + ss => ss.Set() + .Where(e => patternsActual.Any(p => EF.Functions.Like(e.NullableText, p))), + ss => ss.Set() + .Where(e => patternsExpected.Any(p => e.NullableText.StartsWith(p, StringComparison.Ordinal))), + entryCount: 1); + + AssertSql( + @"@__patternsActual_0={ 'a%', 'b%', 'c%' } (DbType = Object) + +SELECT s.""Id"", s.""ArrayContainerEntityId"", s.""Byte"", s.""ByteArray"", s.""Bytea"", s.""IntArray"", s.""IntList"", s.""NonNullableText"", s.""NullableIntArray"", s.""NullableIntList"", s.""NullableStringArray"", s.""NullableStringList"", s.""NullableText"", s.""StringArray"", s.""StringList"", s.""ValueConvertedArray"", s.""ValueConvertedList"", s.""ValueConvertedScalar"" +FROM ""SomeEntities"" AS s +WHERE s.""NullableText"" LIKE ANY (@__patternsActual_0)"); + } + + public override async Task All_like(bool async) + { + await AssertQuery( + async, + ss => ss.Set() + .Where(e => new[] { "b%", "ba%" }.All(p => EF.Functions.Like(e.NullableText, p))), + ss => ss.Set() + .Where(e => new[] { "b", "ba" }.All(p => e.NullableText.StartsWith(p, StringComparison.Ordinal))), + entryCount: 1); + + AssertSql( + @"SELECT s.""Id"", s.""ArrayContainerEntityId"", s.""Byte"", s.""ByteArray"", s.""Bytea"", s.""IntArray"", s.""IntList"", s.""NonNullableText"", s.""NullableIntArray"", s.""NullableIntList"", s.""NullableStringArray"", s.""NullableStringList"", s.""NullableText"", s.""StringArray"", s.""StringList"", s.""ValueConvertedArray"", s.""ValueConvertedList"", s.""ValueConvertedScalar"" +FROM ""SomeEntities"" AS s +WHERE s.""NullableText"" LIKE ALL (ARRAY['b%','ba%']::text[])"); + } + + public override async Task All_ilike(bool async) + { + await AssertQuery( + async, + ss => ss.Set() + .Where(e => new[] { "B%", "ba%" }.All(p => EF.Functions.ILike(e.NullableText, p))), + ss => ss.Set() + .Where(e => new[] { "B", "ba" }.All(p => e.NullableText.StartsWith(p, StringComparison.OrdinalIgnoreCase))), + entryCount: 1); + + AssertSql( + @"SELECT s.""Id"", s.""ArrayContainerEntityId"", s.""Byte"", s.""ByteArray"", s.""Bytea"", s.""IntArray"", s.""IntList"", s.""NonNullableText"", s.""NullableIntArray"", s.""NullableIntList"", s.""NullableStringArray"", s.""NullableStringList"", s.""NullableText"", s.""StringArray"", s.""StringList"", s.""ValueConvertedArray"", s.""ValueConvertedList"", s.""ValueConvertedScalar"" +FROM ""SomeEntities"" AS s +WHERE s.""NullableText"" ILIKE ALL (ARRAY['B%','ba%']::text[])"); + } + + public override async Task Any_Contains_on_constant_array(bool async) + { + await base.Any_Contains_on_constant_array(async); + + AssertSql( + @"SELECT s.""Id"", s.""ArrayContainerEntityId"", s.""Byte"", s.""ByteArray"", s.""Bytea"", s.""IntArray"", s.""IntList"", s.""NonNullableText"", s.""NullableIntArray"", s.""NullableIntList"", s.""NullableStringArray"", s.""NullableStringList"", s.""NullableText"", s.""StringArray"", s.""StringList"", s.""ValueConvertedArray"", s.""ValueConvertedList"", s.""ValueConvertedScalar"" +FROM ""SomeEntities"" AS s +WHERE ARRAY[2,3]::integer[] && s.""IntArray"""); + } + + public override async Task Any_Contains_between_column_and_List(bool async) + { + await base.Any_Contains_between_column_and_List(async); + + AssertSql( + @"@__ints_0={ '2', '3' } (DbType = Object) + +SELECT s.""Id"", s.""ArrayContainerEntityId"", s.""Byte"", s.""ByteArray"", s.""Bytea"", s.""IntArray"", s.""IntList"", s.""NonNullableText"", s.""NullableIntArray"", s.""NullableIntList"", s.""NullableStringArray"", s.""NullableStringList"", s.""NullableText"", s.""StringArray"", s.""StringList"", s.""ValueConvertedArray"", s.""ValueConvertedList"", s.""ValueConvertedScalar"" +FROM ""SomeEntities"" AS s +WHERE s.""IntArray"" && @__ints_0"); + } + + public override async Task Any_Contains_between_column_and_array(bool async) + { + await base.Any_Contains_between_column_and_array(async); + + AssertSql( + @"@__ints_0={ '2', '3' } (DbType = Object) + +SELECT s.""Id"", s.""ArrayContainerEntityId"", s.""Byte"", s.""ByteArray"", s.""Bytea"", s.""IntArray"", s.""IntList"", s.""NonNullableText"", s.""NullableIntArray"", s.""NullableIntList"", s.""NullableStringArray"", s.""NullableStringList"", s.""NullableText"", s.""StringArray"", s.""StringList"", s.""ValueConvertedArray"", s.""ValueConvertedList"", s.""ValueConvertedScalar"" +FROM ""SomeEntities"" AS s +WHERE s.""IntArray"" && @__ints_0"); + } + + public override async Task All_Contains(bool async) + { + await base.All_Contains(async); + + AssertSql( + @"SELECT s.""Id"", s.""ArrayContainerEntityId"", s.""Byte"", s.""ByteArray"", s.""Bytea"", s.""IntArray"", s.""IntList"", s.""NonNullableText"", s.""NullableIntArray"", s.""NullableIntList"", s.""NullableStringArray"", s.""NullableStringList"", s.""NullableText"", s.""StringArray"", s.""StringList"", s.""ValueConvertedArray"", s.""ValueConvertedList"", s.""ValueConvertedScalar"" +FROM ""SomeEntities"" AS s +WHERE ARRAY[5,6]::integer[] <@ s.""IntArray"""); + } + + #endregion Any/All + + #region Other translations + + public override async Task Append(bool async) + { + await base.Append(async); + + AssertSql( + @"SELECT s.""Id"", s.""ArrayContainerEntityId"", s.""Byte"", s.""ByteArray"", s.""Bytea"", s.""IntArray"", s.""IntList"", s.""NonNullableText"", s.""NullableIntArray"", s.""NullableIntList"", s.""NullableStringArray"", s.""NullableStringList"", s.""NullableText"", s.""StringArray"", s.""StringList"", s.""ValueConvertedArray"", s.""ValueConvertedList"", s.""ValueConvertedScalar"" +FROM ""SomeEntities"" AS s +WHERE array_append(s.""IntArray"", 5) = ARRAY[3,4,5]::integer[]"); + } + + public override async Task Concat(bool async) + { + await base.Concat(async); + + AssertSql( + @"SELECT s.""Id"", s.""ArrayContainerEntityId"", s.""Byte"", s.""ByteArray"", s.""Bytea"", s.""IntArray"", s.""IntList"", s.""NonNullableText"", s.""NullableIntArray"", s.""NullableIntList"", s.""NullableStringArray"", s.""NullableStringList"", s.""NullableText"", s.""StringArray"", s.""StringList"", s.""ValueConvertedArray"", s.""ValueConvertedList"", s.""ValueConvertedScalar"" +FROM ""SomeEntities"" AS s +WHERE array_cat(s.""IntArray"", ARRAY[5,6]::integer[]) = ARRAY[3,4,5,6]::integer[]"); + } + + public override async Task Array_IndexOf1(bool async) + { + await AssertQuery( + async, + ss => ss.Set().Where(e => Array.IndexOf(e.IntArray, 6) == 1), + entryCount: 1); + + AssertSql( + @"SELECT s.""Id"", s.""ArrayContainerEntityId"", s.""Byte"", s.""ByteArray"", s.""Bytea"", s.""IntArray"", s.""IntList"", s.""NonNullableText"", s.""NullableIntArray"", s.""NullableIntList"", s.""NullableStringArray"", s.""NullableStringList"", s.""NullableText"", s.""StringArray"", s.""StringList"", s.""ValueConvertedArray"", s.""ValueConvertedList"", s.""ValueConvertedScalar"" +FROM ""SomeEntities"" AS s +WHERE COALESCE(array_position(s.""IntArray"", 6) - 1, -1) = 1"); + } + + public override async Task Array_IndexOf2(bool async) + { + await AssertQuery( + async, + ss => ss.Set().Where(e => Array.IndexOf(e.IntArray, 6, 1) == 1), + entryCount: 1); + + AssertSql( + @"SELECT s.""Id"", s.""ArrayContainerEntityId"", s.""Byte"", s.""ByteArray"", s.""Bytea"", s.""IntArray"", s.""IntList"", s.""NonNullableText"", s.""NullableIntArray"", s.""NullableIntList"", s.""NullableStringArray"", s.""NullableStringList"", s.""NullableText"", s.""StringArray"", s.""StringList"", s.""ValueConvertedArray"", s.""ValueConvertedList"", s.""ValueConvertedScalar"" +FROM ""SomeEntities"" AS s +WHERE COALESCE(array_position(s.""IntArray"", 6, 2) - 1, -1) = 1"); + } + + public override async Task String_Join(bool async) + { + await base.String_Join(async); + + AssertSql( + @"SELECT s.""Id"", s.""ArrayContainerEntityId"", s.""Byte"", s.""ByteArray"", s.""Bytea"", s.""IntArray"", s.""IntList"", s.""NonNullableText"", s.""NullableIntArray"", s.""NullableIntList"", s.""NullableStringArray"", s.""NullableStringList"", s.""NullableText"", s.""StringArray"", s.""StringList"", s.""ValueConvertedArray"", s.""ValueConvertedList"", s.""ValueConvertedScalar"" +FROM ""SomeEntities"" AS s +WHERE array_to_string(s.""IntArray"", ', ', '') = '3, 4'"); + } + + #endregion Other translations + + public class ArrayArrayQueryFixture : ArrayQueryFixture + { + protected override string StoreName + => "ArrayQueryTest"; + } + } +} diff --git a/test/EFCore.PG.FunctionalTests/Query/ArrayListQueryTest.cs b/test/EFCore.PG.FunctionalTests/Query/ArrayListQueryTest.cs new file mode 100644 index 000000000..55f9b5b84 --- /dev/null +++ b/test/EFCore.PG.FunctionalTests/Query/ArrayListQueryTest.cs @@ -0,0 +1,777 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Linq.Expressions; +using System.Reflection; +using System.Threading.Tasks; +using Microsoft.EntityFrameworkCore; +using Npgsql.EntityFrameworkCore.PostgreSQL.TestModels.Array; +using Xunit; +using Xunit.Abstractions; + +namespace Npgsql.EntityFrameworkCore.PostgreSQL.Query +{ + public class ArrayListQueryTest : ArrayQueryTest + { + public ArrayListQueryTest(ArrayListQueryFixture fixture, ITestOutputHelper testOutputHelper) + : base(fixture, testOutputHelper) + { + } + + #region Indexers + + public override async Task Index_with_constant(bool async) + { + await base.Index_with_constant(async); + + AssertSql( + @"SELECT s.""Id"", s.""ArrayContainerEntityId"", s.""Byte"", s.""ByteArray"", s.""Bytea"", s.""IntArray"", s.""IntList"", s.""NonNullableText"", s.""NullableIntArray"", s.""NullableIntList"", s.""NullableStringArray"", s.""NullableStringList"", s.""NullableText"", s.""StringArray"", s.""StringList"", s.""ValueConvertedArray"", s.""ValueConvertedList"", s.""ValueConvertedScalar"" +FROM ""SomeEntities"" AS s +WHERE s.""IntList""[1] = 3"); + } + + public override async Task Index_with_parameter(bool async) + { + await base.Index_with_parameter(async); + + AssertSql( + @"@__x_0='0' + +SELECT s.""Id"", s.""ArrayContainerEntityId"", s.""Byte"", s.""ByteArray"", s.""Bytea"", s.""IntArray"", s.""IntList"", s.""NonNullableText"", s.""NullableIntArray"", s.""NullableIntList"", s.""NullableStringArray"", s.""NullableStringList"", s.""NullableText"", s.""StringArray"", s.""StringList"", s.""ValueConvertedArray"", s.""ValueConvertedList"", s.""ValueConvertedScalar"" +FROM ""SomeEntities"" AS s +WHERE s.""IntList""[@__x_0 + 1] = 3"); + } + + public override async Task Nullable_index_with_constant(bool async) + { + await base.Nullable_index_with_constant(async); + + AssertSql( + @"SELECT s.""Id"", s.""ArrayContainerEntityId"", s.""Byte"", s.""ByteArray"", s.""Bytea"", s.""IntArray"", s.""IntList"", s.""NonNullableText"", s.""NullableIntArray"", s.""NullableIntList"", s.""NullableStringArray"", s.""NullableStringList"", s.""NullableText"", s.""StringArray"", s.""StringList"", s.""ValueConvertedArray"", s.""ValueConvertedList"", s.""ValueConvertedScalar"" +FROM ""SomeEntities"" AS s +WHERE s.""NullableIntList""[1] = 3"); + } + + public override async Task Nullable_value_array_index_compare_to_null(bool async) + { + await base.Nullable_value_array_index_compare_to_null(async); + + AssertSql( + @"SELECT s.""Id"", s.""ArrayContainerEntityId"", s.""Byte"", s.""ByteArray"", s.""Bytea"", s.""IntArray"", s.""IntList"", s.""NonNullableText"", s.""NullableIntArray"", s.""NullableIntList"", s.""NullableStringArray"", s.""NullableStringList"", s.""NullableText"", s.""StringArray"", s.""StringList"", s.""ValueConvertedArray"", s.""ValueConvertedList"", s.""ValueConvertedScalar"" +FROM ""SomeEntities"" AS s +WHERE (s.""NullableIntList""[3] IS NULL)"); + } + + public override async Task Non_nullable_value_array_index_compare_to_null(bool async) + { + await base.Non_nullable_value_array_index_compare_to_null(async); + + AssertSql( + @"SELECT s.""Id"", s.""ArrayContainerEntityId"", s.""Byte"", s.""ByteArray"", s.""Bytea"", s.""IntArray"", s.""IntList"", s.""NonNullableText"", s.""NullableIntArray"", s.""NullableIntList"", s.""NullableStringArray"", s.""NullableStringList"", s.""NullableText"", s.""StringArray"", s.""StringList"", s.""ValueConvertedArray"", s.""ValueConvertedList"", s.""ValueConvertedScalar"" +FROM ""SomeEntities"" AS s +WHERE FALSE"); + } + + public override async Task Nullable_reference_array_index_compare_to_null(bool async) + { + await base.Nullable_reference_array_index_compare_to_null(async); + + AssertSql( + @"SELECT s.""Id"", s.""ArrayContainerEntityId"", s.""Byte"", s.""ByteArray"", s.""Bytea"", s.""IntArray"", s.""IntList"", s.""NonNullableText"", s.""NullableIntArray"", s.""NullableIntList"", s.""NullableStringArray"", s.""NullableStringList"", s.""NullableText"", s.""StringArray"", s.""StringList"", s.""ValueConvertedArray"", s.""ValueConvertedList"", s.""ValueConvertedScalar"" +FROM ""SomeEntities"" AS s +WHERE (s.""NullableStringList""[3] IS NULL)"); + } + + public override async Task Non_nullable_reference_array_index_compare_to_null(bool async) + { + await base.Non_nullable_reference_array_index_compare_to_null(async); + + AssertSql( + @"SELECT s.""Id"", s.""ArrayContainerEntityId"", s.""Byte"", s.""ByteArray"", s.""Bytea"", s.""IntArray"", s.""IntList"", s.""NonNullableText"", s.""NullableIntArray"", s.""NullableIntList"", s.""NullableStringArray"", s.""NullableStringList"", s.""NullableText"", s.""StringArray"", s.""StringList"", s.""ValueConvertedArray"", s.""ValueConvertedList"", s.""ValueConvertedScalar"" +FROM ""SomeEntities"" AS s +WHERE FALSE"); + } + + #endregion + + #region SequenceEqual + + public override async Task SequenceEqual_with_parameter(bool async) + { + await base.SequenceEqual_with_parameter(async); + + AssertSql( + @"@__arr_0={ '3', '4' } (DbType = Object) + +SELECT s.""Id"", s.""ArrayContainerEntityId"", s.""Byte"", s.""ByteArray"", s.""Bytea"", s.""IntArray"", s.""IntList"", s.""NonNullableText"", s.""NullableIntArray"", s.""NullableIntList"", s.""NullableStringArray"", s.""NullableStringList"", s.""NullableText"", s.""StringArray"", s.""StringList"", s.""ValueConvertedArray"", s.""ValueConvertedList"", s.""ValueConvertedScalar"" +FROM ""SomeEntities"" AS s +WHERE s.""IntList"" = @__arr_0"); + } + + public override async Task SequenceEqual_with_array_literal(bool async) + { + await base.SequenceEqual_with_array_literal(async); + + AssertSql( + @"SELECT s.""Id"", s.""ArrayContainerEntityId"", s.""Byte"", s.""ByteArray"", s.""Bytea"", s.""IntArray"", s.""IntList"", s.""NonNullableText"", s.""NullableIntArray"", s.""NullableIntList"", s.""NullableStringArray"", s.""NullableStringList"", s.""NullableText"", s.""StringArray"", s.""StringList"", s.""ValueConvertedArray"", s.""ValueConvertedList"", s.""ValueConvertedScalar"" +FROM ""SomeEntities"" AS s +WHERE s.""IntList"" = ARRAY[3,4]::integer[]"); + } + + public override async Task SequenceEqual_over_nullable_with_parameter(bool async) + { + await base.SequenceEqual_over_nullable_with_parameter(async); + + AssertSql( + @"@__arr_0={ '3', '4', NULL } (DbType = Object) + +SELECT s.""Id"", s.""ArrayContainerEntityId"", s.""Byte"", s.""ByteArray"", s.""Bytea"", s.""IntArray"", s.""IntList"", s.""NonNullableText"", s.""NullableIntArray"", s.""NullableIntList"", s.""NullableStringArray"", s.""NullableStringList"", s.""NullableText"", s.""StringArray"", s.""StringList"", s.""ValueConvertedArray"", s.""ValueConvertedList"", s.""ValueConvertedScalar"" +FROM ""SomeEntities"" AS s +WHERE s.""NullableIntList"" = @__arr_0"); + } + + #endregion SequenceEqual + + #region Containment + + public override async Task Array_column_Any_equality_operator(bool async) + { + await base.Array_column_Any_equality_operator(async); + + AssertSql( + @"SELECT s.""Id"", s.""ArrayContainerEntityId"", s.""Byte"", s.""ByteArray"", s.""Bytea"", s.""IntArray"", s.""IntList"", s.""NonNullableText"", s.""NullableIntArray"", s.""NullableIntList"", s.""NullableStringArray"", s.""NullableStringList"", s.""NullableText"", s.""StringArray"", s.""StringList"", s.""ValueConvertedArray"", s.""ValueConvertedList"", s.""ValueConvertedScalar"" +FROM ""SomeEntities"" AS s +WHERE s.""StringList"" @> ARRAY['3']::text[]"); + } + + public override async Task Array_column_Any_Equals(bool async) + { + await base.Array_column_Any_Equals(async); + + AssertSql( + @"SELECT s.""Id"", s.""ArrayContainerEntityId"", s.""Byte"", s.""ByteArray"", s.""Bytea"", s.""IntArray"", s.""IntList"", s.""NonNullableText"", s.""NullableIntArray"", s.""NullableIntList"", s.""NullableStringArray"", s.""NullableStringList"", s.""NullableText"", s.""StringArray"", s.""StringList"", s.""ValueConvertedArray"", s.""ValueConvertedList"", s.""ValueConvertedScalar"" +FROM ""SomeEntities"" AS s +WHERE s.""StringList"" @> ARRAY['3']::text[]"); + } + + public override async Task Array_column_Contains_literal_item(bool async) + { + await base.Array_column_Contains_literal_item(async); + + AssertSql( + @"SELECT s.""Id"", s.""ArrayContainerEntityId"", s.""Byte"", s.""ByteArray"", s.""Bytea"", s.""IntArray"", s.""IntList"", s.""NonNullableText"", s.""NullableIntArray"", s.""NullableIntList"", s.""NullableStringArray"", s.""NullableStringList"", s.""NullableText"", s.""StringArray"", s.""StringList"", s.""ValueConvertedArray"", s.""ValueConvertedList"", s.""ValueConvertedScalar"" +FROM ""SomeEntities"" AS s +WHERE s.""IntList"" @> ARRAY[3]::integer[]"); + } + + public override async Task Array_column_Contains_parameter_item(bool async) + { + await base.Array_column_Contains_parameter_item(async); + + AssertSql( + @"@__p_0='3' + +SELECT s.""Id"", s.""ArrayContainerEntityId"", s.""Byte"", s.""ByteArray"", s.""Bytea"", s.""IntArray"", s.""IntList"", s.""NonNullableText"", s.""NullableIntArray"", s.""NullableIntList"", s.""NullableStringArray"", s.""NullableStringList"", s.""NullableText"", s.""StringArray"", s.""StringList"", s.""ValueConvertedArray"", s.""ValueConvertedList"", s.""ValueConvertedScalar"" +FROM ""SomeEntities"" AS s +WHERE s.""IntList"" @> ARRAY[@__p_0]::integer[]"); + } + + public override async Task Array_column_Contains_column_item(bool async) + { + await base.Array_column_Contains_column_item(async); + + AssertSql( + @"SELECT s.""Id"", s.""ArrayContainerEntityId"", s.""Byte"", s.""ByteArray"", s.""Bytea"", s.""IntArray"", s.""IntList"", s.""NonNullableText"", s.""NullableIntArray"", s.""NullableIntList"", s.""NullableStringArray"", s.""NullableStringList"", s.""NullableText"", s.""StringArray"", s.""StringList"", s.""ValueConvertedArray"", s.""ValueConvertedList"", s.""ValueConvertedScalar"" +FROM ""SomeEntities"" AS s +WHERE s.""IntList"" @> ARRAY[s.""Id"" + 2]::integer[]"); + } + + public override async Task Array_column_Contains_null_constant(bool async) + { + await base.Array_column_Contains_null_constant(async); + + AssertSql( + @"SELECT s.""Id"", s.""ArrayContainerEntityId"", s.""Byte"", s.""ByteArray"", s.""Bytea"", s.""IntArray"", s.""IntList"", s.""NonNullableText"", s.""NullableIntArray"", s.""NullableIntList"", s.""NullableStringArray"", s.""NullableStringList"", s.""NullableText"", s.""StringArray"", s.""StringList"", s.""ValueConvertedArray"", s.""ValueConvertedList"", s.""ValueConvertedScalar"" +FROM ""SomeEntities"" AS s +WHERE (array_position(s.""NullableStringList"", NULL) IS NOT NULL)"); + } + + public override void Array_column_Contains_null_parameter_does_not_work() + { + using var ctx = CreateContext(); + + string p = null; + + // We incorrectly miss arrays containing non-constant nulls, because detecting those + // would prevent index use. + Assert.Equal( + 0, + ctx.SomeEntities.Count(e => e.StringList.Contains(p))); + + AssertSql( + @"SELECT COUNT(*)::INT +FROM ""SomeEntities"" AS s +WHERE s.""StringList"" @> ARRAY[NULL]::text[]"); + } + + public override async Task Nullable_array_column_Contains_literal_item(bool async) + { + await base.Nullable_array_column_Contains_literal_item(async); + + AssertSql( + @"SELECT s.""Id"", s.""ArrayContainerEntityId"", s.""Byte"", s.""ByteArray"", s.""Bytea"", s.""IntArray"", s.""IntList"", s.""NonNullableText"", s.""NullableIntArray"", s.""NullableIntList"", s.""NullableStringArray"", s.""NullableStringList"", s.""NullableText"", s.""StringArray"", s.""StringList"", s.""ValueConvertedArray"", s.""ValueConvertedList"", s.""ValueConvertedScalar"" +FROM ""SomeEntities"" AS s +WHERE s.""NullableIntList"" @> ARRAY[3]::integer[]"); + } + + public override async Task Array_constant_Contains_column(bool async) + { + await AssertQuery( + async, + ss => ss.Set().Where(e => new[] { "foo", "xxx" }.Contains(e.NullableText)), + entryCount: 1); + + AssertSql( + @"SELECT s.""Id"", s.""ArrayContainerEntityId"", s.""Byte"", s.""ByteArray"", s.""Bytea"", s.""IntArray"", s.""IntList"", s.""NonNullableText"", s.""NullableIntArray"", s.""NullableIntList"", s.""NullableStringArray"", s.""NullableStringList"", s.""NullableText"", s.""StringArray"", s.""StringList"", s.""ValueConvertedArray"", s.""ValueConvertedList"", s.""ValueConvertedScalar"" +FROM ""SomeEntities"" AS s +WHERE s.""NullableText"" IN ('foo', 'xxx')"); + } + + public override async Task Array_param_Contains_nullable_column(bool async) + { + var array = new List { "foo", "xxx" }; + + await AssertQuery( + async, + ss => ss.Set().Where(e => array.Contains(e.NullableText)), + entryCount: 1); + + AssertSql( + @"@__array_0={ 'foo', 'xxx' } (DbType = Object) + +SELECT s.""Id"", s.""ArrayContainerEntityId"", s.""Byte"", s.""ByteArray"", s.""Bytea"", s.""IntArray"", s.""IntList"", s.""NonNullableText"", s.""NullableIntArray"", s.""NullableIntList"", s.""NullableStringArray"", s.""NullableStringList"", s.""NullableText"", s.""StringArray"", s.""StringList"", s.""ValueConvertedArray"", s.""ValueConvertedList"", s.""ValueConvertedScalar"" +FROM ""SomeEntities"" AS s +WHERE s.""NullableText"" = ANY (@__array_0) OR ((s.""NullableText"" IS NULL) AND (array_position(@__array_0, NULL) IS NOT NULL))"); + } + + public override async Task Array_param_Contains_non_nullable_column(bool async) + { + var array = new List { 1 }; + + await AssertQuery( + async, + ss => ss.Set().Where(e => array.Contains(e.Id)), + entryCount: 1); + + AssertSql( + @"@__array_0={ '1' } (DbType = Object) + +SELECT s.""Id"", s.""ArrayContainerEntityId"", s.""Byte"", s.""ByteArray"", s.""Bytea"", s.""IntArray"", s.""IntList"", s.""NonNullableText"", s.""NullableIntArray"", s.""NullableIntList"", s.""NullableStringArray"", s.""NullableStringList"", s.""NullableText"", s.""StringArray"", s.""StringList"", s.""ValueConvertedArray"", s.""ValueConvertedList"", s.""ValueConvertedScalar"" +FROM ""SomeEntities"" AS s +WHERE s.""Id"" = ANY (@__array_0)"); + } + + public override void Array_param_with_null_Contains_non_nullable_not_found() + { + using var ctx = CreateContext(); + + var array = new List + { + "unknown1", + "unknown2", + null + }; + + Assert.Equal(0, ctx.SomeEntities.Count(e => array.Contains(e.NonNullableText))); + + AssertSql( + @"@__array_0={ 'unknown1', 'unknown2', NULL } (DbType = Object) + +SELECT COUNT(*)::INT +FROM ""SomeEntities"" AS s +WHERE s.""NonNullableText"" = ANY (@__array_0)"); + } + + public override void Array_param_with_null_Contains_non_nullable_not_found_negated() + { + using var ctx = CreateContext(); + + var array = new List + { + "unknown1", + "unknown2", + null + }; + + Assert.Equal(2, ctx.SomeEntities.Count(e => !array.Contains(e.NonNullableText))); + + AssertSql( + @"@__array_0={ 'unknown1', 'unknown2', NULL } (DbType = Object) + +SELECT COUNT(*)::INT +FROM ""SomeEntities"" AS s +WHERE NOT (s.""NonNullableText"" = ANY (@__array_0) AND (s.""NonNullableText"" = ANY (@__array_0) IS NOT NULL))"); + } + + public override void Array_param_with_null_Contains_nullable_not_found() + { + using var ctx = CreateContext(); + + var array = new List + { + "unknown1", + "unknown2", + null + }; + + Assert.Equal(0, ctx.SomeEntities.Count(e => array.Contains(e.NullableText))); + + AssertSql( + @"@__array_0={ 'unknown1', 'unknown2', NULL } (DbType = Object) + +SELECT COUNT(*)::INT +FROM ""SomeEntities"" AS s +WHERE s.""NullableText"" = ANY (@__array_0) OR ((s.""NullableText"" IS NULL) AND (array_position(@__array_0, NULL) IS NOT NULL))"); + } + + public override void Array_param_with_null_Contains_nullable_not_found_negated() + { + using var ctx = CreateContext(); + + var array = new List + { + "unknown1", + "unknown2", + null + }; + + Assert.Equal(2, ctx.SomeEntities.Count(e => !array.Contains(e.NullableText))); + + AssertSql( + @"@__array_0={ 'unknown1', 'unknown2', NULL } (DbType = Object) + +SELECT COUNT(*)::INT +FROM ""SomeEntities"" AS s +WHERE NOT (s.""NullableText"" = ANY (@__array_0) AND (s.""NullableText"" = ANY (@__array_0) IS NOT NULL)) AND ((s.""NullableText"" IS NOT NULL) OR (array_position(@__array_0, NULL) IS NULL))"); + } + + public override async Task Byte_array_parameter_contains_column(bool async) + { + var values = new List { 20 }; + + await AssertQuery( + async, + ss => ss.Set().Where(e => values.Contains(e.Byte)), + entryCount: 1); + + AssertSql( + @"@__values_0={ '20' } (DbType = Object) + +SELECT s.""Id"", s.""ArrayContainerEntityId"", s.""Byte"", s.""ByteArray"", s.""Bytea"", s.""IntArray"", s.""IntList"", s.""NonNullableText"", s.""NullableIntArray"", s.""NullableIntList"", s.""NullableStringArray"", s.""NullableStringList"", s.""NullableText"", s.""StringArray"", s.""StringList"", s.""ValueConvertedArray"", s.""ValueConvertedList"", s.""ValueConvertedScalar"" +FROM ""SomeEntities"" AS s +WHERE s.""Byte"" = ANY (@__values_0)"); + } + + public override async Task Array_param_Contains_value_converted_column(bool async) + { + var array = new List { SomeEnum.Two, SomeEnum.Three }; + + await AssertQuery( + async, + ss => ss.Set().Where(e => array.Contains(e.ValueConvertedScalar)), + entryCount: 1); + + AssertSql( + @"@__array_0={ '-2', '-3' } (DbType = Object) + +SELECT s.""Id"", s.""ArrayContainerEntityId"", s.""Byte"", s.""ByteArray"", s.""Bytea"", s.""IntArray"", s.""IntList"", s.""NonNullableText"", s.""NullableIntArray"", s.""NullableIntList"", s.""NullableStringArray"", s.""NullableStringList"", s.""NullableText"", s.""StringArray"", s.""StringList"", s.""ValueConvertedArray"", s.""ValueConvertedList"", s.""ValueConvertedScalar"" +FROM ""SomeEntities"" AS s +WHERE s.""ValueConvertedScalar"" = ANY (@__array_0)"); + } + + public override async Task Array_column_Contains_value_converted_param(bool async) + { + await base.Array_column_Contains_value_converted_param(async); + + AssertSql( + @"@__item_0='-8' + +SELECT s.""Id"", s.""ArrayContainerEntityId"", s.""Byte"", s.""ByteArray"", s.""Bytea"", s.""IntArray"", s.""IntList"", s.""NonNullableText"", s.""NullableIntArray"", s.""NullableIntList"", s.""NullableStringArray"", s.""NullableStringList"", s.""NullableText"", s.""StringArray"", s.""StringList"", s.""ValueConvertedArray"", s.""ValueConvertedList"", s.""ValueConvertedScalar"" +FROM ""SomeEntities"" AS s +WHERE s.""ValueConvertedList"" @> ARRAY[@__item_0]::integer[]"); + } + + public override async Task Array_param_Contains_value_converted_array_column(bool async) + { + var p = new List { SomeEnum.Eight, SomeEnum.Nine }; + + await AssertQuery( + async, + ss => ss.Set().Where(e => e.ValueConvertedArray.All(x => p.Contains(x))), + entryCount: 1); + + AssertSql( + @"@__p_0={ '-8', '-9' } (DbType = Object) + +SELECT s.""Id"", s.""ArrayContainerEntityId"", s.""Byte"", s.""ByteArray"", s.""Bytea"", s.""IntArray"", s.""IntList"", s.""NonNullableText"", s.""NullableIntArray"", s.""NullableIntList"", s.""NullableStringArray"", s.""NullableStringList"", s.""NullableText"", s.""StringArray"", s.""StringList"", s.""ValueConvertedArray"", s.""ValueConvertedList"", s.""ValueConvertedScalar"" +FROM ""SomeEntities"" AS s +WHERE s.""ValueConvertedList"" <@ @__p_0"); + } + + public override async Task Array_column_Contains_in_scalar_subquery(bool async) + { + await base.Array_column_Contains_in_scalar_subquery(async); + + AssertSql( + @"SELECT s.""Id"" +FROM ""SomeEntityContainers"" AS s +WHERE 3 = ANY (( + SELECT s0.""NullableIntList"" + FROM ""SomeEntities"" AS s0 + WHERE s.""Id"" = s0.""ArrayContainerEntityId"" + ORDER BY s0.""Id"" NULLS FIRST + LIMIT 1)::integer[])"); + } + + #endregion Containment + + #region Length/Count + + public override async Task Array_Length(bool async) + { + await AssertQuery( + async, + ss => ss.Set().Where(e => e.IntList.Count == 2), + entryCount: 1); + + AssertSql( + @"SELECT s.""Id"", s.""ArrayContainerEntityId"", s.""Byte"", s.""ByteArray"", s.""Bytea"", s.""IntArray"", s.""IntList"", s.""NonNullableText"", s.""NullableIntArray"", s.""NullableIntList"", s.""NullableStringArray"", s.""NullableStringList"", s.""NullableText"", s.""StringArray"", s.""StringList"", s.""ValueConvertedArray"", s.""ValueConvertedList"", s.""ValueConvertedScalar"" +FROM ""SomeEntities"" AS s +WHERE cardinality(s.""IntList"") = 2"); + } + + public override async Task Nullable_array_Length(bool async) + { + await AssertQuery( + async, + ss => ss.Set().Where(e => e.NullableIntList.Count == 3), + entryCount: 1); + + AssertSql( + @"SELECT s.""Id"", s.""ArrayContainerEntityId"", s.""Byte"", s.""ByteArray"", s.""Bytea"", s.""IntArray"", s.""IntList"", s.""NonNullableText"", s.""NullableIntArray"", s.""NullableIntList"", s.""NullableStringArray"", s.""NullableStringList"", s.""NullableText"", s.""StringArray"", s.""StringList"", s.""ValueConvertedArray"", s.""ValueConvertedList"", s.""ValueConvertedScalar"" +FROM ""SomeEntities"" AS s +WHERE cardinality(s.""NullableIntList"") = 3"); + } + + public override async Task Array_Length_on_EF_Property(bool async) + { + await AssertQuery( + async, + ss => ss.Set().Where(e => EF.Property>(e, nameof(ArrayEntity.IntList)).Count == 2), + entryCount: 1); + + AssertSql( + @"SELECT s.""Id"", s.""ArrayContainerEntityId"", s.""Byte"", s.""ByteArray"", s.""Bytea"", s.""IntArray"", s.""IntList"", s.""NonNullableText"", s.""NullableIntArray"", s.""NullableIntList"", s.""NullableStringArray"", s.""NullableStringList"", s.""NullableText"", s.""StringArray"", s.""StringList"", s.""ValueConvertedArray"", s.""ValueConvertedList"", s.""ValueConvertedScalar"" +FROM ""SomeEntities"" AS s +WHERE cardinality(s.""IntList"") = 2"); + } + + #endregion Length/Count + + #region Any/All + + public override async Task Any_no_predicate(bool async) + { + await base.Any_no_predicate(async); + + AssertSql( + @"SELECT s.""Id"", s.""ArrayContainerEntityId"", s.""Byte"", s.""ByteArray"", s.""Bytea"", s.""IntArray"", s.""IntList"", s.""NonNullableText"", s.""NullableIntArray"", s.""NullableIntList"", s.""NullableStringArray"", s.""NullableStringList"", s.""NullableText"", s.""StringArray"", s.""StringList"", s.""ValueConvertedArray"", s.""ValueConvertedList"", s.""ValueConvertedScalar"" +FROM ""SomeEntities"" AS s +WHERE cardinality(s.""IntList"") > 0"); + } + + public override async Task Any_like(bool async) + { + await AssertQuery( + async, + ss => ss.Set() + .Where(e => new[] { "a%", "b%", "c%" }.Any(p => EF.Functions.Like(e.NullableText, p))), + ss => ss.Set() + .Where(e => new[] { "a", "b", "c" }.Any(p => e.NullableText.StartsWith(p, StringComparison.Ordinal))), + entryCount: 1); + + AssertSql( + @"SELECT s.""Id"", s.""ArrayContainerEntityId"", s.""Byte"", s.""ByteArray"", s.""Bytea"", s.""IntArray"", s.""IntList"", s.""NonNullableText"", s.""NullableIntArray"", s.""NullableIntList"", s.""NullableStringArray"", s.""NullableStringList"", s.""NullableText"", s.""StringArray"", s.""StringList"", s.""ValueConvertedArray"", s.""ValueConvertedList"", s.""ValueConvertedScalar"" +FROM ""SomeEntities"" AS s +WHERE s.""NullableText"" LIKE ANY (ARRAY['a%','b%','c%']::text[])"); + } + + public override async Task Any_ilike(bool async) + { + await AssertQuery( + async, + ss => ss.Set() + .Where(e => new[] { "a%", "b%", "c%" }.Any(p => EF.Functions.ILike(e.NullableText, p))), + ss => ss.Set() + .Where(e => new[] { "a", "b", "c" }.Any(p => e.NullableText.StartsWith(p, StringComparison.OrdinalIgnoreCase))), + entryCount: 1); + + AssertSql( + @"SELECT s.""Id"", s.""ArrayContainerEntityId"", s.""Byte"", s.""ByteArray"", s.""Bytea"", s.""IntArray"", s.""IntList"", s.""NonNullableText"", s.""NullableIntArray"", s.""NullableIntList"", s.""NullableStringArray"", s.""NullableStringList"", s.""NullableText"", s.""StringArray"", s.""StringList"", s.""ValueConvertedArray"", s.""ValueConvertedList"", s.""ValueConvertedScalar"" +FROM ""SomeEntities"" AS s +WHERE s.""NullableText"" ILIKE ANY (ARRAY['a%','b%','c%']::text[])"); + } + + public override async Task Any_like_anonymous(bool async) + { + using var ctx = CreateContext(); + + var patternsActual = new List { "a%", "b%", "c%" }; + var patternsExpected = new List { "a", "b", "c" }; + + await AssertQuery( + async, + ss => ss.Set() + .Where(e => patternsActual.Any(p => EF.Functions.Like(e.NullableText, p))), + ss => ss.Set() + .Where(e => patternsExpected.Any(p => e.NullableText.StartsWith(p, StringComparison.Ordinal))), + entryCount: 1); + + AssertSql( + @"@__patternsActual_0={ 'a%', 'b%', 'c%' } (DbType = Object) + +SELECT s.""Id"", s.""ArrayContainerEntityId"", s.""Byte"", s.""ByteArray"", s.""Bytea"", s.""IntArray"", s.""IntList"", s.""NonNullableText"", s.""NullableIntArray"", s.""NullableIntList"", s.""NullableStringArray"", s.""NullableStringList"", s.""NullableText"", s.""StringArray"", s.""StringList"", s.""ValueConvertedArray"", s.""ValueConvertedList"", s.""ValueConvertedScalar"" +FROM ""SomeEntities"" AS s +WHERE s.""NullableText"" LIKE ANY (@__patternsActual_0)"); + } + + public override async Task All_like(bool async) + { + await AssertQuery( + async, + ss => ss.Set() + .Where(e => new List { "b%", "ba%" }.All(p => EF.Functions.Like(e.NullableText, p))), + ss => ss.Set() + .Where(e => new List { "b", "ba" }.All(p => e.NullableText.StartsWith(p, StringComparison.Ordinal))), + entryCount: 1); + + AssertSql( + @"SELECT s.""Id"", s.""ArrayContainerEntityId"", s.""Byte"", s.""ByteArray"", s.""Bytea"", s.""IntArray"", s.""IntList"", s.""NonNullableText"", s.""NullableIntArray"", s.""NullableIntList"", s.""NullableStringArray"", s.""NullableStringList"", s.""NullableText"", s.""StringArray"", s.""StringList"", s.""ValueConvertedArray"", s.""ValueConvertedList"", s.""ValueConvertedScalar"" +FROM ""SomeEntities"" AS s +WHERE s.""NullableText"" LIKE ALL (ARRAY['b%','ba%']::text[])"); + } + + public override async Task All_ilike(bool async) + { + await AssertQuery( + async, + ss => ss.Set() + .Where(e => new List { "B%", "ba%" }.All(p => EF.Functions.ILike(e.NullableText, p))), + ss => ss.Set() + .Where(e => new List { "B", "ba" }.All(p => e.NullableText.StartsWith(p, StringComparison.OrdinalIgnoreCase))), + entryCount: 1); + + AssertSql( + @"SELECT s.""Id"", s.""ArrayContainerEntityId"", s.""Byte"", s.""ByteArray"", s.""Bytea"", s.""IntArray"", s.""IntList"", s.""NonNullableText"", s.""NullableIntArray"", s.""NullableIntList"", s.""NullableStringArray"", s.""NullableStringList"", s.""NullableText"", s.""StringArray"", s.""StringList"", s.""ValueConvertedArray"", s.""ValueConvertedList"", s.""ValueConvertedScalar"" +FROM ""SomeEntities"" AS s +WHERE s.""NullableText"" ILIKE ALL (ARRAY['B%','ba%']::text[])"); + } + + public override async Task Any_Contains_on_constant_array(bool async) + { + await base.Any_Contains_on_constant_array(async); + + AssertSql( + @"SELECT s.""Id"", s.""ArrayContainerEntityId"", s.""Byte"", s.""ByteArray"", s.""Bytea"", s.""IntArray"", s.""IntList"", s.""NonNullableText"", s.""NullableIntArray"", s.""NullableIntList"", s.""NullableStringArray"", s.""NullableStringList"", s.""NullableText"", s.""StringArray"", s.""StringList"", s.""ValueConvertedArray"", s.""ValueConvertedList"", s.""ValueConvertedScalar"" +FROM ""SomeEntities"" AS s +WHERE ARRAY[2,3]::integer[] && s.""IntList"""); + } + + public override async Task Any_Contains_between_column_and_List(bool async) + { + await base.Any_Contains_between_column_and_List(async); + + AssertSql( + @"@__ints_0={ '2', '3' } (DbType = Object) + +SELECT s.""Id"", s.""ArrayContainerEntityId"", s.""Byte"", s.""ByteArray"", s.""Bytea"", s.""IntArray"", s.""IntList"", s.""NonNullableText"", s.""NullableIntArray"", s.""NullableIntList"", s.""NullableStringArray"", s.""NullableStringList"", s.""NullableText"", s.""StringArray"", s.""StringList"", s.""ValueConvertedArray"", s.""ValueConvertedList"", s.""ValueConvertedScalar"" +FROM ""SomeEntities"" AS s +WHERE s.""IntList"" && @__ints_0"); + } + + public override async Task Any_Contains_between_column_and_array(bool async) + { + await base.Any_Contains_between_column_and_array(async); + + AssertSql( + @"@__ints_0={ '2', '3' } (DbType = Object) + +SELECT s.""Id"", s.""ArrayContainerEntityId"", s.""Byte"", s.""ByteArray"", s.""Bytea"", s.""IntArray"", s.""IntList"", s.""NonNullableText"", s.""NullableIntArray"", s.""NullableIntList"", s.""NullableStringArray"", s.""NullableStringList"", s.""NullableText"", s.""StringArray"", s.""StringList"", s.""ValueConvertedArray"", s.""ValueConvertedList"", s.""ValueConvertedScalar"" +FROM ""SomeEntities"" AS s +WHERE s.""IntList"" && @__ints_0"); + } + + public override async Task All_Contains(bool async) + { + await base.All_Contains(async); + + AssertSql( + @"SELECT s.""Id"", s.""ArrayContainerEntityId"", s.""Byte"", s.""ByteArray"", s.""Bytea"", s.""IntArray"", s.""IntList"", s.""NonNullableText"", s.""NullableIntArray"", s.""NullableIntList"", s.""NullableStringArray"", s.""NullableStringList"", s.""NullableText"", s.""StringArray"", s.""StringList"", s.""ValueConvertedArray"", s.""ValueConvertedList"", s.""ValueConvertedScalar"" +FROM ""SomeEntities"" AS s +WHERE ARRAY[5,6]::integer[] <@ s.""IntList"""); + } + + #endregion Any/All + + #region Other translations + + public override async Task Append(bool async) + { + await base.Append(async); + + AssertSql( + @"SELECT s.""Id"", s.""ArrayContainerEntityId"", s.""Byte"", s.""ByteArray"", s.""Bytea"", s.""IntArray"", s.""IntList"", s.""NonNullableText"", s.""NullableIntArray"", s.""NullableIntList"", s.""NullableStringArray"", s.""NullableStringList"", s.""NullableText"", s.""StringArray"", s.""StringList"", s.""ValueConvertedArray"", s.""ValueConvertedList"", s.""ValueConvertedScalar"" +FROM ""SomeEntities"" AS s +WHERE array_append(s.""IntList"", 5) = ARRAY[3,4,5]::integer[]"); + } + + public override async Task Concat(bool async) + { + await base.Concat(async); + + AssertSql( + @"SELECT s.""Id"", s.""ArrayContainerEntityId"", s.""Byte"", s.""ByteArray"", s.""Bytea"", s.""IntArray"", s.""IntList"", s.""NonNullableText"", s.""NullableIntArray"", s.""NullableIntList"", s.""NullableStringArray"", s.""NullableStringList"", s.""NullableText"", s.""StringArray"", s.""StringList"", s.""ValueConvertedArray"", s.""ValueConvertedList"", s.""ValueConvertedScalar"" +FROM ""SomeEntities"" AS s +WHERE array_cat(s.""IntList"", ARRAY[5,6]::integer[]) = ARRAY[3,4,5,6]::integer[]"); + } + + [Theory] + [MemberData(nameof(IsAsyncData))] + public override async Task Array_IndexOf1(bool async) + { + await AssertQuery( + async, + ss => ss.Set().Where(e => e.IntList.IndexOf(6) == 1), + entryCount: 1); + + AssertSql( + @"SELECT s.""Id"", s.""ArrayContainerEntityId"", s.""Byte"", s.""ByteArray"", s.""Bytea"", s.""IntArray"", s.""IntList"", s.""NonNullableText"", s.""NullableIntArray"", s.""NullableIntList"", s.""NullableStringArray"", s.""NullableStringList"", s.""NullableText"", s.""StringArray"", s.""StringList"", s.""ValueConvertedArray"", s.""ValueConvertedList"", s.""ValueConvertedScalar"" +FROM ""SomeEntities"" AS s +WHERE COALESCE(array_position(s.""IntList"", 6) - 1, -1) = 1"); + } + + [Theory] + [MemberData(nameof(IsAsyncData))] + public override async Task Array_IndexOf2(bool async) + { + await AssertQuery( + async, + ss => ss.Set().Where(e => e.IntList.IndexOf(6, 1) == 1), + entryCount: 1); + + AssertSql( + @"SELECT s.""Id"", s.""ArrayContainerEntityId"", s.""Byte"", s.""ByteArray"", s.""Bytea"", s.""IntArray"", s.""IntList"", s.""NonNullableText"", s.""NullableIntArray"", s.""NullableIntList"", s.""NullableStringArray"", s.""NullableStringList"", s.""NullableText"", s.""StringArray"", s.""StringList"", s.""ValueConvertedArray"", s.""ValueConvertedList"", s.""ValueConvertedScalar"" +FROM ""SomeEntities"" AS s +WHERE COALESCE(array_position(s.""IntList"", 6, 2) - 1, -1) = 1"); + } + + public override async Task String_Join(bool async) + { + await base.String_Join(async); + + AssertSql( + @"SELECT s.""Id"", s.""ArrayContainerEntityId"", s.""Byte"", s.""ByteArray"", s.""Bytea"", s.""IntArray"", s.""IntList"", s.""NonNullableText"", s.""NullableIntArray"", s.""NullableIntList"", s.""NullableStringArray"", s.""NullableStringList"", s.""NullableText"", s.""StringArray"", s.""StringList"", s.""ValueConvertedArray"", s.""ValueConvertedList"", s.""ValueConvertedScalar"" +FROM ""SomeEntities"" AS s +WHERE array_to_string(s.""IntList"", ', ', '') = '3, 4'"); + } + + #endregion Other translations + + public class ArrayListQueryFixture : ArrayQueryFixture + { + protected override string StoreName + => "ArrayListTest"; + } + + protected override Expression RewriteServerQueryExpression(Expression serverQueryExpression) + => new ArrayToListReplacingExpressionVisitor().Visit(serverQueryExpression); + + internal class ArrayToListReplacingExpressionVisitor : ExpressionVisitor + { + private static readonly PropertyInfo IntArray + = typeof(ArrayEntity).GetProperty(nameof(ArrayEntity.IntArray)); + + private static readonly PropertyInfo NullableIntArray + = typeof(ArrayEntity).GetProperty(nameof(ArrayEntity.NullableIntArray)); + + private static readonly PropertyInfo IntList + = typeof(ArrayEntity).GetProperty(nameof(ArrayEntity.IntList)); + + private static readonly PropertyInfo NullableIntList + = typeof(ArrayEntity).GetProperty(nameof(ArrayEntity.NullableIntList)); + + private static readonly PropertyInfo StringArray + = typeof(ArrayEntity).GetProperty(nameof(ArrayEntity.StringArray)); + + private static readonly PropertyInfo NullableStringArray + = typeof(ArrayEntity).GetProperty(nameof(ArrayEntity.NullableStringArray)); + + private static readonly PropertyInfo StringList + = typeof(ArrayEntity).GetProperty(nameof(ArrayEntity.StringList)); + + private static readonly PropertyInfo NullableStringList + = typeof(ArrayEntity).GetProperty(nameof(ArrayEntity.NullableStringList)); + + private static readonly PropertyInfo ValueConvertedArray + = typeof(ArrayEntity).GetProperty(nameof(ArrayEntity.ValueConvertedArray)); + + private static readonly PropertyInfo ValueConvertedList + = typeof(ArrayEntity).GetProperty(nameof(ArrayEntity.ValueConvertedList)); + + protected override Expression VisitMember(MemberExpression node) + { + if (node.Member == IntArray) + { + return Expression.MakeMemberAccess(node.Expression, IntList); + } + + if (node.Member == NullableIntArray) + { + return Expression.MakeMemberAccess(node.Expression, NullableIntList); + } + + if (node.Member == StringArray) + { + return Expression.MakeMemberAccess(node.Expression, StringList); + } + + if (node.Member == NullableStringArray) + { + return Expression.MakeMemberAccess(node.Expression, NullableStringList); + } + + if (node.Member == ValueConvertedArray) + { + return Expression.MakeMemberAccess(node.Expression, ValueConvertedList); + } + + return node; + } + + protected override Expression VisitBinary(BinaryExpression node) + { + if (node.NodeType == ExpressionType.ArrayIndex) + { + var listExpression = Visit(node.Left); + if (listExpression.Type.IsGenericList()) + { + var getItemMethod = listExpression.Type.GetMethod("get_Item", new[] { typeof(int) })!; + return Expression.Call(listExpression, getItemMethod, node.Right); + } + } + + return base.VisitBinary(node); + } + } + } +} diff --git a/test/EFCore.PG.FunctionalTests/Query/ArrayQueryFixture.cs b/test/EFCore.PG.FunctionalTests/Query/ArrayQueryFixture.cs new file mode 100644 index 000000000..fae7663b2 --- /dev/null +++ b/test/EFCore.PG.FunctionalTests/Query/ArrayQueryFixture.cs @@ -0,0 +1,88 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Linq; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Diagnostics; +using Microsoft.EntityFrameworkCore.Query; +using Microsoft.EntityFrameworkCore.TestUtilities; +using Npgsql.EntityFrameworkCore.PostgreSQL.TestModels.Array; +using Npgsql.EntityFrameworkCore.PostgreSQL.TestUtilities; +using Xunit; + +namespace Npgsql.EntityFrameworkCore.PostgreSQL.Query +{ + public abstract class ArrayQueryFixture : SharedStoreFixtureBase, IQueryFixtureBase + { + protected override ITestStoreFactory TestStoreFactory => NpgsqlTestStoreFactory.Instance; + public TestSqlLoggerFactory TestSqlLoggerFactory => (TestSqlLoggerFactory)ListLoggerFactory; + + private ArrayQueryData _expectedData; + + public override DbContextOptionsBuilder AddOptions(DbContextOptionsBuilder builder) + => base.AddOptions(builder).ConfigureWarnings(wcb => wcb.Ignore(CoreEventId.CollectionWithoutComparer)); + + protected override void Seed(ArrayQueryContext context) => ArrayQueryContext.Seed(context); + + public Func GetContextCreator() + => CreateContext; + + public ISetSource GetExpectedData() + => _expectedData ??= new ArrayQueryData(); + + public IReadOnlyDictionary GetEntitySorters() + => new Dictionary> { + { typeof(ArrayEntity), e => ((ArrayEntity)e)?.Id }, + { typeof(ArrayContainerEntity), e => ((ArrayContainerEntity)e)?.Id } + }.ToDictionary(e => e.Key, e => (object)e.Value); + + public IReadOnlyDictionary GetEntityAsserters() + => new Dictionary> + { + { + typeof(ArrayEntity), (e, a) => + { + Assert.Equal(e is null, a is null); + if (a is not null) + { + var ee = (ArrayEntity)e; + var aa = (ArrayEntity)a; + + Assert.Equal(ee.Id, aa.Id); + Assert.Equal(ee.IntArray, ee.IntArray); + Assert.Equal(ee.IntList, ee.IntList); + Assert.Equal(ee.NullableIntArray, ee.NullableIntArray); + Assert.Equal(ee.Bytea, ee.Bytea); + Assert.Equal(ee.ByteArray, ee.ByteArray); + Assert.Equal(ee.StringArray, ee.StringArray); + Assert.Equal(ee.StringList, ee.StringList); + Assert.Equal(ee.NullableStringArray, ee.NullableStringArray); + Assert.Equal(ee.NullableStringList, ee.NullableStringList); + Assert.Equal(ee.NullableText, ee.NullableText); + Assert.Equal(ee.NonNullableText, ee.NonNullableText); + Assert.Equal(ee.ValueConvertedScalar, ee.ValueConvertedScalar); + Assert.Equal(ee.ValueConvertedArray, ee.ValueConvertedArray); + Assert.Equal(ee.ValueConvertedList, ee.ValueConvertedList); + Assert.Equal(ee.Byte, ee.Byte); + } + } + }, + { + typeof(ArrayContainerEntity), (e, a) => + { + Assert.Equal(e is null, a is null); + if (a is not null) + { + var ee = (ArrayContainerEntity)e; + var aa = (ArrayContainerEntity)a; + + Assert.Equal(ee.Id, aa.Id); + Assert.Equal(ee.ArrayEntities, ee.ArrayEntities); + } + } + } + }.ToDictionary(e => e.Key, e => (object)e.Value); + } +} diff --git a/test/EFCore.PG.FunctionalTests/Query/ArrayQueryTest.cs b/test/EFCore.PG.FunctionalTests/Query/ArrayQueryTest.cs index 9c0225779..3ea8acfe9 100644 --- a/test/EFCore.PG.FunctionalTests/Query/ArrayQueryTest.cs +++ b/test/EFCore.PG.FunctionalTests/Query/ArrayQueryTest.cs @@ -1,36 +1,30 @@ -using System; -using System.Collections.Generic; +using System.Collections.Generic; using System.Linq; -using System.Linq.Expressions; -using System.Reflection; using System.Threading.Tasks; using Microsoft.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore.Diagnostics; -using Microsoft.EntityFrameworkCore.Query.Internal; -using Microsoft.EntityFrameworkCore.TestUtilities; -using Microsoft.EntityFrameworkCore.Utilities; -using Npgsql.EntityFrameworkCore.PostgreSQL.Query.ArrayTests; -using Npgsql.EntityFrameworkCore.PostgreSQL.TestUtilities; +using Microsoft.EntityFrameworkCore.Query; +using Npgsql.EntityFrameworkCore.PostgreSQL.TestModels.Array; using Xunit; using Xunit.Abstractions; +// ReSharper disable ConvertToConstant.Local + namespace Npgsql.EntityFrameworkCore.PostgreSQL.Query { - public class ArrayQueryTest : IClassFixture + public abstract class ArrayQueryTest : QueryTestBase + where TFixture : ArrayQueryFixture, new() { - private ArrayArrayQueryFixture Fixture { get; } - // ReSharper disable once UnusedParameter.Local - public ArrayQueryTest(ArrayArrayQueryFixture fixture, ITestOutputHelper testOutputHelper) + public ArrayQueryTest(TFixture fixture, ITestOutputHelper testOutputHelper) + : base(fixture) { - Fixture = fixture; Fixture.TestSqlLoggerFactory.Clear(); - // Fixture.TestSqlLoggerFactory.SetTestOutputHelper(testOutputHelper); + Fixture.TestSqlLoggerFactory.SetTestOutputHelper(testOutputHelper); } #region Roundtrip - [Fact] + [ConditionalFact] public void Roundtrip() { using var ctx = CreateContext(); @@ -38,189 +32,121 @@ public void Roundtrip() Assert.Equal(new[] { 3, 4 }, x.IntArray); Assert.Equal(new List { 3, 4 }, x.IntList); - Assert.Equal(new int?[] { 3, 4, null}, x.NullableIntArray); - Assert.Equal(new List { 3, 4, null}, x.NullableIntList); + Assert.Equal(new int?[] { 3, 4, null }, x.NullableIntArray); + Assert.Equal( + new List + { + 3, + 4, + null + }, x.NullableIntList); } #endregion #region Indexers - [Fact] - public void Array_index_with_constant() - { - using var ctx = CreateContext(); - var id = ctx.SomeEntities - .Where(e => e.IntArray[0] == 3) - .Select(e => e.Id) - .Single(); - - Assert.Equal(1, id); - AssertSql( - @"SELECT s.""Id"" -FROM ""SomeEntities"" AS s -WHERE s.""IntArray""[1] = 3 -LIMIT 2"); - } - - [Fact] - public void List_index_with_constant() - { - using var ctx = CreateContext(); - var id = ctx.SomeEntities - .Where(e => e.IntList[0] == 3) - .Select(e => e.Id) - .Single(); - - Assert.Equal(1, id); - AssertSql( - @"SELECT s.""Id"" -FROM ""SomeEntities"" AS s -WHERE s.""IntList""[1] = 3 -LIMIT 2"); - } - - [Fact] - public void Nullable_array_index_with_constant() - { - using var ctx = CreateContext(); - var id = ctx.SomeEntities - .Where(e => e.NullableIntArray[0] == 3) - .Select(e => e.Id) - .Single(); - - Assert.Equal(1, id); - AssertSql( - @"SELECT s.""Id"" -FROM ""SomeEntities"" AS s -WHERE s.""NullableIntArray""[1] = 3 -LIMIT 2"); - } - - [Fact] - public void Nullable_list_index_with_constant() - { - using var ctx = CreateContext(); - var id = ctx.SomeEntities - .Where(e => e.NullableIntList[0] == 3) - .Select(e => e.Id) - .Single(); - - Assert.Equal(1, id); - AssertSql( - @"SELECT s.""Id"" -FROM ""SomeEntities"" AS s -WHERE s.""NullableIntList""[1] = 3 -LIMIT 2"); - } + [Theory] + [MemberData(nameof(IsAsyncData))] + public virtual Task Index_with_constant(bool async) + => AssertQuery( + async, + ss => ss.Set().Where(e => e.IntArray[0] == 3), + entryCount: 1); - [Fact] - public void Index_with_non_constant() + [Theory] + [MemberData(nameof(IsAsyncData))] + public virtual Task Index_with_parameter(bool async) { - using var ctx = CreateContext(); // ReSharper disable once ConvertToConstant.Local var x = 0; - var id = ctx.SomeEntities - .Where(e => e.IntArray[x] == 3) - .Select(e => e.Id) - .Single(); - - Assert.Equal(1, id); - AssertSql( - @"@__x_0='0' - -SELECT s.""Id"" -FROM ""SomeEntities"" AS s -WHERE s.""IntArray""[@__x_0 + 1] = 3 -LIMIT 2"); - } - [Fact] - public void List_index_with_non_constant() - { - using var ctx = CreateContext(); - // ReSharper disable once ConvertToConstant.Local - var x = 0; - var id = ctx.SomeEntities - .Where(e => e.IntList[x] == 3) - .Select(e => e.Id) - .Single(); - - Assert.Equal(1, id); - AssertSql( - @"@__x_0='0' - -SELECT s.""Id"" -FROM ""SomeEntities"" AS s -WHERE s.""IntList""[@__x_0 + 1] = 3 -LIMIT 2"); + return AssertQuery( + async, + ss => ss.Set().Where(e => e.IntArray[x] == 3), + entryCount: 1); } - #endregion + [Theory] + [MemberData(nameof(IsAsyncData))] + public virtual Task Nullable_index_with_constant(bool async) + => AssertQuery( + async, + ss => ss.Set().Where(e => e.NullableIntArray[0] == 3), + entryCount: 1); + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual Task Nullable_value_array_index_compare_to_null(bool async) + => AssertQuery( + async, + ss => ss.Set() + .Where(e => e.NullableIntArray[2] == null), + entryCount: 1); + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual Task Non_nullable_value_array_index_compare_to_null(bool async) + => AssertQuery( + async, + ss => ss.Set() +#pragma warning disable CS0472 + .Where(e => e.IntArray[1] == null)); +#pragma warning restore CS0472 + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual Task Nullable_reference_array_index_compare_to_null(bool async) + => AssertQuery( + async, + ss => ss.Set() + .Where(e => e.NullableStringArray[2] == null), + entryCount: 1); + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual Task Non_nullable_reference_array_index_compare_to_null(bool async) + => AssertQuery( + async, + ss => ss.Set() +#pragma warning disable CS0472 + .Where(e => e.StringArray[1] == null)); +#pragma warning restore CS0472 + + #endregion Indexers #region SequenceEqual - [Theory] - [MemberData(nameof(IsListData))] - public void SequenceEqual_with_parameter(bool list) + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual async Task SequenceEqual_with_parameter(bool async) { - using var ctx = CreateContext(); var arr = new[] { 3, 4 }; - var id = ctx.SomeEntities - .Where(e => e.IntArray.SequenceEqual(arr)) - .Select(e => e.Id) - .OverArrayOrList(list) - .Single(); - - Assert.Equal(1, id); - AssertSql(list, - @"@__arr_0={ '3', '4' } (DbType = Object) - -SELECT s.""Id"" -FROM ""SomeEntities"" AS s -WHERE s.""IntArray"" = @__arr_0 -LIMIT 2"); - } - [Theory] - [MemberData(nameof(IsListData))] - public void SequenceEqual_with_array_literal(bool list) - { - using var ctx = CreateContext(); - var id = ctx.SomeEntities - .Where(e => e.IntArray.SequenceEqual(new[] { 3, 4 })) - .Select(e => e.Id) - .OverArrayOrList(list) - .Single(); - - Assert.Equal(1, id); - AssertSql(list, - @"SELECT s.""Id"" -FROM ""SomeEntities"" AS s -WHERE s.""IntArray"" = ARRAY[3,4]::integer[] -LIMIT 2"); + await AssertQuery( + async, + ss => ss.Set().Where(e => e.IntArray.SequenceEqual(arr)), + entryCount: 1); } - [Theory] - [MemberData(nameof(IsListData))] - public void SequenceEqual_over_nullable_with_parameter(bool list) + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual async Task SequenceEqual_with_array_literal(bool async) + => await AssertQuery( + async, + ss => ss.Set().Where(e => e.IntArray.SequenceEqual(new[] { 3, 4 })), + entryCount: 1); + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual async Task SequenceEqual_over_nullable_with_parameter(bool async) { - using var ctx = CreateContext(); var arr = new int?[] { 3, 4, null }; - var id = ctx.SomeEntities - .Where(e => e.NullableIntArray.SequenceEqual(arr)) - .Select(e => e.Id) - .OverArrayOrList(list) - .Single(); - - Assert.Equal(1, id); - AssertSql(list, - @"@__arr_0={ '3', '4', NULL } (DbType = Object) - -SELECT s.""Id"" -FROM ""SomeEntities"" AS s -WHERE s.""NullableIntArray"" = @__arr_0 -LIMIT 2"); + + await AssertQuery( + async, + ss => ss.Set().Where(e => e.NullableIntArray.SequenceEqual(arr)), + entryCount: 1); } #endregion @@ -229,1213 +155,273 @@ SELECT s.""Id"" // See also tests in NorthwindMiscellaneousQueryNpgsqlTest - [Theory] - [MemberData(nameof(IsListData))] - public void Array_column_Any_equality_operator(bool list) - { - using var ctx = CreateContext(); - var id = ctx.SomeEntities - .Where(e => e.StringArray.Any(p => p == "3")) - .OverArrayOrList(list) - .Select(e => e.Id) - .Single(); - - Assert.Equal(1, id); - - AssertSql(list, - @"SELECT s.""Id"" -FROM ""SomeEntities"" AS s -WHERE s.""StringArray"" @> ARRAY['3']::text[] -LIMIT 2"); - } - - [Theory] - [MemberData(nameof(IsListData))] - public void Array_column_Any_Equals(bool list) - { - using var ctx = CreateContext(); - var id = ctx.SomeEntities - .Where(e => e.StringArray.Any(p => "3".Equals(p))) - .OverArrayOrList(list) - .Select(e => e.Id) - .Single(); - - Assert.Equal(1, id); - - AssertSql(list, - @"SELECT s.""Id"" -FROM ""SomeEntities"" AS s -WHERE s.""StringArray"" @> ARRAY['3']::text[] -LIMIT 2"); - } - - [Theory] - [MemberData(nameof(IsListData))] - public void Array_column_Contains_literal_item(bool list) - { - using var ctx = CreateContext(); - var id = ctx.SomeEntities - .Where(e => e.IntArray.Contains(3)) - .Select(e => e.Id) - .OverArrayOrList(list) - .Single(); - - Assert.Equal(1, id); - AssertSql(list, - @"SELECT s.""Id"" -FROM ""SomeEntities"" AS s -WHERE s.""IntArray"" @> ARRAY[3]::integer[] -LIMIT 2"); - } - - [Theory] - [MemberData(nameof(IsListData))] - public void Array_column_Contains_parameter_item(bool list) + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual async Task Array_column_Any_equality_operator(bool async) + => await AssertQuery( + async, + ss => ss.Set().Where(e => e.StringArray.Any(p => p == "3")), + entryCount: 1); + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual async Task Array_column_Any_Equals(bool async) + => await AssertQuery( + async, + ss => ss.Set().Where(e => e.StringArray.Any(p => "3".Equals(p))), + entryCount: 1); + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual async Task Array_column_Contains_literal_item(bool async) + => await AssertQuery( + async, + ss => ss.Set().Where(e => e.IntArray.Contains(3)), + entryCount: 1); + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual async Task Array_column_Contains_parameter_item(bool async) { - using var ctx = CreateContext(); - // ReSharper disable once ConvertToConstant.Local var p = 3; - var id = ctx.SomeEntities - .Where(e => e.IntArray.Contains(p)) - .Select(e => e.Id) - .OverArrayOrList(list) - .Single(); - - Assert.Equal(1, id); - AssertSql(list, - @"@__p_0='3' - -SELECT s.""Id"" -FROM ""SomeEntities"" AS s -WHERE s.""IntArray"" @> ARRAY[@__p_0]::integer[] -LIMIT 2"); - } - - [Theory] - [MemberData(nameof(IsListData))] - public void Array_column_Contains_column_item(bool list) - { - using var ctx = CreateContext(); - var id = ctx.SomeEntities - .Where(e => e.IntArray.Contains(e.Id + 2)) - .Select(e => e.Id) - .OverArrayOrList(list) - .Single(); - - Assert.Equal(1, id); - AssertSql(list, - @"SELECT s.""Id"" -FROM ""SomeEntities"" AS s -WHERE s.""IntArray"" @> ARRAY[s.""Id"" + 2]::integer[] -LIMIT 2"); - } - - [Theory] - [MemberData(nameof(IsListData))] - public void Array_column_Contains_null_constant(bool list) - { - using var ctx = CreateContext(); - var id = ctx.SomeEntities - .Where(e => e.NullableStringArray.Contains(null)) - .Select(e => e.Id) - .OverArrayOrList(list) - .Single(); - - Assert.Equal(1, id); - AssertSql(list, - @"SELECT s.""Id"" -FROM ""SomeEntities"" AS s -WHERE (array_position(s.""NullableStringArray"", NULL) IS NOT NULL) -LIMIT 2"); - } - - [Theory] - [MemberData(nameof(IsListData))] - public void Array_column_Contains_null_parameter_does_not_work(bool list) - { - using var ctx = CreateContext(); - string p = null; - var results = ctx.SomeEntities - .Where(e => e.StringArray.Contains(p)) - .Select(e => e.Id) - .OverArrayOrList(list) - .ToList(); - - // We incorrectly miss arrays containing non-constant nulls, because detecting those - // would prevent index use. - Assert.Empty(results); - AssertSql(list, - @"SELECT s.""Id"" -FROM ""SomeEntities"" AS s -WHERE s.""StringArray"" @> ARRAY[NULL]::text[]"); - } - - [Theory] - [MemberData(nameof(IsListData))] - public void Nullable_array_column_Contains_literal_item(bool list) - { - using var ctx = CreateContext(); - var id = ctx.SomeEntities - .Where(e => e.NullableIntArray.Contains(3)) - .Select(e => e.Id) - .OverArrayOrList(list) - .Single(); - - Assert.Equal(1, id); - AssertSql(list, - @"SELECT s.""Id"" -FROM ""SomeEntities"" AS s -WHERE s.""NullableIntArray"" @> ARRAY[3]::integer[] -LIMIT 2"); - } - - [Fact] - public void Array_constant_Contains() - { - using var ctx = CreateContext(); - var id = ctx.SomeEntities - .Where(e => new[] { "foo", "xxx" }.Contains(e.NullableText)) - .Select(e => e.Id) - .Single(); - - Assert.Equal(1, id); - AssertSql( - @"SELECT s.""Id"" -FROM ""SomeEntities"" AS s -WHERE s.""NullableText"" IN ('foo', 'xxx') -LIMIT 2"); - } - - [Fact] - public void Array_param_Contains_nullable_column() - { - using var ctx = CreateContext(); - var array = new[] { "foo", "xxx" }; - var id = ctx.SomeEntities - .Where(e => array.Contains(e.NullableText)) - .Select(e => e.Id) - .Single(); - - Assert.Equal(1, id); - AssertSql( - @"@__array_0={ 'foo', 'xxx' } (DbType = Object) - -SELECT s.""Id"" -FROM ""SomeEntities"" AS s -WHERE s.""NullableText"" = ANY (@__array_0) OR ((s.""NullableText"" IS NULL) AND (array_position(@__array_0, NULL) IS NOT NULL)) -LIMIT 2"); - } - - [Fact] - public void Array_param_Contains_non_nullable_column() - { - using var ctx = CreateContext(); - var array = new[] { 1 }; - var id = ctx.SomeEntities - .Where(e => array.Contains(e.Id)) - .Select(e => e.Id) - .Single(); - - Assert.Equal(1, id); - AssertSql( - @"@__array_0={ '1' } (DbType = Object) - -SELECT s.""Id"" -FROM ""SomeEntities"" AS s -WHERE s.""Id"" = ANY (@__array_0) -LIMIT 2"); - } - - [Fact] - public void Array_param_with_null_Contains_non_nullable_not_found() - { - using var ctx = CreateContext(); - var array = new[] { "unknown1", "unknown2", null }; - var count = ctx.SomeEntities.Count(e => array.Contains(e.NonNullableText)); - - Assert.Equal(0, count); - AssertSql( - @"@__array_0={ 'unknown1', 'unknown2', NULL } (DbType = Object) -SELECT COUNT(*)::INT -FROM ""SomeEntities"" AS s -WHERE s.""NonNullableText"" = ANY (@__array_0)"); + await AssertQuery( + async, + ss => ss.Set().Where(e => e.IntArray.Contains(p)), + entryCount: 1); } - [Fact] - public void Array_param_with_null_Contains_non_nullable_not_found_negated() - { - using var ctx = CreateContext(); - var array = new[] { "unknown1", "unknown2", null }; - var count = ctx.SomeEntities.Count(e => !array.Contains(e.NonNullableText)); - - Assert.Equal(2, count); + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual async Task Array_column_Contains_column_item(bool async) + => await AssertQuery( + async, + ss => ss.Set().Where(e => e.IntArray.Contains(e.Id + 2)), + entryCount: 1); - AssertSql( - @"@__array_0={ 'unknown1', 'unknown2', NULL } (DbType = Object) - -SELECT COUNT(*)::INT -FROM ""SomeEntities"" AS s -WHERE NOT (s.""NonNullableText"" = ANY (@__array_0) AND (s.""NonNullableText"" = ANY (@__array_0) IS NOT NULL))"); - } + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual async Task Array_column_Contains_null_constant(bool async) + => await AssertQuery( + async, + ss => ss.Set().Where(e => e.NullableStringArray.Contains(null)), + entryCount: 1); - [Fact] - public void Array_param_with_null_Contains_nullable_not_found() - { - using var ctx = CreateContext(); - var array = new[] { "unknown1", "unknown2", null }; - var count = ctx.SomeEntities.Count(e => array.Contains(e.NullableText)); + [ConditionalFact] + public abstract void Array_column_Contains_null_parameter_does_not_work(); - Assert.Equal(0, count); - AssertSql( - @"@__array_0={ 'unknown1', 'unknown2', NULL } (DbType = Object) + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual async Task Nullable_array_column_Contains_literal_item(bool async) + => await AssertQuery( + async, + ss => ss.Set().Where(e => e.NullableIntArray.Contains(3)), + entryCount: 1); -SELECT COUNT(*)::INT -FROM ""SomeEntities"" AS s -WHERE s.""NullableText"" = ANY (@__array_0) OR ((s.""NullableText"" IS NULL) AND (array_position(@__array_0, NULL) IS NOT NULL))"); - } + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public abstract Task Array_constant_Contains_column(bool async); - [Fact] - public void Array_param_with_null_Contains_nullable_not_found_negated() - { - using var ctx = CreateContext(); - var array = new[] { "unknown1", "unknown2", null }; - var count = ctx.SomeEntities.Count(e => !array.Contains(e.NullableText)); + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public abstract Task Array_param_Contains_nullable_column(bool async); - Assert.Equal(2, count); - AssertSql( - @"@__array_0={ 'unknown1', 'unknown2', NULL } (DbType = Object) + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public abstract Task Array_param_Contains_non_nullable_column(bool async); -SELECT COUNT(*)::INT -FROM ""SomeEntities"" AS s -WHERE NOT (s.""NullableText"" = ANY (@__array_0) AND (s.""NullableText"" = ANY (@__array_0) IS NOT NULL)) AND ((s.""NullableText"" IS NOT NULL) OR (array_position(@__array_0, NULL) IS NULL))"); - } + [ConditionalFact] + public abstract void Array_param_with_null_Contains_non_nullable_not_found(); - [Fact] - public void List_param_Contains_non_nullable_column() - { - using var ctx = CreateContext(); - var list = new List { 1 }; - var id = ctx.SomeEntities - .Where(e => list.Contains(e.Id)) - .Select(e => e.Id) - .Single(); - - Assert.Equal(1, id); - AssertSql( - @"@__list_0={ '1' } (DbType = Object) - -SELECT s.""Id"" -FROM ""SomeEntities"" AS s -WHERE s.""Id"" = ANY (@__list_0) -LIMIT 2"); - } + [ConditionalFact] + public abstract void Array_param_with_null_Contains_non_nullable_not_found_negated(); - [Fact] - public void Byte_array_parameter_contains_column() - { - using var ctx = CreateContext(); - var values = new byte[] { 20 }; - var id = ctx.SomeEntities - .Where(e => values.Contains(e.Byte)) - .Select(e => e.Id) - .Single(); - - Assert.Equal(2, id); - // Note: EF Core prints the parameter as a bytea, but it's actually a smallint[] (otherwise ANY would fail) - AssertSql( - @"@__values_0='0x14' (DbType = Object) - -SELECT s.""Id"" -FROM ""SomeEntities"" AS s -WHERE s.""Byte"" = ANY (@__values_0) -LIMIT 2"); - } + [ConditionalFact] + public abstract void Array_param_with_null_Contains_nullable_not_found(); - [Fact] - public void Array_param_Contains_value_converted_column() - { - using var ctx = CreateContext(); - var array = new[] { SomeEnum.Two, SomeEnum.Three }; - var id = ctx.SomeEntities - .Where(e => array.Contains(e.ValueConvertedScalar)) - .Select(e => e.Id) - .Single(); - - Assert.Equal(2, id); - AssertSql( - @"@__array_0={ '-2', '-3' } (DbType = Object) - -SELECT s.""Id"" -FROM ""SomeEntities"" AS s -WHERE s.""ValueConvertedScalar"" = ANY (@__array_0) -LIMIT 2"); - } + [ConditionalFact] + public abstract void Array_param_with_null_Contains_nullable_not_found_negated(); - [Fact] - public void List_param_Contains_value_converted_column() - { - using var ctx = CreateContext(); - var list = new List { SomeEnum.Two, SomeEnum.Three }; - var id = ctx.SomeEntities - .Where(e => list.Contains(e.ValueConvertedScalar)) - .Select(e => e.Id) - .Single(); - - Assert.Equal(2, id); - AssertSql( - @"@__list_0={ '-2', '-3' } (DbType = Object) - -SELECT s.""Id"" -FROM ""SomeEntities"" AS s -WHERE s.""ValueConvertedScalar"" = ANY (@__list_0) -LIMIT 2"); - } + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public abstract Task Byte_array_parameter_contains_column(bool async); - [Fact] - public void Array_column_Contains_value_converted_param() - { - using var ctx = CreateContext(); - var item = SomeEnum.Eight; - var id = ctx.SomeEntities - .Where(e => e.ValueConvertedArray.Contains(item)) - .Select(e => e.Id) - .Single(); - - Assert.Equal(1, id); - AssertSql( - @"@__item_0='-8' - -SELECT s.""Id"" -FROM ""SomeEntities"" AS s -WHERE s.""ValueConvertedArray"" @> ARRAY[@__item_0]::integer[] -LIMIT 2"); - } + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public abstract Task Array_param_Contains_value_converted_column(bool async); - [Fact] - public void List_column_Contains_value_converted_param() + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual async Task Array_column_Contains_value_converted_param(bool async) { - using var ctx = CreateContext(); var item = SomeEnum.Eight; - var id = ctx.SomeEntities - .Where(e => e.ValueConvertedList.Contains(item)) - .Select(e => e.Id) - .Single(); - - Assert.Equal(1, id); - AssertSql( - @"@__item_0='-8' - -SELECT s.""Id"" -FROM ""SomeEntities"" AS s -WHERE s.""ValueConvertedList"" @> ARRAY[@__item_0]::integer[] -LIMIT 2"); - } - [Fact] - public void Array_param_Contains_value_converted_array_column() - { - using var ctx = CreateContext(); - var p = new[] { SomeEnum.Eight, SomeEnum.Nine }; - var id = ctx.SomeEntities - .Where(e => e.ValueConvertedArray.All(x => p.Contains(x))) - .Select(e => e.Id) - .Single(); - - Assert.Equal(1, id); - AssertSql( - @"@__p_0={ '-8', '-9' } (DbType = Object) - -SELECT s.""Id"" -FROM ""SomeEntities"" AS s -WHERE s.""ValueConvertedArray"" <@ @__p_0 -LIMIT 2"); + await AssertQuery( + async, + ss => ss.Set().Where(e => e.ValueConvertedArray.Contains(item)), + entryCount: 1); } - [Fact] - public void List_param_Contains_value_converted_list_column() - { - using var ctx = CreateContext(); - var p = new List { SomeEnum.Eight, SomeEnum.Nine }; - var id = ctx.SomeEntities - .Where(e => e.ValueConvertedList.All(x => p.Contains(x))) - .Select(e => e.Id) - .Single(); - - Assert.Equal(1, id); - AssertSql( - @"@__p_0={ '-8', '-9' } (DbType = Object) - -SELECT s.""Id"" -FROM ""SomeEntities"" AS s -WHERE s.""ValueConvertedList"" <@ @__p_0 -LIMIT 2"); - } + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public abstract Task Array_param_Contains_value_converted_array_column(bool async); - [Theory] - [MemberData(nameof(IsListData))] - public void Array_column_Contains_in_scalar_subquery(bool list) - { - using var ctx = CreateContext(); - var id = ctx.SomeEntityContainers - .Where(c => c.ArrayEntities.OrderBy(e => e.Id).First().NullableIntArray.Contains(3)) - .Select(e => e.Id) - .OverArrayOrList(list) - .Single(); - - Assert.Equal(1, id); - AssertSql(list, - @"SELECT s.""Id"" -FROM ""SomeEntityContainers"" AS s -WHERE 3 = ANY (( - SELECT s0.""NullableIntArray"" - FROM ""SomeEntities"" AS s0 - WHERE s.""Id"" = s0.""SomeContainerEntityId"" - ORDER BY s0.""Id"" NULLS FIRST - LIMIT 1)::integer[]) -LIMIT 2"); - } + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual async Task Array_column_Contains_in_scalar_subquery(bool async) + => await AssertQuery( + async, + ss => ss.Set().Where(c => c.ArrayEntities.OrderBy(e => e.Id).First().NullableIntArray.Contains(3)), + entryCount: 1); #endregion #region Length/Count - [Fact] - public void ArrayLength() - { - using var ctx = CreateContext(); - var id = ctx.SomeEntities - .Where(e => e.IntArray.Length == 2) - .Select(e => e.Id) - .Single(); - - Assert.Equal(1, id); - AssertSql( - @"SELECT s.""Id"" -FROM ""SomeEntities"" AS s -WHERE cardinality(s.""IntArray"") = 2 -LIMIT 2"); - } - - [Fact] - public void NullableArrayLength() - { - using var ctx = CreateContext(); - var id = ctx.SomeEntities - .Where(e => e.NullableIntArray.Length == 3) - .Select(e => e.Id) - .Single(); - - Assert.Equal(1, id); - AssertSql( - @"SELECT s.""Id"" -FROM ""SomeEntities"" AS s -WHERE cardinality(s.""NullableIntArray"") = 3 -LIMIT 2"); - } - - [Fact] - public void ListCount() - { - using var ctx = CreateContext(); - var id = ctx.SomeEntities - .Where(e => e.IntList.Count == 2) - .Select(e => e.Id) - .Single(); - - Assert.Equal(1, id); - AssertSql( - @"SELECT s.""Id"" -FROM ""SomeEntities"" AS s -WHERE cardinality(s.""IntList"") = 2 -LIMIT 2"); - } + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public abstract Task Array_Length(bool async); - [Fact] - public void Array_Length_on_EF_Property() - { - using var ctx = CreateContext(); - var id = ctx.SomeEntities - .Where(e => EF.Property(e, nameof(SomeArrayEntity.IntArray)).Length == 2) - .Select(e => e.Id) - .Single(); - - Assert.Equal(1, id); - AssertSql( - @"SELECT s.""Id"" -FROM ""SomeEntities"" AS s -WHERE cardinality(s.""IntArray"") = 2 -LIMIT 2"); - } + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public abstract Task Nullable_array_Length(bool async); - [Fact] - public void Length_on_literal_not_translated() - { - using var ctx = CreateContext(); - var id = ctx.SomeEntities - .Where(e => new[] { 1, 2 }.Length == e.Id) - .Select(e => e.Id) - .Single(); - - Assert.Equal(2, id); - AssertSql( - @"SELECT s.""Id"" -FROM ""SomeEntities"" AS s -WHERE 2 = s.""Id"" -LIMIT 2"); - } + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public abstract Task Array_Length_on_EF_Property(bool async); - #endregion + #endregion Length/Count #region Any/All - [Theory] - [MemberData(nameof(IsListData))] - public void Any_no_predicate(bool list) - { - using var ctx = CreateContext(); - var count = ctx.SomeEntities - .Where(e => e.IntArray.Any()) - .OverArrayOrList(list) - .Count(); - - Assert.Equal(2, count); - AssertSql(list, - @"SELECT COUNT(*)::INT -FROM ""SomeEntities"" AS s -WHERE cardinality(s.""IntArray"") > 0"); - } - - [Fact] - public void Any_like() - { - using var ctx = CreateContext(); - var id = ctx.SomeEntities - .Where(e => new[] { "a%", "b%", "c%" }.Any(p => EF.Functions.Like(e.NullableText, p))) - .Select(e => e.Id) - .Single(); - - Assert.Equal(2, id); - - AssertSql( - @"SELECT s.""Id"" -FROM ""SomeEntities"" AS s -WHERE s.""NullableText"" LIKE ANY (ARRAY['a%','b%','c%']::text[]) -LIMIT 2"); - } - - [Fact] - public void Any_ilike() - { - using var ctx = CreateContext(); - var id = ctx.SomeEntities - .Where(e => new[] { "a%", "b%", "c%" }.Any(p => EF.Functions.ILike(e.NullableText, p))) - .Select(e => e.Id) - .Single(); - - Assert.Equal(2, id); - - AssertSql( - @"SELECT s.""Id"" -FROM ""SomeEntities"" AS s -WHERE s.""NullableText"" ILIKE ANY (ARRAY['a%','b%','c%']::text[]) -LIMIT 2"); - } - - [Fact] - public void Any_like_anonymous() - { - using var ctx = CreateContext(); - var patterns = new[] { "a%", "b%", "c%" }; - - var _ = ctx.SomeEntities - .Select( - x => new - { - Array = x.IntArray, - Text = x.NullableText - }) - .Where(x => patterns.Any(p => EF.Functions.Like(x.Text, p))) - .ToList(); - - AssertSql( - @"@__patterns_0={ 'a%', 'b%', 'c%' } (DbType = Object) - -SELECT s.""IntArray"" AS ""Array"", s.""NullableText"" AS ""Text"" -FROM ""SomeEntities"" AS s -WHERE s.""NullableText"" LIKE ANY (@__patterns_0)"); - } - - [Fact] - public void All_like() - { - using var ctx = CreateContext(); - var id = ctx.SomeEntities - .Where(e => new[] { "b%", "%r" }.All(p => EF.Functions.Like(e.NullableText, p))) - .Select(e => e.Id) - .Single(); - - Assert.Equal(2, id); - - AssertSql( - @"SELECT s.""Id"" -FROM ""SomeEntities"" AS s -WHERE s.""NullableText"" LIKE ALL (ARRAY['b%','%r']::text[]) -LIMIT 2"); - } - - [Fact] - public void All_ilike() - { - using var ctx = CreateContext(); - var id = ctx.SomeEntities - .Where(e => new[] { "B%", "%r" }.All(p => EF.Functions.ILike(e.NullableText, p))) - .Select(e => e.Id) - .Single(); - - Assert.Equal(2, id); - - AssertSql( - @"SELECT s.""Id"" -FROM ""SomeEntities"" AS s -WHERE s.""NullableText"" ILIKE ALL (ARRAY['B%','%r']::text[]) -LIMIT 2"); - } - - [Theory] - [MemberData(nameof(IsListData))] - public void Any_Contains_on_constant_array(bool list) - { - using var ctx = CreateContext(); - - var id = ctx.SomeEntities - .Where(e => new[] { 2, 3 }.Any(p => e.IntArray.Contains(p))) - .Select(e => e.Id) - .OverArrayOrList(list) - .Single(); - Assert.Equal(1, id); - - var count = ctx.SomeEntities - .Where(e => new[] { 1, 2 }.Any(p => e.IntArray.Contains(p))) - .OverArrayOrList(list) - .Count(); - Assert.Equal(0, count); - - AssertSql(list, - @"SELECT s.""Id"" -FROM ""SomeEntities"" AS s -WHERE ARRAY[2,3]::integer[] && s.""IntArray"" -LIMIT 2", - // - @"SELECT COUNT(*)::INT -FROM ""SomeEntities"" AS s -WHERE ARRAY[1,2]::integer[] && s.""IntArray"""); - } - - [Theory] - [MemberData(nameof(IsListData))] - public void Any_Contains_between_column_and_List(bool list) + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual async Task Any_no_predicate(bool async) + => await AssertQuery( + async, + ss => ss.Set().Where(e => e.IntArray.Any()), + entryCount: 2); + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public abstract Task Any_like(bool async); + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public abstract Task Any_ilike(bool async); + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public abstract Task Any_like_anonymous(bool async); + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public abstract Task All_like(bool async); + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public abstract Task All_ilike(bool async); + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual async Task Any_Contains_on_constant_array(bool async) + => await AssertQuery( + async, + ss => ss.Set().Where(e => new[] { 2, 3 }.Any(p => e.IntArray.Contains(p))), + entryCount: 1); + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual async Task Any_Contains_between_column_and_List(bool async) { - using var ctx = CreateContext(); - var ints = new List { 2, 3 }; - var id = ctx.SomeEntities - .Where(e => e.IntArray.Any(i => ints.Contains(i))) - .Select(e => e.Id) - .OverArrayOrList(list) - .Single(); - Assert.Equal(1, id); - - ints = new List { 1, 2 }; - var count = ctx.SomeEntities - .Where(e => e.IntArray.Any(i => ints.Contains(i))) - .OverArrayOrList(list) - .Count(); - Assert.Equal(0, count); - - AssertSql(list, - @"@__ints_0={ '2', '3' } (DbType = Object) - -SELECT s.""Id"" -FROM ""SomeEntities"" AS s -WHERE s.""IntArray"" && @__ints_0 -LIMIT 2", - // - @"@__ints_0={ '1', '2' } (DbType = Object) - -SELECT COUNT(*)::INT -FROM ""SomeEntities"" AS s -WHERE s.""IntArray"" && @__ints_0"); - } - - [Theory] - [MemberData(nameof(IsListData))] - public void Any_Contains_between_column_and_array(bool list) - { - using var ctx = CreateContext(); - - var ints = new[] { 2, 3 }; - var id = ctx.SomeEntities - .Where(e => e.IntArray.Any(i => ints.Contains(i))) - .Select(e => e.Id) - .OverArrayOrList(list) - .Single(); - Assert.Equal(1, id); - - ints = new[] { 1, 2 }; - var count = ctx.SomeEntities - .Where(e => e.IntArray.Any(i => ints.Contains(i))) - .OverArrayOrList(list) - .Count(); - Assert.Equal(0, count); - - AssertSql(list, - @"@__ints_0={ '2', '3' } (DbType = Object) - -SELECT s.""Id"" -FROM ""SomeEntities"" AS s -WHERE s.""IntArray"" && @__ints_0 -LIMIT 2", - // - @"@__ints_0={ '1', '2' } (DbType = Object) - -SELECT COUNT(*)::INT -FROM ""SomeEntities"" AS s -WHERE s.""IntArray"" && @__ints_0"); - } - - [Theory] - [MemberData(nameof(IsListData))] - public void All_Contains(bool list) - { - using var ctx = CreateContext(); - - var id = ctx.SomeEntities - .Where(e => new[] { 5, 6 }.All(p => e.IntArray.Contains(p))) - .Select(e => e.Id) - .OverArrayOrList(list) - .Single(); - Assert.Equal(2, id); - - var count = ctx.SomeEntities - .Where(e => new[] { 4, 5, 6 }.All(p => e.IntArray.Contains(p))) - .OverArrayOrList(list) - .Count(); - Assert.Equal(0, count); - - AssertSql(list, - @"SELECT s.""Id"" -FROM ""SomeEntities"" AS s -WHERE ARRAY[5,6]::integer[] <@ s.""IntArray"" -LIMIT 2", - // - @"SELECT COUNT(*)::INT -FROM ""SomeEntities"" AS s -WHERE ARRAY[4,5,6]::integer[] <@ s.""IntArray"""); - } - [Theory] - [MemberData(nameof(IsListData))] - public Task Any_like_column(bool list) - { - using var ctx = CreateContext(); - - return AssertTranslationFailed(() => ctx.SomeEntities - .Where(e => e.StringArray.Any(p => EF.Functions.Like(p, "3"))) - .OverArrayOrList(list) - .ToListAsync()); + await AssertQuery( + async, + ss => ss.Set().Where(e => e.IntArray.Any(i => ints.Contains(i))), + entryCount: 1); } - #endregion - - #region bytea - - [Fact] - public void Index_bytea_with_constant() + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual async Task Any_Contains_between_column_and_array(bool async) { - using var ctx = CreateContext(); - var id = ctx.SomeEntities - .Where(e => e.Bytea[0] == 3) - .Select(e => e.Id) - .Single(); - - Assert.Equal(1, id); - AssertSql( - @"SELECT s.""Id"" -FROM ""SomeEntities"" AS s -WHERE get_byte(s.""Bytea"", 0) = 3 -LIMIT 2"); - } - - public void Index_text_with_constant() - { - using var ctx = CreateContext(); - var actual = ctx.SomeEntities.Where(e => e.NullableText[0] == 'f').ToList(); + var ints = new[] { 2, 3 }; - Assert.Single(actual); - AssertSql( - @"SELECT s.""Id"", s.""SomeArray"", s.""SomeBytea"", s.""SomeMatrix"", s.""SomeText"" -FROM ""SomeEntities"" AS s -WHERE (get_byte(s.""SomeBytea"", 0) = 3) AND get_byte(s.""SomeBytea"", 0) IS NOT NULL"); + await AssertQuery( + async, + ss => ss.Set().Where(e => e.IntArray.Any(i => ints.Contains(i))), + entryCount: 1); } - #endregion - - #region Nullability + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual async Task All_Contains(bool async) + => await AssertQuery( + async, + ss => ss.Set().Where(e => new[] { 5, 6 }.All(p => e.IntArray.Contains(p))), + entryCount: 1); [ConditionalFact] - public void Nullable_value_array_index_compare_to_null() + public virtual async Task Any_like_column() { using var ctx = CreateContext(); - var id = ctx.SomeEntities - .Where(e => e.NullableIntArray[3] == null) - .Select(e => e.Id) - .Count(); - - Assert.Equal(1, id); - AssertSql( - @"SELECT COUNT(*)::INT -FROM ""SomeEntities"" AS s -WHERE (s.""NullableIntArray""[4] IS NULL)"); - } - [ConditionalFact] - public void Non_nullable_value_array_index_compare_to_null() - { - using var ctx = CreateContext(); - var count = ctx.SomeEntities -#pragma warning disable CS0472 - // ReSharper disable once ConditionIsAlwaysTrueOrFalse - .Where(e => e.IntArray[1] == null) -#pragma warning restore CS0472 - .Select(e => e.Id) - .Count(); - - Assert.Equal(0, count); - AssertSql( - @"SELECT COUNT(*)::INT -FROM ""SomeEntities"" AS s -WHERE FALSE"); + await AssertTranslationFailed( + () => ctx.SomeEntities + .Where(e => e.StringArray.Any(p => EF.Functions.Like(p, "3"))) + .ToListAsync()); } - [ConditionalFact] - public void Nullable_reference_array_index_compare_to_null() - { - using var ctx = CreateContext(); - var id = ctx.SomeEntities - .Where(e => e.NullableStringArray[3] == null) - .Select(e => e.Id) - .Count(); - - Assert.Equal(1, id); - AssertSql( - @"SELECT COUNT(*)::INT -FROM ""SomeEntities"" AS s -WHERE (s.""NullableStringArray""[4] IS NULL)"); - } + #endregion Any/All - [ConditionalFact] - public void Non_nullable_reference_array_index_compare_to_null() - { - using var ctx = CreateContext(); - var count = ctx.SomeEntities -#pragma warning disable CS0472 - // ReSharper disable once ConditionIsAlwaysTrueOrFalse - .Where(e => e.StringArray[1] == null) -#pragma warning restore CS0472 - .Select(e => e.Id) - .Count(); - - Assert.Equal(0, count); - AssertSql( - @"SELECT COUNT(*)::INT -FROM ""SomeEntities"" AS s -WHERE FALSE"); - } + #region Other translations - [ConditionalFact] - public void Nullable_value_list_index_compare_to_null() - { - using var ctx = CreateContext(); - var id = ctx.SomeEntities - .Where(e => e.NullableIntList[3] == null) - .Select(e => e.Id) - .Count(); - - Assert.Equal(1, id); - AssertSql( - @"SELECT COUNT(*)::INT -FROM ""SomeEntities"" AS s -WHERE (s.""NullableIntList""[4] IS NULL)"); - } + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual Task Append(bool async) + => AssertQuery( + async, + ss => ss.Set() + .Where(e => e.IntArray.Append(5).SequenceEqual(new[] { 3, 4, 5 })), + entryCount: 1); - [ConditionalFact] - public void Non_nullable_value_list_index_compare_to_null() - { - using var ctx = CreateContext(); - var count = ctx.SomeEntities -#pragma warning disable CS0472 - // ReSharper disable once ConditionIsAlwaysTrueOrFalse - .Where(e => e.IntList[1] == null) -#pragma warning restore CS0472 - .Select(e => e.Id) - .Count(); - - Assert.Equal(0, count); - AssertSql( - @"SELECT COUNT(*)::INT -FROM ""SomeEntities"" AS s -WHERE FALSE"); - } + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual Task Concat(bool async) + => AssertQuery( + async, + ss => ss.Set() + .Where(e => e.IntArray.Concat(new[] { 5, 6 }).SequenceEqual(new[] { 3, 4, 5, 6 })), + entryCount: 1); - [ConditionalFact] - public void Nullable_reference_list_index_compare_to_null() - { - using var ctx = CreateContext(); - var id = ctx.SomeEntities - .Where(e => e.NullableStringList[3] == null) - .Select(e => e.Id) - .Count(); - - Assert.Equal(1, id); - AssertSql( - @"SELECT COUNT(*)::INT -FROM ""SomeEntities"" AS s -WHERE (s.""NullableStringList""[4] IS NULL)"); - } + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public abstract Task Array_IndexOf1(bool async); - [ConditionalFact] - public void Non_nullable_reference_list_index_compare_to_null() - { - using var ctx = CreateContext(); - var count = ctx.SomeEntities -#pragma warning disable CS0472 - // ReSharper disable once ConditionIsAlwaysTrueOrFalse - .Where(e => e.StringList[1] == null) -#pragma warning restore CS0472 - .Select(e => e.Id) - .Count(); - - Assert.Equal(0, count); - AssertSql( - @"SELECT COUNT(*)::INT -FROM ""SomeEntities"" AS s -WHERE FALSE"); - } + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public abstract Task Array_IndexOf2(bool async); - #endregion Nullability + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual Task String_Join(bool async) + => AssertQuery( + async, + ss => ss.Set() + .Where(e => string.Join(", ", e.IntArray) == "3, 4"), + entryCount: 1); - #region Support + #endregion Other translations - protected ArrayArrayQueryContext CreateContext() => Fixture.CreateContext(); + #region Support - private void AssertSql(bool list, params string[] expected) - => AssertSql(list - ? expected.Select(e => e - .Replace(@"""IntArray""", @"""IntList""") - .Replace(@"""NullableIntArray""", @"""NullableIntList""") - .Replace(@"""StringArray""", @"""StringList""") - .Replace(@"""NullableStringArray""", @"""NullableStringList""")) - .ToArray() - : expected); + protected ArrayQueryContext CreateContext() + => Fixture.CreateContext(); - private void AssertSql(params string[] expected) + protected void AssertSql(params string[] expected) => Fixture.TestSqlLoggerFactory.AssertBaseline(expected); - public static IEnumerable IsListData = new[] { new object[] { false }, new object[] { true } }; - - public class ArrayArrayQueryContext : PoolableDbContext - { - public DbSet SomeEntities { get; set; } - public DbSet SomeEntityContainers { get; set; } - - public ArrayArrayQueryContext(DbContextOptions options) : base(options) {} - - protected override void OnModelCreating(ModelBuilder modelBuilder) - => modelBuilder.Entity( - e => - { - // We do negative to make sure our value converter is properly used, and not the built-in one - e.Property(ae => ae.ValueConvertedScalar) - .HasConversion(w => -(int)w, v => (SomeEnum)(-v)); - - e.Property(ae => ae.ValueConvertedArray) - .HasPostgresArrayConversion(w => -(int)w, v => (SomeEnum)(-v)); - - e.Property(ae => ae.ValueConvertedList) - .HasPostgresArrayConversion(w => -(int)w, v => (SomeEnum)(-v)); - }); - - public static void Seed(ArrayArrayQueryContext context) - { - var arrayEntities = new SomeArrayEntity[] - { - new() - { - Id = 1, - IntArray = new[] { 3, 4 }, - IntList = new List { 3, 4 }, - NullableIntArray = new int?[] { 3, 4, null }, - NullableIntList = new List { 3, 4, null }, - Bytea = new byte[] { 3, 4 }, - ByteArray = new byte[] { 3, 4 }, - StringArray = new[] { "3", "4" }, - NullableStringArray = new[] { "3", "4", null }, - StringList = new List { "3", "4" }, - NullableStringList = new List { "3", "4", null}, - NullableText = "foo", - NonNullableText = "foo", - ValueConvertedScalar = SomeEnum.One, - ValueConvertedArray = new[] { SomeEnum.Eight, SomeEnum.Nine }, - ValueConvertedList = new List { SomeEnum.Eight, SomeEnum.Nine }, - Byte = 10 - }, - new() - { - Id = 2, - IntArray = new[] { 5, 6, 7, 8 }, - IntList = new List { 5, 6, 7, 8 }, - NullableIntArray = new int?[] { 5, 6, 7, 8 }, - NullableIntList = new List { 5, 6, 7, 8 }, - Bytea = new byte[] { 5, 6, 7, 8 }, - ByteArray = new byte[] { 5, 6, 7, 8 }, - StringArray = new[] { "5", "6", "7", "8" }, - NullableStringArray = new[] { "5", "6", "7", "8" }, - StringList = new List { "5", "6", "7", "8" }, - NullableStringList = new List { "5", "6", "7", "8" }, - NullableText = "bar", - NonNullableText = "bar", - ValueConvertedScalar = SomeEnum.Two, - ValueConvertedArray = new[] { SomeEnum.Nine, SomeEnum.Ten }, - ValueConvertedList = new List { SomeEnum.Nine, SomeEnum.Ten }, - Byte = 20 - } - }; - - context.SomeEntities.AddRange(arrayEntities); - context.SomeEntityContainers.Add(new SomeContainerEntity - { - Id = 1, - ArrayEntities = new List { arrayEntities[0], arrayEntities[1] } - }); - context.SaveChanges(); - } - } - - #nullable enable - - public class SomeArrayEntity - { - public int Id { get; set; } - public int[] IntArray { get; set; } = null!; - public List IntList { get; set; } = null!; - public int?[] NullableIntArray { get; set; } = null!; - public List NullableIntList { get; set; } = null!; - public byte[] Bytea { get; set; } = null!; - public byte[] ByteArray { get; set; } = null!; - public string[] StringArray { get; set; } = null!; - public List StringList { get; set; } = null!; - public string?[] NullableStringArray { get; set; } = null!; - public List NullableStringList { get; set; } = null!; - public string? NullableText { get; set; } - public string NonNullableText { get; set; } = null!; - public SomeEnum ValueConvertedScalar { get; set; } - public SomeEnum[] ValueConvertedArray { get; set; } = null!; - public List ValueConvertedList { get; set; } = null!; - public byte Byte { get; set; } - } - - public enum SomeEnum - { - One = 1, - Two = 2, - Three = 3, - Eight = 8, - Nine = 9, - Ten = 10 - } - - public class SomeContainerEntity - { - public int Id { get; set; } - public List ArrayEntities { get; set; } = null!; - } - - #nullable restore - - public class ArrayArrayQueryFixture : SharedStoreFixtureBase - { - protected override string StoreName => "ArrayArrayQueryTest"; - protected override ITestStoreFactory TestStoreFactory => NpgsqlTestStoreFactory.Instance; - public TestSqlLoggerFactory TestSqlLoggerFactory => (TestSqlLoggerFactory)ListLoggerFactory; - - public override DbContextOptionsBuilder AddOptions(DbContextOptionsBuilder builder) - => base.AddOptions(builder).ConfigureWarnings(wcb => wcb.Ignore(CoreEventId.CollectionWithoutComparer)); - - protected override void Seed(ArrayArrayQueryContext context) => ArrayArrayQueryContext.Seed(context); - } - - protected static async Task AssertTranslationFailed(Func query) - => Assert.Contains( - CoreStrings.TranslationFailed("").Substring(48), - (await Assert.ThrowsAsync(query)) - .Message); - #endregion } } - -namespace Npgsql.EntityFrameworkCore.PostgreSQL.Query.ArrayTests -{ - using SomeArrayEntity = ArrayQueryTest.SomeArrayEntity; - - internal static class QueryableExtensions - { - internal static IQueryable OverArrayOrList(this IQueryable source, bool list) - { - Check.NotNull(source, nameof(source)); - - return source.Provider is EntityQueryProvider && list - ? source.Provider.CreateQuery( - new ArrayToListReplacingExpressionVisitor().Visit(source.Expression)) - : source; - } - } - - internal class ArrayToListReplacingExpressionVisitor : ExpressionVisitor - { - private static readonly PropertyInfo IntArray = typeof(SomeArrayEntity).GetProperty(nameof(SomeArrayEntity.IntArray)); - private static readonly PropertyInfo NullableIntArray = typeof(SomeArrayEntity).GetProperty(nameof(SomeArrayEntity.NullableIntArray)); - private static readonly PropertyInfo IntList = typeof(SomeArrayEntity).GetProperty(nameof(SomeArrayEntity.IntList)); - private static readonly PropertyInfo NullableIntList = typeof(SomeArrayEntity).GetProperty(nameof(SomeArrayEntity.NullableIntList)); - private static readonly PropertyInfo StringArray = typeof(SomeArrayEntity).GetProperty(nameof(SomeArrayEntity.StringArray)); - private static readonly PropertyInfo NullableStringArray = typeof(SomeArrayEntity).GetProperty(nameof(SomeArrayEntity.NullableStringArray)); - private static readonly PropertyInfo StringList = typeof(SomeArrayEntity).GetProperty(nameof(SomeArrayEntity.StringList)); - private static readonly PropertyInfo NullableStringList = typeof(SomeArrayEntity).GetProperty(nameof(SomeArrayEntity.NullableStringList)); - - protected override Expression VisitMember(MemberExpression node) - { - if (node.Member == IntArray) - { - return Expression.MakeMemberAccess(node.Expression, IntList); - } - - if (node.Member == NullableIntArray) - { - return Expression.MakeMemberAccess(node.Expression, NullableIntList); - } - - if (node.Member == StringArray) - { - return Expression.MakeMemberAccess(node.Expression, StringList); - } - - if (node.Member == NullableStringArray) - { - return Expression.MakeMemberAccess(node.Expression, NullableStringList); - } - - return node; - } - } -} diff --git a/test/EFCore.PG.FunctionalTests/TestModels/Array/ArrayContainerEntity.cs b/test/EFCore.PG.FunctionalTests/TestModels/Array/ArrayContainerEntity.cs new file mode 100644 index 000000000..a205ca3dc --- /dev/null +++ b/test/EFCore.PG.FunctionalTests/TestModels/Array/ArrayContainerEntity.cs @@ -0,0 +1,12 @@ +using System.Collections.Generic; + +#nullable enable + +namespace Npgsql.EntityFrameworkCore.PostgreSQL.TestModels.Array +{ + public class ArrayContainerEntity + { + public int Id { get; set; } + public List ArrayEntities { get; set; } = null!; + } +} diff --git a/test/EFCore.PG.FunctionalTests/TestModels/Array/ArrayEntity.cs b/test/EFCore.PG.FunctionalTests/TestModels/Array/ArrayEntity.cs new file mode 100644 index 000000000..9a8811751 --- /dev/null +++ b/test/EFCore.PG.FunctionalTests/TestModels/Array/ArrayEntity.cs @@ -0,0 +1,27 @@ +using System.Collections.Generic; + +#nullable enable + +namespace Npgsql.EntityFrameworkCore.PostgreSQL.TestModels.Array +{ + public class ArrayEntity + { + public int Id { get; set; } + public int[] IntArray { get; set; } = null!; + public List IntList { get; set; } = null!; + public int?[] NullableIntArray { get; set; } = null!; + public List NullableIntList { get; set; } = null!; + public byte[] Bytea { get; set; } = null!; + public byte[] ByteArray { get; set; } = null!; + public string[] StringArray { get; set; } = null!; + public List StringList { get; set; } = null!; + public string?[] NullableStringArray { get; set; } = null!; + public List NullableStringList { get; set; } = null!; + public string? NullableText { get; set; } + public string NonNullableText { get; set; } = null!; + public SomeEnum ValueConvertedScalar { get; set; } + public SomeEnum[] ValueConvertedArray { get; set; } = null!; + public List ValueConvertedList { get; set; } = null!; + public byte Byte { get; set; } + } +} diff --git a/test/EFCore.PG.FunctionalTests/TestModels/Array/ArrayQueryContext.cs b/test/EFCore.PG.FunctionalTests/TestModels/Array/ArrayQueryContext.cs new file mode 100644 index 000000000..24271c4ae --- /dev/null +++ b/test/EFCore.PG.FunctionalTests/TestModels/Array/ArrayQueryContext.cs @@ -0,0 +1,44 @@ +using System.Linq; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.TestUtilities; + +namespace Npgsql.EntityFrameworkCore.PostgreSQL.TestModels.Array +{ + public class ArrayQueryContext : PoolableDbContext + { + public DbSet SomeEntities { get; set; } + public DbSet SomeEntityContainers { get; set; } + + public ArrayQueryContext(DbContextOptions options) : base(options) {} + + protected override void OnModelCreating(ModelBuilder modelBuilder) + => modelBuilder.Entity( + e => + { + // We do negative to make sure our value converter is properly used, and not the built-in one + e.Property(ae => ae.ValueConvertedScalar) + .HasConversion(w => -(int)w, v => (SomeEnum)(-v)); + + e.Property(ae => ae.ValueConvertedArray) + .HasPostgresArrayConversion(w => -(int)w, v => (SomeEnum)(-v)); + + e.Property(ae => ae.ValueConvertedList) + .HasPostgresArrayConversion(w => -(int)w, v => (SomeEnum)(-v)); + }); + + public static void Seed(ArrayQueryContext context) + { + var arrayEntities = ArrayQueryData.CreateArrayEntities(); + + context.SomeEntities.AddRange(arrayEntities); + context.SomeEntityContainers.Add( + new ArrayContainerEntity + { + Id = 1, + ArrayEntities = arrayEntities.ToList() + } + ); + context.SaveChanges(); + } + } +} diff --git a/test/EFCore.PG.FunctionalTests/TestModels/Array/ArrayQueryData.cs b/test/EFCore.PG.FunctionalTests/TestModels/Array/ArrayQueryData.cs new file mode 100644 index 000000000..f0be8ddff --- /dev/null +++ b/test/EFCore.PG.FunctionalTests/TestModels/Array/ArrayQueryData.cs @@ -0,0 +1,87 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Microsoft.EntityFrameworkCore.TestUtilities; + +namespace Npgsql.EntityFrameworkCore.PostgreSQL.TestModels.Array +{ + public class ArrayQueryData : ISetSource + { + public IReadOnlyList ArrayEntities { get; } + public IReadOnlyList ContainerEntities { get; } + + public ArrayQueryData() + => (ArrayEntities, ContainerEntities) = (CreateArrayEntities(), CreateContainerEntities()); + + public IQueryable Set() + where TEntity : class + { + if (typeof(TEntity) == typeof(ArrayEntity)) + { + return (IQueryable)ArrayEntities.AsQueryable(); + } + + if (typeof(TEntity) == typeof(ArrayContainerEntity)) + { + return (IQueryable)ContainerEntities.AsQueryable(); + } + + throw new InvalidOperationException("Invalid entity type: " + typeof(TEntity)); + } + + public static IReadOnlyList CreateArrayEntities() + => new ArrayEntity[] + { + new() + { + Id = 1, + IntArray = new[] { 3, 4 }, + IntList = new List { 3, 4 }, + NullableIntArray = new int?[] { 3, 4, null }, + NullableIntList = new List { 3, 4, null }, + Bytea = new byte[] { 3, 4 }, + ByteArray = new byte[] { 3, 4 }, + StringArray = new[] { "3", "4" }, + NullableStringArray = new[] { "3", "4", null }, + StringList = new List { "3", "4" }, + NullableStringList = new List { "3", "4", null}, + NullableText = "foo", + NonNullableText = "foo", + ValueConvertedScalar = SomeEnum.One, + ValueConvertedArray = new[] { SomeEnum.Eight, SomeEnum.Nine }, + ValueConvertedList = new List { SomeEnum.Eight, SomeEnum.Nine }, + Byte = 10 + }, + new() + { + Id = 2, + IntArray = new[] { 5, 6, 7, 8 }, + IntList = new List { 5, 6, 7, 8 }, + NullableIntArray = new int?[] { 5, 6, 7, 8 }, + NullableIntList = new List { 5, 6, 7, 8 }, + Bytea = new byte[] { 5, 6, 7, 8 }, + ByteArray = new byte[] { 5, 6, 7, 8 }, + StringArray = new[] { "5", "6", "7", "8" }, + NullableStringArray = new[] { "5", "6", "7", "8" }, + StringList = new List { "5", "6", "7", "8" }, + NullableStringList = new List { "5", "6", "7", "8" }, + NullableText = "bar", + NonNullableText = "bar", + ValueConvertedScalar = SomeEnum.Two, + ValueConvertedArray = new[] { SomeEnum.Nine, SomeEnum.Ten }, + ValueConvertedList = new List { SomeEnum.Nine, SomeEnum.Ten }, + Byte = 20 + } + }; + + public static IReadOnlyList CreateContainerEntities() + => new[] + { + new ArrayContainerEntity + { + Id = 1, + ArrayEntities = CreateArrayEntities().ToList() + } + }; + } +} diff --git a/test/EFCore.PG.FunctionalTests/TestModels/Array/SomeEnum.cs b/test/EFCore.PG.FunctionalTests/TestModels/Array/SomeEnum.cs new file mode 100644 index 000000000..a1cdc2e3e --- /dev/null +++ b/test/EFCore.PG.FunctionalTests/TestModels/Array/SomeEnum.cs @@ -0,0 +1,12 @@ +namespace Npgsql.EntityFrameworkCore.PostgreSQL.TestModels.Array +{ + public enum SomeEnum + { + One = 1, + Two = 2, + Three = 3, + Eight = 8, + Nine = 9, + Ten = 10 + } +}