Skip to content

Contains on a JSON primitive collection compares JSON text, so DateTime and TimeOnly items never match #4099

Description

@CaffeinatedCoder
Environment
PostgreSQL 18.6 (postgres:18), session TimeZone Etc/UTC
.NET 10 runtime 10.0.11, Npgsql.EntityFrameworkCore.PostgreSQL 10.0.3, EF Core 10.0.4
.NET 11 runtime 11.0.0-rc.2, Npgsql.EntityFrameworkCore.PostgreSQL 11.0.0-rc.2, EF Core 11.0.0-rc.2
Also Npgsql 10.0.3 on both lines, macOS arm64

Both lines print the same output. main at 150011d has the same code.

LINQ Stored by EF .NET PostgreSQL SQL
r.Doc.Dates.Contains(ten) "2026-01-01T10:00:00Z" match no match (r."Doc" -> 'Dates') @> to_jsonb(@ten)
r.Doc.Times.Contains(new TimeOnly(10, 0)) "10:00:00.0000000" match no match (r."Doc" -> 'Times') @> to_jsonb(TIME '10:00:00')

to_jsonb() of the item gives "2026-01-01T10:00:00+00:00" and "10:00:00", not the text EF wrote into the document, and @> compares the strings.

Repro
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;

await using var context = new TestContext();
await context.Database.EnsureDeletedAsync();
await context.Database.EnsureCreatedAsync();

var ten = new DateTime(2026, 1, 1, 10, 0, 0, DateTimeKind.Utc);
context.Rows.Add(new Row { Doc = new Doc { Dates = [ten], Times = [new TimeOnly(10, 0)] } });
await context.SaveChangesAsync();
var rows = await context.Rows.AsNoTracking().ToListAsync();

