`TimeSpan`/`Duration` component members: negative values are floored, and intervals computed in SQL are read field by field

Đang mở Phù hợp với người mới
#4,088 0 bình luận 0 reaction 0 người được giao Xem trên GitHub

Maintainer thường phản hồi trong vòng 1 ngày

@jeremy-morren đang làm issue này rồi.

Từ ngày 29/10/2026.

  • #4116 của @jeremy-morren — đang mở

Đánh giá

Độ khó
2/5
Thời gian dự kiến
1-3 giờ
Mức phù hợp với người mới
70/100
Loại issue
Lỗi
Độ rõ ràng
Đặc tả rõ ràng
Mức độ hoạt động
Sôi nổi
Công nghệ
csharp, postgresql, sql
Lĩnh vực
database

Hướng nghiên cứu

Các bản dịch nằm trong src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlTimeSpanMemberTranslator.cs (lines 47-53, 67-75) và src/EFCore.PG.NodaTime/Query/Internal/NpgsqlNodaTimeMemberTranslatorPlugin.cs (lines 119-122, 234). Thay floor bằng trunc và bọc các khoảng thời gian đã tính toán trong justify_interval() trước khi đọc các trường. Xác nhận với repro của issue rằng các span âm khớp với .NET và các khoảng thời gian được chuẩn hóa.

Do mô hình lập chỉ mục viết ra từ nội dung của issue.

Mô tả

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

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

.NET and NodaTime truncate the components of a negative span toward zero. The translations floor them. And an interval computed in SQL keeps its days and time fields apart (1 day -02:00:00), so .Days and .Hours read those raw fields.

LINQ Value .NET PostgreSQL SQL
r.Span.Seconds TimeSpan −1.5 s −1 −2 floor(date_part('second', r."Span"))::int
r.Duration.Seconds Duration −1.5 s −1 −2 floor(date_part('second', r."Duration"))::int
(r.SpanA + r.SpanB).Days / .Hours 1 day + (−2 h) 0 / 22 1 / −2 floor(date_part('day', r."SpanA" + r."SpanB"))::int
(r.DurationA + r.DurationB).Days / .Hours 1 day + (−1 h) 0 / 23 1 / −1 date_part('day', r."DurationA" + r."DurationB")::int
Repro
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
using NodaTime;

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

context.Rows.Add(new Row
{
    Span = TimeSpan.FromMilliseconds(-1500),
    Duration = Duration.FromMilliseconds(-1500),
    SpanA = TimeSpan.FromDays(1), SpanB = TimeSpan.FromHours(-2),
    DurationA = Duration.FromDays(1), DurationB = Duration.FromHours(-1),
});
await context.SaveChangesAsync();
var row = await context.Rows.AsNoTracking().SingleAsync();

// 1. .NET and NodaTime truncate a negative component toward zero; the translation floors it
Console.WriteLine($"TimeSpan(-1.5 s).Seconds: .NET {row.Span.Seconds}, database {await context.Rows.Select(r => r.Span.Seconds).SingleAsync()}");
Console.WriteLine($"Duration(-1.5 s).Seconds: .NET {row.Duration.Seconds}, database {await context.Rows.Select(r => r.Duration.Seconds).SingleAsync()}");

// 2. An interval computed in SQL keeps its days and time fields apart (1 day + -2 h is '1 day -02:00:00')
Console.WriteLine($"(1 day + -2 h).Days/.Hours as TimeSpan: .NET {(row.SpanA + row.SpanB).Days}/{(row.SpanA + row.SpanB).Hours}, database {await context.Rows.Select(r => (r.SpanA + r.SpanB).Days).SingleAsync()}/{await context.Rows.Select(r => (r.SpanA + r.SpanB).Hours).SingleAsync()}");
Console.WriteLine($"(1 day + -1 h).Days/.Hours as Duration: .NET {(row.DurationA + row.DurationB).Days}/{(row.DurationA + row.DurationB).Hours}, database {await context.Rows.Select(r => (r.DurationA + r.DurationB).Days).SingleAsync()}/{await context.Rows.Select(r => (r.DurationA + r.DurationB).Hours).SingleAsync()}");

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

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

public class Row
{
    public int Id { get; set; }
    public TimeSpan Span { get; set; }
    public Duration Duration { get; set; }
    public TimeSpan SpanA { get; set; }
    public TimeSpan SpanB { get; set; }
    public Duration DurationA { get; set; }
    public Duration DurationB { get; set; }
}

Output, the same on both lines:

TimeSpan(-1.5 s).Seconds: .NET -1, database -2
Duration(-1.5 s).Seconds: .NET -1, database -2
(1 day + -2 h).Days/.Hours as TimeSpan: .NET 0/22, database 1/-2
(1 day + -1 h).Days/.Hours as Duration: .NET 0/23, database 1/-1

trunc instead of floor matches .NET for negative values. justify_interval() normalizes a computed interval before its fields are read.

Ngôn ngữ chính
C#
Star
1.8k
Fork
268
Merge trung bình
2 giờ 35 phút
Pull request đã merge (30 ngày)
136

Chuẩn bị môi trường

Dự án này không cung cấp dev container, Dockerfile hay hướng dẫn đóng góp, nên bạn cần tự thiết lập môi trường: hãy bắt đầu từ README và xem hướng dẫn đóng góp lần đầu của chúng tôi để biết các bước chung.

Bắt đầu từ đâu

  1. Đọc hết issue, rồi đọc hướng dẫn đóng góp của dự án.
  2. Bình luận trên issue rằng bạn sẽ nhận — tránh hai người làm cùng một việc.
  3. Fork repository và làm thay đổi trên một nhánh.
  4. Mở pull request có tham chiếu số hiệu của issue.

Issue khác của npgsql/efcore.pg

Tất cả issue của npgsql/efcore.pg

Issue tương tự

Thêm issue về C#

Nhận issue mới trong hộp thư của bạn

Bản tóm tắt ngắn những issue GitHub phù hợp với người mới.