Repository navigation
fix: reject a string set compared with a value under a collation that may not compare bytes (#507) - #513
Merged
Conversation
… 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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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.Collateover the set. Suppose the set column has no collation, which is the fix #483's error message suggests, andOwnerhas a case-insensitive one. WithMembersholding{A,b}andOwnerholding"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
ValueSetSqlGuardwalker (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:COLLATEwins;CASEcontributes its results, not its tests.If that collation may not compare bytes, the query throws and names it.
EF.Functions.Collateover 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, soproperty.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 anen_USdatabase: 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,
### Breakinglines in 11.0.0-preview.1 (root and EF Core), and claim E89.Oracles:
StringSetCollationTestsis registered as anexceptionoracle, 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 andExecuteUpdatewithRemoveeach throw and name the collation, and the row stays unchanged. A"C"column,Collate(Owner, "C"), and aCASEthat only testsOwnerrun and answer as memory does. The rejection also holds on a compiled model, generated asdotnet ef dbcontext optimizedoes.CompiledModelJsonTests.CompileModelnow takes any context.StringSetCollationTests: adds a derived table's column, a set operation's column, a concatenation and aCASEresult.oracles.cs seeds string-set-collationcatches both seeds: the throw removed, and the convention leaving the column unnamed.🤖 Generated with Claude Code