Console.WriteLine($"Doc.Dates.Contains(10:00 UTC): .NET {rows.Count(r => r.Doc.Dates.Contains(ten))}, database {await context.Rows.CountAsync(r => r.Doc.Dates.Contains(ten))}");
Console.WriteLine($"Doc.Times.Contains(10:00): .NET {rows.Count(r => r.Doc.Times.Contains(new TimeOnly(10, 0)))}, database {await context.Rows.CountAsync(r => r.Doc.Times.Contains(new TimeOnly(10, 0)))}");
Console.WriteLine($"Stored document: {await context.Database.SqlQueryRaw<string>("""SELECT "Doc"::text AS "Value" FROM "Rows" """).SingleAsync()}");

public class TestContext : DbContext
{
    public DbSet<Row> Rows => Set<Row>();

    protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
        => optionsBuilder
            .UseNpgsql(Environment.GetEnvironmentVariable("Test__Npgsql__DefaultConnection"))
            .LogTo(Console.WriteLine, LogLevel.Information)
            .EnableSensitiveDataLogging();

    protected override void OnModelCreating(ModelBuilder modelBuilder)
        => modelBuilder.Entity<Row>().ComplexProperty(r => r.Doc, d => d.ToJson());
}

public class Row
{
    public int Id { get; set; }
    public Doc Doc { get; set; } = new();
}

public class Doc
{
    public List<DateTime> Dates { get; set; } = [];
    public List<TimeOnly> Times { get; set; } = [];
}

Output, the same on both lines:

Doc.Dates.Contains(10:00 UTC): .NET 1, database 0
Doc.Times.Contains(10:00): .NET 1, database 0
Stored document: {"Dates": ["2026-01-01T10:00:00Z"], "Times": ["10:00:00.0000000"]}

The translation is NpgsqlQueryableMethodTranslatingExpressionVisitor.cs#L756-L766. Its TODO already mentions producing the JSON value on the client with the JsonValueReaderWriter for constants and parameters, which would fix it for those. For a column operand, the comparison would have to be typed (unnest the array and compare as the element type).

Activity

  1. roji commented on Oct 25, 2026

    @roji
    Member

    AI Triage

    The below is an AI-generated analysis and may contain inaccuracies.

    Confirmed provider bug and regression introduced in 10.0. Both reported queries return zero rows even though the matching values were inserted and can be read back. They work with Npgsql 9.0.4.

    Version comparison

    The PostgreSQL runs used isolated PostgreSQL 18 containers. Each query should return one row.

    Npgsql provider EF Core JSON mapping DateTime matches TimeOnly matches
    9.0.4 9.0.4 OwnsOne().ToJson() 1 1
    10.0.0 10.0.0 OwnsOne().ToJson() 0 0
    10.0.3 10.0.4 OwnsOne().ToJson() 0 0
    10.0.3 10.0.4 ComplexProperty().ToJson() 0 0
    10.0.3 10.0.13 ComplexProperty().ToJson() 0 0
    11.0.0-rc.2 11.0.0-rc.2.26504.105 ComplexProperty().ToJson() 0 0

    Using owned JSON on both 9.0.4 and 10.0.0 establishes that this is not just a limitation of the newer complex-type JSON mapping.

    Cause and provider comparison

    The regression traces to 4405acb1, merged in #3623. Its JSON Contains translation introduced @> to_jsonb(item).

    On 9.0.4, the JSON elements are extracted and cast to timestamp with time zone / time without time zone before comparison. From 10.0.0, JSON containment compares strings instead: EF writes "2026-01-01T10:00:00Z" and "10:00:00.0000000", whereas PostgreSQL's to_jsonb() produces different strings. The existing JsonValueReaderWriter TODO identifies the serialization mismatch for constants and parameters.

    With SQL Server 2025 and EF Core 10.0.4, the same complex-type model and both queries return one row. SQL Server uses typed OPENJSON projections (datetime2 and time). This supports keeping the Npgsql regression here rather than moving it upstream.

    SQLite has an additional wrinkle: with both EF Core 10.0.4 and 11 RC2, the DateTime query succeeds but the TimeOnly query also returns zero. That is a related SQLite representation mismatch, not evidence that Npgsql's @> to_jsonb() translation comes from shared EF code.

    Changing either query to Any(x => x == value) is not a workaround: Npgsql translates it to the same failing containment expression.

    Related issues

    No exact duplicate was found in the provider or upstream issue searches. The closest upstream references are:

    • dotnet/efcore#36749, SQLite: align JSON and M.D.Sqlite representations (closed), describes the same trailing-zero mismatch for TimeOnly. Despite that issue being closed, the primitive-collection query above still fails on SQLite 11 RC2.
    • dotnet/efcore#30727, JSON type representations and conversions to store types (closed), discusses extracting JSON values into their relational store types before comparison. This is related background, not a duplicate of this PostgreSQL regression.
    minimal repro

    Use a net10.0 console project with Npgsql.EntityFrameworkCore.PostgreSQL 10.0.3 and Microsoft.EntityFrameworkCore 10.0.4. Set Test__Npgsql__DefaultConnection to a disposable test database; this program deletes and recreates it.

    using Microsoft.EntityFrameworkCore;
    using Microsoft.Extensions.Logging;
    
    await using var context = new TestContext();
    await context.Database.EnsureDeletedAsync();
    await context.Database.EnsureCreatedAsync();
    
    var ten = new DateTime(2026, 1, 1, 10, 0, 0, DateTimeKind.Utc);
    context.Rows.Add(new Row { Doc = new Doc { Dates = [ten], Times = [new TimeOnly(10, 0)] } });
    await context.SaveChangesAsync();
    var rows = await context.Rows.AsNoTracking().ToListAsync();
    
    Console.WriteLine($"Dates: .NET {rows.Count(r => r.Doc.Dates.Contains(ten))}, database {await context.Rows.CountAsync(r => r.Doc.Dates.Contains(ten))}");
    Console.WriteLine($"Times: .NET {rows.Count(r => r.Doc.Times.Contains(new TimeOnly(10, 0)))}, database {await context.Rows.CountAsync(r => r.Doc.Times.Contains(new TimeOnly(10, 0)))}");
    
    public class TestContext : DbContext
    {
        public DbSet<Row> Rows => Set<Row>();
    
        protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
            => optionsBuilder
                .UseNpgsql(Environment.GetEnvironmentVariable("Test__Npgsql__DefaultConnection"))
                .LogTo(Console.WriteLine, LogLevel.Information)
                .EnableSensitiveDataLogging();
    
        protected override void OnModelCreating(ModelBuilder modelBuilder)
            => modelBuilder.Entity<Row>().ComplexProperty(r => r.Doc, d => d.ToJson());
    }
    
    public class Row
    {
        public int Id { get; set; }
        public Doc Doc { get; set; } = new();
    }
    
    public class Doc
    {
        public List<DateTime> Dates { get; set; } = [];
        public List<TimeOnly> Times { get; set; } = [];
    }

    Output:

    Dates: .NET 1, database 0
    Times: .NET 1, database 0
    

    For the 9.0.4 comparison, replace ComplexProperty with OwnsOne and use provider/EF Core 9.0.4; both database counts become 1.

  2. added theissue type on Oct 25, 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

    No labels
    No labels

    Type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions