Skip to content

Text set ==, != and Count through a scalar subquery skip #485's rewrite and compare the unsorted array_cat #505

Closed

Description

@CaffeinatedCoder

Found by a pre-release review of release/10.x @ a958bef (an outside review, and the sweep's long-method lens on ValueSetSqlGuard); reproduced against PostgreSQL 16 in a scratch test. Also on main @ 2949415.

Problem

ValueSetSqlGuard's two predicates for "may be an unsorted text set" disagree on a scalar subquery:

Nothing throws. Rows are dropped or counted wrong.

Scenario

A row has Tags = {a,c}, and other = {a,b}. In memory, {a,c}.Union({a,b}) == {a,b,c} is true.

context.Reservations.Where(r => context.Reservations
    .Where(s => s.Id == r.Id).Select(s => s.Tags.Union(other)).FirstOrDefault() == expected)
WHERE (SELECT array_cat(r0."Tags", @other) FROM "Reservations" AS r0 … LIMIT 1) = @expected

On the server this compares {a,c,a,b} with {a,b,c}, so the row is excluded.

Measured forms (PostgreSQL 16; the first live, the other two as translation tests):

  • ==: no rows, where memory returns the row.
  • !=: raw <>.
  • (subquery ?? b.Tags).Count: cardinality(COALESCE((SELECT array_cat(…) …), b."Tags")), which counts the repeated element: {a} ∪ {a,b} gives 3 where memory gives 2.

.FirstOrDefault().Count with no COALESCE is correct, because EF moves Count inside the subquery, where the rewrite applies.

Traced, not run: NULLIF (ValueSetSqlGuard.cs:506), and CASE over a scalar subquery.

Change

Make the two predicates agree. Two ways:

  • Give IsDerived a ScalarSubqueryExpression arm: the subquery's single projection is a placeholder, is derived, or is a derived column.
  • Or have the walker record the derived scalar-subquery nodes for the expander, as it already does with DerivedColumns.

A convention or sweep that holds Walker.Derived and IsDerived arm for arm would catch the next divergence. The walker's NULLIF arm also requires IsSet(kept), which IsDerived's does not.

Test

In UnionCanonicalityTests (integration), with its existing rows: ==, != and Count over COALESCE through a scalar subquery, each asserting the server answer equals LINQ over the loaded rows. Revert the arm to prove each test fails (verify-the-guard), and add a seed under test/oracles/seeds/.

Release

silent, so it goes under ### Changed answers in 10.4.0 on release/10.x (#485's text forms first ship there), main first, then a port.

Activity

  1. added this to the 10.4.0 milestone on Oct 26, 2026
  2. added
    bugSomething isn't working
    silentAnswers wrongly without throwing — fixes go in the next major, never deferred
    area/value-setsValue set families and wrapper arities
    package/efcoreAffects CodoMetis.ValueRanges.EFCore.PostgreSQL
    on Oct 26, 2026
  3. CaffeinatedCoder commented on Oct 26, 2026

    @CaffeinatedCoder
    OwnerAuthor

    Found a third time by the sweep's differential lens, which adds two facts:

    Still to cover in the regression test: !=, and Intersect, Except and Add as well as Union, over StringSet<TElement> too.

  4. added
    breakingChanges an answer or a signature — needs a major
    and removed
    breakingChanges an answer or a signature — needs a major
    on Oct 26, 2026
  5. CaffeinatedCoder commented on Oct 26, 2026

    @CaffeinatedCoder
    OwnerAuthor

    Fix on main: #508. Port to release/10.x: #510 → PR #514.

    The port carries no silent label and no ### Changed answers row. The wrong answer shipped in no release: 10.3.0 threw for this query, and #497's 10.4.0 row already promises == and Count.

  6. added a commit that references this issue on Oct 26, 2026
  7. added a commit that references this issue on Oct 26, 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/value-setsValue set families and wrapper aritiesbugSomething isn't workingpackage/efcoreAffects CodoMetis.ValueRanges.EFCore.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