diff --git a/src/DocumentDbTests/Deleting/soft_deletes.cs b/src/DocumentDbTests/Deleting/soft_deletes.cs index 5385ce15a7..2b6494d83c 100644 --- a/src/DocumentDbTests/Deleting/soft_deletes.cs +++ b/src/DocumentDbTests/Deleting/soft_deletes.cs @@ -720,6 +720,24 @@ public async Task should_partition_through_attribute() partitioning.Partitions.Single().ShouldBe(new ListPartition("deleted", "true")); } + + [Fact] + public async Task delete_sets_ISoftDeleted_properties_on_in_memory_document() + { + var doc1 = new SoftDeletedDocument { Id = "50" }; + theSession.Store(doc1); + await theSession.SaveChangesAsync(); + + await using var session2 = theStore.IdentitySession(); + var loadedDoc = await session2.LoadAsync(doc1.Id); + loadedDoc.ShouldNotBeNull(); + + session2.Delete(loadedDoc); + await session2.SaveChangesAsync(); + + loadedDoc.Deleted.ShouldBeTrue(); + loadedDoc.DeletedAt.ShouldNotBeNull(); + } } public class soft_deletes_with_partitioning: OneOffConfigurationsContext, IAsyncLifetime diff --git a/src/Marten.NodaTime.Testing/Acceptance/noda_time_acceptance.cs b/src/Marten.NodaTime.Testing/Acceptance/noda_time_acceptance.cs index 57a57094ee..155fb15e5d 100644 --- a/src/Marten.NodaTime.Testing/Acceptance/noda_time_acceptance.cs +++ b/src/Marten.NodaTime.Testing/Acceptance/noda_time_acceptance.cs @@ -316,6 +316,33 @@ public async Task bug_1276_can_select_instant(SerializerType serializerType) } } + [Theory] + [InlineData(SerializerType.SystemTextJson)] + [InlineData(SerializerType.Newtonsoft)] + public async Task can_index_noda_time_types(SerializerType serializerType) + { + StoreOptions(opts => + { + if (serializerType == SerializerType.Newtonsoft) + { + opts.UseNewtonsoftForSerialization(); + } + opts.UseNodaTime(); + opts.DatabaseSchemaName = "NodaTime"; + opts.Schema.For() + .Duplicate(x => x.LocalDate) + .Duplicate(x => x.LocalDateTime) + .Duplicate(x => x.InstantUTC); + }, true); + + await theStore.Advanced.Clean.CompletelyRemoveAllAsync(); + + // This will apply schema changes, creating indexes on NodaTime fields. + // Previously this would fail with "functions in index expression must be marked IMMUTABLE" + await theStore.Storage.ApplyAllConfiguredChangesToDatabaseAsync(); + await theStore.Storage.Database.AssertDatabaseMatchesConfigurationAsync(); + } + private class CustomJsonSerializer: ISerializer { public EnumStorage EnumStorage => throw new NotSupportedException(); diff --git a/src/Marten.NodaTime/NodaTimeExtensions.cs b/src/Marten.NodaTime/NodaTimeExtensions.cs index e86259d613..282882351d 100644 --- a/src/Marten.NodaTime/NodaTimeExtensions.cs +++ b/src/Marten.NodaTime/NodaTimeExtensions.cs @@ -26,6 +26,8 @@ public static void UseNodaTime(this StoreOptions storeOptions, bool shouldConfig SetNodaTimeTypeMappings(); NpgsqlConnection.GlobalTypeMapper.UseNodaTime(); + storeOptions.Linq.MemberSources.Insert(0, new NodaTimeMemberSource()); + if (!shouldConfigureJsonSerializer) return; storeOptions.Advanced.ModifySerializer(serializer => diff --git a/src/Marten.NodaTime/NodaTimeMemberSource.cs b/src/Marten.NodaTime/NodaTimeMemberSource.cs new file mode 100644 index 0000000000..d32faff31f --- /dev/null +++ b/src/Marten.NodaTime/NodaTimeMemberSource.cs @@ -0,0 +1,63 @@ +#nullable enable +using System; +using System.Diagnostics.CodeAnalysis; +using System.Reflection; +using Marten.Linq.Members; +using NodaTime; + +namespace Marten.NodaTimePlugin; + +internal class NodaTimeMemberSource: IMemberSource +{ + public bool TryResolve(IQueryableMember parent, StoreOptions options, MemberInfo memberInfo, Type memberType, + [NotNullWhen(true)] out IQueryableMember? member) + { + if (memberType == typeof(LocalDate) || memberType == typeof(LocalDate?)) + { + member = new DateOnlyMember(options, parent, options.Serializer().Casing, memberInfo); + return true; + } + + if (memberType == typeof(LocalDateTime) || memberType == typeof(LocalDateTime?)) + { + member = new DateTimeMember(options, parent, options.Serializer().Casing, memberInfo); + return true; + } + + if (memberType == typeof(Instant) || memberType == typeof(Instant?) + || memberType == typeof(ZonedDateTime) || memberType == typeof(ZonedDateTime?) + || memberType == typeof(OffsetDateTime) || memberType == typeof(OffsetDateTime?)) + { + member = new NodaTimeTimestampTzMember(options, parent, options.Serializer().Casing, memberInfo); + return true; + } + + if (memberType == typeof(LocalTime) || memberType == typeof(LocalTime?)) + { + member = new TimeOnlyMember(options, parent, options.Serializer().Casing, memberInfo); + return true; + } + + member = null; + return false; + } +} + +/// +/// Member class for NodaTime types that map to PostgreSQL 'timestamp with time zone'. +/// Uses the mt_immutable_timestamptz wrapper function for index compatibility +/// without the .NET DateTimeOffset-specific casting in comparisons. +/// +internal class NodaTimeTimestampTzMember: QueryableMember, IComparableMember +{ + public NodaTimeTimestampTzMember(StoreOptions options, IQueryableMember parent, Casing casing, MemberInfo member) + : base(parent, casing, member) + { + TypedLocator = $"{options.DatabaseSchemaName}.mt_immutable_timestamptz({RawLocator})"; + } + + public override string SelectorForDuplication(string pgType) + { + return TypedLocator.Replace("d.data", "data"); + } +} diff --git a/src/Marten/Internal/Sessions/DocumentSessionBase.Deletes.cs b/src/Marten/Internal/Sessions/DocumentSessionBase.Deletes.cs index b86aea29e6..278d9b8c78 100644 --- a/src/Marten/Internal/Sessions/DocumentSessionBase.Deletes.cs +++ b/src/Marten/Internal/Sessions/DocumentSessionBase.Deletes.cs @@ -8,6 +8,7 @@ using Marten.Exceptions; using Marten.Internal.Storage; using Marten.Linq.SqlGeneration; +using Marten.Metadata; namespace Marten.Internal.Sessions; @@ -26,6 +27,12 @@ public void Delete(T entity) where T : notnull var deletion = documentStorage.DeleteForDocument(entity, TenantId); _workTracker.Add(deletion); + if (entity is ISoftDeleted softDeleted) + { + softDeleted.Deleted = true; + softDeleted.DeletedAt = DateTimeOffset.UtcNow; + } + documentStorage.Eject(this, entity); ChangeTrackers.RemoveAll(x => x.Document is T t && documentStorage.IdentityFor(entity).Equals(documentStorage.IdentityFor(t)));