diff --git a/src/Umbraco.Cms.Persistence.Sqlite/Mappers/SqlitePocoDateAndTimeOnlyMapper.cs b/src/Umbraco.Cms.Persistence.Sqlite/Mappers/SqlitePocoDateAndTimeOnlyMapper.cs new file mode 100644 index 0000000000..4e3ec6a411 --- /dev/null +++ b/src/Umbraco.Cms.Persistence.Sqlite/Mappers/SqlitePocoDateAndTimeOnlyMapper.cs @@ -0,0 +1,64 @@ +using NPoco; + +namespace Umbraco.Cms.Persistence.Sqlite.Mappers; + +/// +/// Provides a custom POCO mapper for handling date and time only values when working with SQLite databases. +/// +public class SqlitePocoDateAndTimeOnlyMapper : DefaultMapper +{ + /// + public override Func GetFromDbConverter(Type destType, Type sourceType) + { + if (IsDateOnlyType(destType)) + { + return value => ConvertToDateOnly(value, IsNullableType(destType)); + } + + if (IsTimeOnlyType(destType)) + { + return value => ConvertToTimeOnly(value, IsNullableType(destType)); + } + + return base.GetFromDbConverter(destType, sourceType); + } + + private static bool IsDateOnlyType(Type type) => + type == typeof(DateOnly) || type == typeof(DateOnly?); + + private static bool IsTimeOnlyType(Type type) => + type == typeof(TimeOnly) || type == typeof(TimeOnly?); + + private static bool IsNullableType(Type type) => + Nullable.GetUnderlyingType(type) != null; + + private static object? ConvertToDateOnly(object? value, bool isNullable) + { + if (value is null) + { + return isNullable ? null : default(DateOnly); + } + + if (value is DateTime dt) + { + return DateOnly.FromDateTime(dt); + } + + return DateOnly.Parse(value.ToString()!); + } + + private static object? ConvertToTimeOnly(object? value, bool isNullable) + { + if (value is null) + { + return isNullable ? null : default(TimeOnly); + } + + if (value is DateTime dt) + { + return TimeOnly.FromDateTime(dt); + } + + return TimeOnly.Parse(value.ToString()!); + } +} diff --git a/src/Umbraco.Cms.Persistence.Sqlite/Mappers/SqlitePocoDecimalMapper.cs b/src/Umbraco.Cms.Persistence.Sqlite/Mappers/SqlitePocoDecimalMapper.cs new file mode 100644 index 0000000000..567b1cbcb8 --- /dev/null +++ b/src/Umbraco.Cms.Persistence.Sqlite/Mappers/SqlitePocoDecimalMapper.cs @@ -0,0 +1,26 @@ +using System.Globalization; +using NPoco; + +namespace Umbraco.Cms.Persistence.Sqlite.Mappers; + +/// +/// Provides a custom POCO mapper for handling decimal values when working with SQLite databases. +/// +public class SqlitePocoDecimalMapper : DefaultMapper +{ + /// + public override Func GetFromDbConverter(Type destType, Type sourceType) + { + if (destType == typeof(decimal)) + { + return value => Convert.ToDecimal(value, CultureInfo.InvariantCulture); + } + + if (destType == typeof(decimal?)) + { + return value => Convert.ToDecimal(value, CultureInfo.InvariantCulture); + } + + return base.GetFromDbConverter(destType, sourceType); + } +} diff --git a/src/Umbraco.Cms.Persistence.Sqlite/Mappers/SqlitePocoGuidMapper.cs b/src/Umbraco.Cms.Persistence.Sqlite/Mappers/SqlitePocoGuidMapper.cs index ab62b4b1d1..ffb96a6b2b 100644 --- a/src/Umbraco.Cms.Persistence.Sqlite/Mappers/SqlitePocoGuidMapper.cs +++ b/src/Umbraco.Cms.Persistence.Sqlite/Mappers/SqlitePocoGuidMapper.cs @@ -1,19 +1,18 @@ -using System.Globalization; using NPoco; namespace Umbraco.Cms.Persistence.Sqlite.Mappers; +/// +/// Provides a custom POCO mapper for handling GUID values when working with SQLite databases. +/// public class SqlitePocoGuidMapper : DefaultMapper { + /// public override Func GetFromDbConverter(Type destType, Type sourceType) { if (destType == typeof(Guid)) { - return value => - { - var result = Guid.Parse($"{value}"); - return result; - }; + return value => Guid.Parse($"{value}"); } if (destType == typeof(Guid?)) @@ -29,24 +28,6 @@ public class SqlitePocoGuidMapper : DefaultMapper }; } - if (destType == typeof(decimal)) - { - return value => - { - var result = Convert.ToDecimal(value, CultureInfo.InvariantCulture); - return result; - }; - } - - if (destType == typeof(decimal?)) - { - return value => - { - var result = Convert.ToDecimal(value, CultureInfo.InvariantCulture); - return result; - }; - } - return base.GetFromDbConverter(destType, sourceType); } } diff --git a/src/Umbraco.Cms.Persistence.Sqlite/Services/SqliteSpecificMapperFactory.cs b/src/Umbraco.Cms.Persistence.Sqlite/Services/SqliteSpecificMapperFactory.cs index 66f542712a..9c577d6329 100644 --- a/src/Umbraco.Cms.Persistence.Sqlite/Services/SqliteSpecificMapperFactory.cs +++ b/src/Umbraco.Cms.Persistence.Sqlite/Services/SqliteSpecificMapperFactory.cs @@ -12,5 +12,5 @@ public class SqliteSpecificMapperFactory : IProviderSpecificMapperFactory public string ProviderName => Constants.ProviderName; /// - public NPocoMapperCollection Mappers => new(() => new[] { new SqlitePocoGuidMapper() }); + public NPocoMapperCollection Mappers => new(() => [new SqlitePocoGuidMapper(), new SqlitePocoDecimalMapper(), new SqlitePocoDateAndTimeOnlyMapper()]); } diff --git a/src/Umbraco.Infrastructure/Mapping/UmbracoDefaultMapper.cs b/src/Umbraco.Infrastructure/Mapping/UmbracoDefaultMapper.cs index 986bb19d39..0afb677ee9 100644 --- a/src/Umbraco.Infrastructure/Mapping/UmbracoDefaultMapper.cs +++ b/src/Umbraco.Infrastructure/Mapping/UmbracoDefaultMapper.cs @@ -1,30 +1,76 @@ -using System.Globalization; +using System.Globalization; using NPoco; namespace Umbraco.Cms.Core.Mapping; +/// +/// Provides default type conversion logic for mapping Umbraco database values to .NET types, extending the base mapping +/// behavior with support for additional types such as decimal, DateOnly, and TimeOnly. +/// public class UmbracoDefaultMapper : DefaultMapper { + /// public override Func GetFromDbConverter(Type destType, Type sourceType) { if (destType == typeof(decimal)) { - return value => - { - var result = Convert.ToDecimal(value, CultureInfo.InvariantCulture); - return result; - }; + return value => Convert.ToDecimal(value, CultureInfo.InvariantCulture); } if (destType == typeof(decimal?)) { - return value => - { - var result = Convert.ToDecimal(value, CultureInfo.InvariantCulture); - return result; - }; + return value => Convert.ToDecimal(value, CultureInfo.InvariantCulture); + } + + if(IsDateOnlyType(destType)) + { + return value => ConvertToDateOnly(value, IsNullableType(destType)); + } + + if (IsTimeOnlyType(destType)) + { + return value => ConvertToTimeOnly(value, IsNullableType(destType)); } return base.GetFromDbConverter(destType, sourceType); } + + private static bool IsDateOnlyType(Type type) => + type == typeof(DateOnly) || type == typeof(DateOnly?); + + private static bool IsTimeOnlyType(Type type) => + type == typeof(TimeOnly) || type == typeof(TimeOnly?); + + private static bool IsNullableType(Type type) => + Nullable.GetUnderlyingType(type) != null; + + private static object? ConvertToDateOnly(object? value, bool isNullable) + { + if (value is null) + { + return isNullable ? null : default(DateOnly); + } + + if (value is DateTime dt) + { + return DateOnly.FromDateTime(dt); + } + + return DateOnly.Parse(value.ToString()!); + } + + private static object? ConvertToTimeOnly(object? value, bool isNullable) + { + if (value is null) + { + return isNullable ? null : default(TimeOnly); + } + + if (value is DateTime dt) + { + return TimeOnly.FromDateTime(dt); + } + + return TimeOnly.Parse(value.ToString()!); + } } diff --git a/src/Umbraco.Infrastructure/Persistence/SqlSyntax/SqlSyntaxProviderBase.cs b/src/Umbraco.Infrastructure/Persistence/SqlSyntax/SqlSyntaxProviderBase.cs index 9cefd32b5b..82bbf26f81 100644 --- a/src/Umbraco.Infrastructure/Persistence/SqlSyntax/SqlSyntaxProviderBase.cs +++ b/src/Umbraco.Infrastructure/Persistence/SqlSyntax/SqlSyntaxProviderBase.cs @@ -100,6 +100,10 @@ public abstract class SqlSyntaxProviderBase : ISqlSyntaxProvider public string TimeColumnDefinition { get; protected set; } = "DATETIME"; + public string DateOnlyColumnDefinition { get; protected set; } = "DATE"; + + public string TimeOnlyColumnDefinition { get; protected set; } = "TIME"; + protected IList> ClauseOrder { get; } protected DbTypes DbTypeMap => _dbTypes.Value; @@ -531,6 +535,10 @@ public abstract class SqlSyntaxProviderBase : ISqlSyntaxProvider dbTypeMap.Set(DbType.Time, TimeColumnDefinition); dbTypeMap.Set(DbType.DateTimeOffset, DateTimeOffsetColumnDefinition); dbTypeMap.Set(DbType.DateTimeOffset, DateTimeOffsetColumnDefinition); + dbTypeMap.Set(DbType.Date, DateOnlyColumnDefinition); + dbTypeMap.Set(DbType.Date, DateOnlyColumnDefinition); + dbTypeMap.Set(DbType.Time, TimeOnlyColumnDefinition); + dbTypeMap.Set(DbType.Time, TimeOnlyColumnDefinition); dbTypeMap.Set(DbType.Byte, IntColumnDefinition); dbTypeMap.Set(DbType.Byte, IntColumnDefinition); diff --git a/tests/Umbraco.Tests.Integration/Testing/SqliteTestDatabase.cs b/tests/Umbraco.Tests.Integration/Testing/SqliteTestDatabase.cs index a20b00ebe5..73b9c444a7 100644 --- a/tests/Umbraco.Tests.Integration/Testing/SqliteTestDatabase.cs +++ b/tests/Umbraco.Tests.Integration/Testing/SqliteTestDatabase.cs @@ -96,6 +96,8 @@ public class SqliteTestDatabase : BaseTestDatabase, ITestDatabase database.Mappers.Add(new NullableDateMapper()); database.Mappers.Add(new SqlitePocoGuidMapper()); + database.Mappers.Add(new SqlitePocoDecimalMapper()); + database.Mappers.Add(new SqlitePocoDateAndTimeOnlyMapper()); foreach (var dbCommand in _cachedDatabaseInitCommands) {