diff --git a/src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlArrayMethodTranslator.cs b/src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlArrayMethodTranslator.cs index 847da4c17..f4c56ba38 100644 --- a/src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlArrayMethodTranslator.cs +++ b/src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlArrayMethodTranslator.cs @@ -79,21 +79,28 @@ public NpgsqlArrayMethodTranslator(NpgsqlSqlExpressionFactory sqlExpressionFacto // During preprocessing, ArrayIndex and List[] get normalized to ElementAt; so we handle indexing into array/list here if (method.IsClosedFormOf(Enumerable_ElementAt)) { - // Indexing over bytea is special, we have to use function rather than subscript - if (arguments[0].TypeMapping is NpgsqlByteArrayTypeMapping) + return arguments[0].TypeMapping switch { - return _sqlExpressionFactory.Function( - "get_byte", - [arguments[0], arguments[1]], - nullable: true, - argumentsPropagateNullability: TrueArrays[2], - typeof(byte)); - } - - // Try translating indexing inside JSON column - // Note that Length over PG arrays (not within JSON) gets translated by QueryableMethodTranslatingEV, since arrays are primitive - // collections - return _jsonPocoTranslator.TranslateMemberAccess(arguments[0], arguments[1], method.ReturnType); + // Indexing over bytea is special, we have to use function rather than subscript + NpgsqlByteArrayTypeMapping + => _sqlExpressionFactory.Function( + "get_byte", + [arguments[0], arguments[1]], + nullable: true, + argumentsPropagateNullability: TrueArrays[2], + typeof(byte)), + + NpgsqlArrayTypeMapping typeMapping + => _sqlExpressionFactory.ArrayIndex( + arguments[0], + _sqlExpressionFactory.GenerateOneBasedIndexExpression(arguments[1]), + nullable: true), + + // Try translating indexing inside JSON column + // Note that Length over PG arrays (not within JSON) gets translated by QueryableMethodTranslatingEV, since arrays are primitive + // collections + _ => _jsonPocoTranslator.TranslateMemberAccess(arguments[0], arguments[1], method.ReturnType) + }; } if (method.IsClosedFormOf(Enumerable_SequenceEqual) diff --git a/src/EFCore.PG/Query/Internal/NpgsqlQuerySqlGenerator.cs b/src/EFCore.PG/Query/Internal/NpgsqlQuerySqlGenerator.cs index 6dc0c2c2c..8b9f0587b 100644 --- a/src/EFCore.PG/Query/Internal/NpgsqlQuerySqlGenerator.cs +++ b/src/EFCore.PG/Query/Internal/NpgsqlQuerySqlGenerator.cs @@ -1485,6 +1485,9 @@ protected override bool RequiresParentheses(SqlExpression outerExpression, SqlEx return true; } + // PG requires function calls to be wrapped in parentheses before indexing on the returned array: + // (string_to_array(c."ContactName", ' '))[1] + case SqlFunctionExpression when outerExpression is PgArrayIndexExpression: case PgUnknownBinaryExpression: return true; diff --git a/test/EFCore.PG.FunctionalTests/Query/NorthwindDbFunctionsQueryNpgsqlTest.cs b/test/EFCore.PG.FunctionalTests/Query/NorthwindDbFunctionsQueryNpgsqlTest.cs index b6116abf2..64df0f9ec 100644 --- a/test/EFCore.PG.FunctionalTests/Query/NorthwindDbFunctionsQueryNpgsqlTest.cs +++ b/test/EFCore.PG.FunctionalTests/Query/NorthwindDbFunctionsQueryNpgsqlTest.cs @@ -228,6 +228,28 @@ WHERE string_to_array(c."ContactName", ' ', 'Maria') = ARRAY[NULL,'Anders']::tex """); } + [Fact] + public void StringToArray_with_index() + { + using var context = CreateContext(); + var count = context.Customers + .Select(c => EF.Functions.StringToArray(c.ContactName, " ")[0]) + .Distinct() + .Count(c => c == "Maria"); + + Assert.Equal(1, count); + + AssertSql( + """ +SELECT count(*)::int +FROM ( + SELECT DISTINCT (string_to_array(c."ContactName", ' '))[1] AS c + FROM "Customers" AS c + WHERE (string_to_array(c."ContactName", ' '))[1] = 'Maria' +) AS c0 +"""); + } + [Fact] public void ToDate() {