Skip to content

fix: reject a collation on a string set column unless it compares bytes (#483) - #492

Merged
CaffeinatedCoder merged 3 commits into
mainfrom
fix/483-reject-nondeterministic-string-set-collation
Oct 26, 2026
Merged

CaffeinatedCoder merged 3 commits into
mainfrom
fix/483-reject-nondeterministic-string-set-collation

Conversation

@CaffeinatedCoder

Copy link
Copy Markdown
Owner

Closes #483

Problem

PostgreSQL compares the elements of a text[] column with the column's collation, and memory compares them ordinally. Under a nondeterministic (case- or accent-insensitive) collation, A equals a. On a StringSet column with UseCollation("case_insensitive") holding {A}, reproduced with the 10.3.0 packages:

  • Contains("a"), IsSubsetOf({a}), Overlaps({a}) and == {a} all matched, where memory says false;
  • Select(p => p.Tags.Remove("a")) gave {};
  • ExecuteUpdate(… Remove("a")) stored {}, deleting an element memory keeps.

Change

The option decided on #483 (reject at model build), after measuring the other one:

  • Rejected: COLLATE "C" in the operators. The GIN index carries the column's collation, so a collated operand turns every indexed @> into a sequential scan, on an ordinary en_US column too. The plans on PostgreSQL 16 and 18 are on the issue.
  • StringSetCollationConvention (new, IModelFinalizingConvention, added through IConventionSetPlugin, which is additive and replaces no service). Building the model throws for a collation on a StringSet/StringSet<T> column other than C, POSIX, ucs_basic, pg_c_utf8 and default. Those compare bytes; a name doesn't say whether a collation is deterministic. A string set inside a JSON document is skipped, since the document holds its literal. A compiled model is checked when it is generated.
  • ValueSetSqlGuard refuses EF.Functions.Collate over a string set with any other collation, on the final SQL tree, so a predicate, an equality, a projection and an ExecuteUpdate setter are all covered.
  • Docs: a Good to know entry in Store a value set in an array column; Migration from v10.x gets a bullet and the section Check string set columns with a collation; the changelogs (root, EF Core) get a line under ### Breaking in 11.0.0-preview.1. The 10.x port adds ## Changed answers in 10.4.0.
  • Oracles: claim E86.

It changes answers, so labelled silent: on 10.x it ships as the 10.4.0 minor under ### Changed answers.

Tests

  • StringSetCollationTests (no database). A nondeterministic collation is rejected on StringSet and StringSet<T>, with a message naming the property. The five byte-order collations map with COLLATE. A JSON document isn't checked. EF.Functions.Collate is refused in a filter, an equality and a projection, and allowed with "C".
  • StringSetCollationIntegrationTests (new). A column collated "C" answers Contains, Overlaps, Remove and ExecuteUpdate with Remove as memory does.
  • Proved by reverting: without the plugin registration the two model-build tests fail, and without the guard case the Collate test fails.
  • Full suite green: unit, conventions, ASP.NET Core and EF no-database projects, and both integration projects on PostgreSQL 16 and 18.

🤖 Generated with Claude Code

…es (#483)

PostgreSQL compares text[] elements with the column's collation, memory
ordinally. Under a nondeterministic collation A equals a: on a case-insensitive
StringSet column Contains("a") matched {A}, IsSubsetOf, Overlaps and == agreed
with it, and ExecuteUpdate with Remove("a") stored {} where memory keeps {A}.

Translating with COLLATE "C" was measured and rejected: the GIN index carries
the column's collation, so a collated operand turns every indexed @> into a
sequential scan, on an en_US column too (PostgreSQL 16 and 18).

So a model-finalizing convention, added through IConventionSetPlugin, throws
for a collation on a StringSet or StringSet<T> column other than C, POSIX,
ucs_basic, pg_c_utf8 and default, which compare bytes; a name doesn't say
whether a collation is deterministic. ValueSetSqlGuard refuses
EF.Functions.Collate over a string set the same way. A JSON document holds
the set's literal and is not checked.

Found-By: CA
Defect-Class: SQL

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 labels Oct 26, 2026
…migration and changelog (#483)

The model-build rejection and the Collate refusal in the previous commit, in
the EF guide's value set section, the v10.x migration list and its section,
the Breaking lines of 11.0.0-preview.1, claim E86, and the live test that a
column collated "C" answers as memory does.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@CaffeinatedCoder
CaffeinatedCoder merged commit 3776504 into main Oct 26, 2026
14 checks passed
@CaffeinatedCoder
CaffeinatedCoder deleted the fix/483-reject-nondeterministic-string-set-collation branch October 26, 2026 13:31
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

Projects

None yet

1 participant