Skip to content

list.Contains(valueSetColumn) throws InvalidCastException in Npgsql #46

Closed

Description

@CaffeinatedCoder

A query that tests whether a value set column is one of a client-side list of sets throws InvalidCastException from Npgsql:

var wanted = new List<StringSet> { StringSet.From("a", "b"), StringSet.From("c") };
db.Items.Where(r => wanted.Contains(r.Tags));

The list is bound as a jagged array (text[][]), which Npgsql can't write. It fails loudly. Either translate it to something Npgsql can bind, or refuse it in translation with a message that says what to write instead.

Found during the 9.0.0 final review. The exact repro needs pinning down before the fix.

Activity

  1. added this to the 9.1.0 milestone on Oct 17, 2026
  2. added
    bugSomething isn't working
    area/value-setsValue set families and wrapper arities
    package/efcoreAffects CodoMetis.ValueRanges.EFCore.PostgreSQL
    on Oct 17, 2026
  3. CaffeinatedCoder commented on Oct 17, 2026

    @CaffeinatedCoder
    OwnerAuthor

    Repro pinned down (live PostgreSQL 17, Npgsql EF 10.0.0)

    Only a captured collection fails. Constant and inline collections already translated and answered correctly.

    How the collection reaches the query SQL before Result before
    captured List<TSet> / TSet[] / IEnumerable<TSet> r."Tags" = ANY (@wanted) InvalidCastException: Writing values of 'System.String[][]' is not supported…
    EF.Parameter(list) r."Tags" = ANY (@wanted) same InvalidCastException
    constant list, EF.Constant(list) r."Tags" IN (ARRAY['a','b']::text[], ARRAY['c']::text[]) correct
    inline new[] { a, b } r."Tags" IN (@p, @p0) correct
    captured HashSet<TSet> (resolves to HashSet.Contains) r."Tags" = ANY (@hash) same InvalidCastException

    Every family fails the same way in the captured case: StringSet, StringSet? (with array_position(@p, NULL) for the null arm), GuidSet, Int16Set, Int32Set, Int64Set, DecimalSet, DateSet, TimeSet, DateTimeSet, DateTimeOffsetSet, LocalDateSet, InstantSet, YearMonthSet (LocalDate[][]), and the wrapper arities (StringSet<TestKey> → String[][], Int32Set<SmallCode> → Int32[][]). Any/Count over the captured collection (unnest(@p)) fail the same way.

    Cause: Npgsql translates a parameterized collection as one array parameter. A value set is already a PostgreSQL array, so the collection becomes an array of arrays. PostgreSQL has no such type (its multi-dimensional arrays are not jagged), so Npgsql cannot write it.

    Decision: translate

    EF Core 10's ParameterTranslationMode.MultipleParameters sends each set as its own parameter, through the set's own mapping. Contains becomes IN (@p1, @p2, …), and Any/Count read the collection from VALUES (@p1), (@p2). Each comparison is array equality between two canonical arrays, which is set equality, so the answer equals the in-memory one. It is the same SQL the constant and inline forms already produce. The query interceptor switches only a collection whose element type is a registered value set and whose effective mode is Parameter, so a context configured for Constant or MultipleParameters keeps its mode.

    EF.Parameter(list) explicitly asks for the one array parameter that can't exist, so it is refused in translation with a message that says to remove it. A server-side Union stays refused under IN by ValueSetSqlGuard, as it already was under = ANY.

    Side finding, filed separately: a captured List<Int32Range> fails too, with 42809: op ANY/ALL (array) requires array on right side.

  4. added a commit that references this issue on Oct 17, 2026
    d076560
  5. CaffeinatedCoder commented on Oct 17, 2026

    @CaffeinatedCoder
    OwnerAuthor

    Oracle check (main at 3ecfa10): no oracle on main catches this (pending). I reverted #75's value-set branch on 3ecfa10 (hand-reverted, because #77 has since changed the same interceptor) and ran every class in oracles.json, including the live ones. All of them stayed green. The fix's own regression tests did fail (SetCollectionContainsTests: 8 methods, InvalidCastException: Writing values of 'System.String[][]'), which shows the revert put the defect back.
    Gap: the EF Core Operand position axis in axes.md has no captured collection value (wanted.Contains(col), Any/Count over a List<T>). DifferentialFuzzParityTests binds each operand only as a column, a constant or a parameter. Claim E36 is held only by the regression tests SetCollectionContainsTests and SetCollectionTranslationTests. The missing pair is captured collection × every set type, and route B steps 2 and 3 don't vary it.

  6. CaffeinatedCoder commented on Oct 17, 2026

    @CaffeinatedCoder
    OwnerAuthor

    Update: would be caught by CapturedCollectionProbeTests (#138, the probe sweep for the gap named above). Its seed set-collection-sent-as-one-parameter is this defect, and dotnet run tools/oracles.cs seeds reports it CAUGHT: 5,760 failing lines, every value set type through every collection form, NotSupportedException: Writing is not supported for jagged collections. Labelled found-by/oracle retroactively.

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 workingfound-by/oracleFound by the generated-oracle machinery (claims × API surfaces)package/efcoreAffects CodoMetis.ValueRanges.EFCore.PostgreSQLseverity/mediumInconsistency or failure in a narrow scenario

    Projects

    No projects

      Milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions