Skip to content

Typed expression indexes render Substring and Trim differently from EF Core's queries, so no query uses them #72

Closed

Description

@CaffeinatedCoder

Problem

PostgreSQL uses an expression index only for a query whose expression matches it. Typed expression indexes (HasExpressionIndex(x => …)) render Substring and Trim differently from Npgsql's query translation of the same lambda, so the index applies and no EF query uses it. The same class of defect 5.4.0 fixed for JSON members.

Measured against ToQueryString() on Npgsql 10.0:

Lambda Package renders EF Core renders
x.Code.Substring(1, 3) substr("Code", 2, 3) substring("Code", 2, 3)
x.Code.Substring(1) substr("Code", 2) substring("Code", 2)
x.Code.Trim() btrim("Code") btrim("Code", E' \t\n\r')
x.Code.TrimStart() ltrim("Code") ltrim("Code", E' \t\n\r')
x.Code.TrimEnd() rtrim("Code") rtrim("Code", E' \t\n\r')

substr and substring are different functions to the planner, and btrim(x) trims spaces only. Evidence from the PostgreSQL analysis (PG 18, enable_seqscan=off): WHERE substring("Code",2,3)='abc' is a Seq Scan against an index on substr(...); a btrim(x, E' \t\n\r') query is a Seq Scan against an index on btrim(x). And a unique index on btrim("Code") accepts both 'Y\t' and 'Y', which Trim() treats as equal: the uniqueness the model declares is not what the database enforces.

ToLower, ToUpper, Replace, ??, Length (EF's ::int on length() is a no-op cast) and string concatenation of non-nullable operands already match.

Fix

Render substring(…) and btrim/ltrim/rtrim(…, E' \t\n\r'), as Npgsql's query translation does, and pin each against ToQueryString() the way NpgsqlJsonMemberRenderingTests does for JSON members.

Changed answer

Typed expression indexes over Substring, Trim, TrimStart or TrimEnd re-create on the first migrations add after upgrading (the stored template changes). A unique trimmed index may then refuse rows that differ only in tabs or line breaks at the ends, which are the duplicates it was declared to refuse. Ships in a minor with a ### Changed answers row.

Found by the 5.5.0 pre-tag analysis. Present since typed expression indexes shipped.

Activity

  1. added this to the 5.5.0 milestone on Oct 19, 2026
  2. added
    bugSomething isn't working
    silentAnswers wrongly without throwing — fixes go in the next major, never deferred
    severity/mediumInconsistency or failure in a narrow scenario
    package/postgresqlAffects EFCore.ComplexIndexes.PostgreSQL
    area/expression-indexesExpression, typed LINQ and JSON member indexes
    on Oct 19, 2026
  3. added a commit that references this issue on Oct 20, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    area/expression-indexesExpression, typed LINQ and JSON member indexesbugSomething isn't workingpackage/postgresqlAffects EFCore.ComplexIndexes.PostgreSQLseverity/mediumInconsistency or failure in a narrow scenariosilentAnswers wrongly without throwing — fixes go in the next major, never deferred

    Projects

    No projects

      Milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions