diff --git a/src/DocumentDbTests/Indexes/computed_indexes.cs b/src/DocumentDbTests/Indexes/computed_indexes.cs index e2ce57284f..5f009ae749 100644 --- a/src/DocumentDbTests/Indexes/computed_indexes.cs +++ b/src/DocumentDbTests/Indexes/computed_indexes.cs @@ -371,3 +371,73 @@ public class ApiResponseRecord public string Response { get; set; } public DateTime RequestTimeUtc { get; set; } } + +public class computed_indexes_jsonpropertyname : OneOffConfigurationsContext +{ + [Fact] + public async Task datetimeoffset_index_ddl_uses_json_property_name() + { + StoreOptions(_ => + { + _.UseSystemTextJsonForSerialization(EnumStorage.AsInteger, Casing.Default); + _.Schema.For().Index(x => x.Timestamp, c => c.Name = "idx_b4253_ts"); + _.AutoCreateSchemaObjects = AutoCreate.All; + }); + + await theStore.Storage.Database.ApplyAllConfiguredChangesToDatabaseAsync(AutoCreate.CreateOrUpdate); + + var table = await theStore.Tenancy.Default.Database.ExistingTableFor(typeof(Bug4253Doc)); + var index = table.IndexFor("idx_b4253_ts"); + + index.ShouldNotBeNull(); + var ddl = index.ToDDL(table); + + // Must reference the JSON key 'ts' from [JsonPropertyName], not the C# name 'Timestamp' + ddl.ShouldContain("'ts'"); + ddl.ShouldNotContain("'Timestamp'"); + } + + [Fact] + public async Task datetimeoffset_required_init_index_ddl_uses_json_property_name() + { + StoreOptions(_ => + { + _.UseSystemTextJsonForSerialization(EnumStorage.AsInteger, Casing.Default); + _.Schema.For().Index(x => x.Timestamp, c => c.Name = "idx_b4253_req_ts"); + _.AutoCreateSchemaObjects = AutoCreate.All; + }); + + await theStore.Storage.Database.ApplyAllConfiguredChangesToDatabaseAsync(AutoCreate.CreateOrUpdate); + + var table = await theStore.Tenancy.Default.Database.ExistingTableFor(typeof(Bug4253RequiredInitDoc)); + var index = table.IndexFor("idx_b4253_req_ts"); + + index.ShouldNotBeNull(); + var ddl = index.ToDDL(table); + + ddl.ShouldContain("'ts'"); + ddl.ShouldNotContain("'Timestamp'"); + } +} + +public class Bug4253Doc +{ + public Guid Id { get; set; } + + [System.Text.Json.Serialization.JsonPropertyName("ts")] + public DateTimeOffset Timestamp { get; set; } + + [System.Text.Json.Serialization.JsonPropertyName("auid")] + public int ActorUserId { get; set; } +} + +public class Bug4253RequiredInitDoc +{ + public Guid Id { get; set; } + + [System.Text.Json.Serialization.JsonPropertyName("ts")] + public required DateTimeOffset Timestamp { get; init; } + + [System.Text.Json.Serialization.JsonPropertyName("auid")] + public required int ActorUserId { get; init; } +} diff --git a/src/LinqTests/Acceptance/json_naming_attributes.cs b/src/LinqTests/Acceptance/json_naming_attributes.cs index f6ceec6682..ececa5be07 100644 --- a/src/LinqTests/Acceptance/json_naming_attributes.cs +++ b/src/LinqTests/Acceptance/json_naming_attributes.cs @@ -7,6 +7,7 @@ using Marten.Testing.Harness; using Newtonsoft.Json; using Shouldly; +using Weasel.Core; namespace LinqTests.Acceptance; @@ -48,6 +49,156 @@ public async Task recognize_stj_json_property_in_linq() command.CommandText.ShouldBe("select d.id, d.data from atts.mt_doc_stjdoc as d where d.data ->> 'shade' = :p0;"); } + + [Fact] + public void recognize_stj_json_property_on_datetimeoffset_in_linq() + { + using var store = DocumentStore.For(opts => + { + opts.Connection(ConnectionSource.ConnectionString); + opts.DatabaseSchemaName = "atts"; + opts.UseSystemTextJsonForSerialization(EnumStorage.AsInteger, Casing.Default); + }); + + using var session = store.LightweightSession(); + + var cutoff = DateTimeOffset.UtcNow.AddDays(-1); + var command = session.Query() + .Where(x => x.Timestamp >= cutoff) + .ToCommand(); + + // The JSON key 'ts' (from [JsonPropertyName]) must appear; 'Timestamp' (C# name) must not + command.CommandText.ShouldContain("'ts'"); + command.CommandText.ShouldNotContain("'Timestamp'"); + } + + [Fact] + public void recognize_stj_json_property_on_datetimeoffset_required_init_in_linq() + { + using var store = DocumentStore.For(opts => + { + opts.Connection(ConnectionSource.ConnectionString); + opts.DatabaseSchemaName = "atts"; + opts.UseSystemTextJsonForSerialization(EnumStorage.AsInteger, Casing.Default); + }); + + using var session = store.LightweightSession(); + + var cutoff = DateTimeOffset.UtcNow.AddDays(-1); + var command = session.Query() + .Where(x => x.Timestamp >= cutoff) + .ToCommand(); + + command.CommandText.ShouldContain("'ts'"); + command.CommandText.ShouldNotContain("'Timestamp'"); + } + + [Fact] + public void recognize_stj_json_property_on_datetime_in_linq() + { + using var store = DocumentStore.For(opts => + { + opts.Connection(ConnectionSource.ConnectionString); + opts.DatabaseSchemaName = "atts"; + opts.UseSystemTextJsonForSerialization(EnumStorage.AsInteger, Casing.Default); + }); + + using var session = store.LightweightSession(); + + var cutoff = DateTime.UtcNow.AddDays(-1); + var command = session.Query() + .Where(x => x.DateTime >= cutoff) + .ToCommand(); + + command.CommandText.ShouldContain("'dt'"); + command.CommandText.ShouldNotContain("'DateTime'"); + } + + [Fact] + public void recognize_stj_json_property_on_dateonly_in_linq() + { + using var store = DocumentStore.For(opts => + { + opts.Connection(ConnectionSource.ConnectionString); + opts.DatabaseSchemaName = "atts"; + opts.UseSystemTextJsonForSerialization(EnumStorage.AsInteger, Casing.Default); + }); + + using var session = store.LightweightSession(); + + var cutoff = DateOnly.FromDateTime(DateTime.UtcNow); + var command = session.Query() + .Where(x => x.DateOnly >= cutoff) + .ToCommand(); + + command.CommandText.ShouldContain("'d_only'"); + command.CommandText.ShouldNotContain("'DateOnly'"); + } + + [Fact] + public void recognize_stj_json_property_on_timeonly_in_linq() + { + using var store = DocumentStore.For(opts => + { + opts.Connection(ConnectionSource.ConnectionString); + opts.DatabaseSchemaName = "atts"; + opts.UseSystemTextJsonForSerialization(EnumStorage.AsInteger, Casing.Default); + }); + + using var session = store.LightweightSession(); + + var cutoff = TimeOnly.FromDateTime(DateTime.UtcNow); + var command = session.Query() + .Where(x => x.TimeOnly >= cutoff) + .ToCommand(); + + command.CommandText.ShouldContain("'t_only'"); + command.CommandText.ShouldNotContain("'TimeOnly'"); + } + + [Fact] + public async Task end_to_end_query_with_datetimeoffset_json_property_name() + { + using var store = DocumentStore.For(opts => + { + opts.Connection(ConnectionSource.ConnectionString); + opts.DatabaseSchemaName = "atts_dto_e2e"; + opts.UseSystemTextJsonForSerialization(EnumStorage.AsInteger, Casing.Default); + }); + + await store.Advanced.Clean.CompletelyRemoveAllAsync(); + + await using (var session = store.LightweightSession()) + { + session.Store(new StjDateTimeOffsetDoc + { + Id = Guid.NewGuid(), + Timestamp = DateTimeOffset.UtcNow, + ActorUserId = 42 + }); + await session.SaveChangesAsync(); + } + + await using (var session = store.QuerySession()) + { + var cutoff = DateTimeOffset.UtcNow.AddDays(-1); + + var totalCount = await session.Query().CountAsync(); + totalCount.ShouldBe(1); + + // The bug: this silently returns 0 even though the data matches + var recent = await session.Query() + .Where(x => x.Timestamp >= cutoff) + .CountAsync(); + recent.ShouldBe(1); + + // Sanity: the int filter with JsonPropertyName works + var byActor = await session.Query() + .Where(x => x.ActorUserId == 42) + .CountAsync(); + byActor.ShouldBe(1); + } + } } public class AttributedDoc @@ -65,3 +216,42 @@ public class StjDoc [JsonPropertyName("shade")] public string Color { get; set; } } + +public class StjDateTimeOffsetDoc +{ + public Guid Id { get; set; } + + [JsonPropertyName("ts")] + public DateTimeOffset Timestamp { get; set; } + + [JsonPropertyName("auid")] + public int ActorUserId { get; set; } +} + +public class StjRequiredInitDateTimeOffsetDoc +{ + public Guid Id { get; set; } + + [JsonPropertyName("ts")] + public required DateTimeOffset Timestamp { get; init; } + + [JsonPropertyName("auid")] + public required int ActorUserId { get; init; } +} + +public class StjTemporalDoc +{ + public Guid Id { get; set; } + + [JsonPropertyName("dt")] + public DateTime DateTime { get; set; } + + [JsonPropertyName("dto")] + public DateTimeOffset DateTimeOffset { get; set; } + + [JsonPropertyName("d_only")] + public DateOnly DateOnly { get; set; } + + [JsonPropertyName("t_only")] + public TimeOnly TimeOnly { get; set; } +}