From 4c5ed7d55db2723658577b8964757ac9b187a938 Mon Sep 17 00:00:00 2001 From: Brian Pursley Date: Fri, 6 Feb 2026 11:15:51 -0500 Subject: [PATCH 1/2] Fix a bug where the result of a function that returns an array is not able to be accessed by array index. --- .../Internal/NpgsqlArrayMethodTranslator.cs | 9 ++++++++ .../Query/Internal/NpgsqlQuerySqlGenerator.cs | 1 + .../NorthwindDbFunctionsQueryNpgsqlTest.cs | 22 +++++++++++++++++++ 3 files changed, 32 insertions(+) diff --git a/src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlArrayMethodTranslator.cs b/src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlArrayMethodTranslator.cs index 847da4c17..04414311e 100644 --- a/src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlArrayMethodTranslator.cs +++ b/src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlArrayMethodTranslator.cs @@ -90,6 +90,15 @@ public NpgsqlArrayMethodTranslator(NpgsqlSqlExpressionFactory sqlExpressionFacto typeof(byte)); } + if (arguments[0].TypeMapping is NpgsqlArrayTypeMapping) + { + return _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 diff --git a/src/EFCore.PG/Query/Internal/NpgsqlQuerySqlGenerator.cs b/src/EFCore.PG/Query/Internal/NpgsqlQuerySqlGenerator.cs index 6dc0c2c2c..372c5c62d 100644 --- a/src/EFCore.PG/Query/Internal/NpgsqlQuerySqlGenerator.cs +++ b/src/EFCore.PG/Query/Internal/NpgsqlQuerySqlGenerator.cs @@ -1485,6 +1485,7 @@ protected override bool RequiresParentheses(SqlExpression outerExpression, SqlEx return true; } + 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() { From cf6bebd647779a9b5319cd1d290c5707daa1ae0d Mon Sep 17 00:00:00 2001 From: Shay Rojansky Date: Sat, 7 Feb 2026 01:29:23 +0100 Subject: [PATCH 2/2] Tweaks --- .../Internal/NpgsqlArrayMethodTranslator.cs | 44 +++++++++---------- .../Query/Internal/NpgsqlQuerySqlGenerator.cs | 2 + 2 files changed, 23 insertions(+), 23 deletions(-) diff --git a/src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlArrayMethodTranslator.cs b/src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlArrayMethodTranslator.cs index 04414311e..f4c56ba38 100644 --- a/src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlArrayMethodTranslator.cs +++ b/src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlArrayMethodTranslator.cs @@ -79,30 +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)); - } - - if (arguments[0].TypeMapping is NpgsqlArrayTypeMapping) - { - return _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 - 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 372c5c62d..8b9f0587b 100644 --- a/src/EFCore.PG/Query/Internal/NpgsqlQuerySqlGenerator.cs +++ b/src/EFCore.PG/Query/Internal/NpgsqlQuerySqlGenerator.cs @@ -1485,6 +1485,8 @@ 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;