Skip to content

fix: reject a string set compared with a value under a collation that may not compare bytes (#507) - #513

Merged
CaffeinatedCoder merged 4 commits into
mainfrom
fix/507-element-side-collation
Oct 26, 2026
Merged

CaffeinatedCoder merged 4 commits into
mainfrom
fix/507-element-side-collation

Conversation

@CaffeinatedCoder

Copy link
Copy Markdown
Owner

Closes #507

Stacked on #509 (both edit the same migration list and changelog section). Review only the last three commits.

Problem

PostgreSQL takes an operator's collation from both sides. #483 checks only the string set's own collation: the property when the model is built, and EF.Functions.Collate over the set. Suppose the set column has no collation, which is the fix #483's error message suggests, and Owner has a case-insensitive one. With Members holding {A,b} and Owner holding "a":

  • Members.Contains(g.Owner) matched the row (@> compared under the value's collation). Memory gives false.
  • ExecuteUpdate(… Members.Remove(g.Owner)) stored {b}. Memory keeps {A,b}.
  • Members.Contains(EF.Functions.Collate("a", "case_insensitive")) matched too.

Change

  • ValueSetSqlGuard walker (Compared, Collation, ColumnCollation): at @>, <@, &&, array_remove, == and the text set placeholders that involve a string set, it derives the operator's collation the way PostgreSQL does:

    • an explicit COLLATE wins;
    • otherwise the column's, or a subquery or set-operation column's projection;
    • or what a string-typed function, operator or array takes from its string-typed inputs;
    • a CASE contributes its results, not its tests.

    If that collation may not compare bytes, the query throws and names it. EF.Functions.Collate over the set itself keeps Text value set operators follow a nondeterministic column collation: on a case-insensitive column, Contains("a") is true for {A} and Remove("a") deletes A #483's message.

  • StringSetCollationConvention: the read-optimized model a query runs on keeps no collation, so property.GetCollation() throws there. The convention that already visits every property now names each other non-byte collation in an annotation (CodoMetis:ValueRanges:Collation), and the guard reads it. A compiled model carries the annotation (tested).

  • Remedy: EF.Functions.Collate(g.Owner, "C"). Measured on PostgreSQL 16 over an en_US database: an explicit "C" on the value turns an indexed @> into a sequential scan unless the set column is "C" too. The migration section says so.

  • Docs: the value-set entry in efcore.md, the v10.x migration list and Check string set columns with a collation, ### Breaking lines in 11.0.0-preview.1 (root and EF Core), and claim E89.

  • Oracles: StringSetCollationTests is registered as an exception oracle, with two seeds.

It changes answers, so it's labelled silent. On 10.x: #512.

Tests

  • StringSetCollationIntegrationTests (live): Contains, !Contains, Remove, Add().Count, ToLower(), EF.Functions.Collate, a subquery's column and ExecuteUpdate with Remove each throw and name the collation, and the row stays unchanged. A "C" column, Collate(Owner, "C"), and a CASE that only tests Owner run and answer as memory does. The rejection also holds on a compiled model, generated as dotnet ef dbcontext optimize does. CompiledModelJsonTests.CompileModel now takes any context.
  • StringSetCollationTests: adds a derived table's column, a set operation's column, a concatenation and a CASE result.
  • Proved by reverting: without the walker's throw, all three new tests fail, because each query runs instead of throwing. oracles.cs seeds string-set-collation catches both seeds: the throw removed, and the convention leaving the column unnamed.
  • Full suite green on PostgreSQL 16, and the collation, Union and compiled-model classes on 18.

🤖 Generated with Claude Code

CaffeinatedCoder and others added 3 commits October 26, 2026 18:10
… may not compare bytes (#507)

PostgreSQL derives an operator's collation from both sides, and #483 only
checks the set's: a value from a case-insensitive column, or collated by
EF.Functions.Collate, made @>, && and array_remove compare
case-insensitively with the set column uncollated, as the remedy leaves
it. Members.Contains(g.Owner) matched {A,b} for "a", and ExecuteUpdate
with Members.Remove(g.Owner) stored {b}, where memory keeps {A,b}.

The guard's walker now derives the collation of a string set operator's
operands as PostgreSQL does (an explicit COLLATE wins; a column's;
a subquery or set-operation column's projection; what a string-typed
function, operator or array takes from its string inputs; a CASE's
results, not its tests) at @>, <@, &&, array_remove, == and the text set
placeholders, and throws for one that may not compare bytes, naming it.
EF.Functions.Collate(value, "C") compares as memory does. The read-optimized
model a query runs on keeps no collation, so the convention that rejects a
collated string set column names every other non-byte collation on its
property (CodoMetis:ValueRanges:Collation), which a compiled model carries.

StringSetCollationIntegrationTests rejects Contains, !Contains, Remove,
Add().Count, ToLower(), EF.Functions.Collate, a subquery's column and
ExecuteUpdate with Remove on a live server and leaves the row unchanged,
runs "C" and a CASE that only tests the column as memory answers, and
holds the rejection on a compiled model; StringSetCollationTests adds a
derived table's and a set operation's column, a concatenation and a CASE
result. Without the walker check every rejection runs and answers.

Found-By: CA
Defect-Class: SQL

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ation check (#507)

Two seeds, both caught: the guard's element check removed, and the
convention leaving a collated column unnamed, which the read-optimized
model would then hide from the guard.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…e migration and changelog (#507)

The value set entry in efcore.md gains the value's side and the
EF.Functions.Collate(value, "C") remedy; the v10.x migration list and
Check string set columns with a collation gain it, with its before and
after table and the GIN index cost of an explicit "C" (measured on
PostgreSQL 16 over an en_US database: a sequential scan); the Breaking
lines of 11.0.0-preview.1 in the root and EF Core changelogs; claim E89.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@CaffeinatedCoder CaffeinatedCoder added this to the 10.4.0 milestone Oct 26, 2026
@CaffeinatedCoder CaffeinatedCoder added bug Something isn't working silent Answers wrongly without throwing — fixes go in the next major, never deferred area/value-sets Value set families and wrapper arities package/efcore Affects CodoMetis.ValueRanges.EFCore.PostgreSQL severity/medium Inconsistency or failure in a narrow scenario stacked Base is another open PR; merge in order, CI does not run until it targets main labels Oct 26, 2026
Base automatically changed from fix/506-yearmonth-upper-bound to main October 26, 2026 17:28
@CaffeinatedCoder
CaffeinatedCoder merged commit dbea14b into main Oct 26, 2026
14 checks passed
@CaffeinatedCoder
CaffeinatedCoder deleted the fix/507-element-side-collation branch October 26, 2026 17:47
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area/value-sets Value set families and wrapper arities bug Something isn't working package/efcore Affects CodoMetis.ValueRanges.EFCore.PostgreSQL severity/medium Inconsistency or failure in a narrow scenario silent Answers wrongly without throwing — fixes go in the next major, never deferred stacked Base is another open PR; merge in order, CI does not run until it targets main

Projects

None yet

1 participant