Database migrations: Support DateOnly and TimeOnly in syntax providers (#20784)

* sql column type map include dateonly and timeonly

* Split Mapper and add check null value

* Minor code tidy resolving a few warnings.

* add spaces

* clean code

---------

Co-authored-by: Lan Nguyen Thuy <lnt@umbraco.dk>
Co-authored-by: Andy Butland <abutland73@gmail.com>
This commit is contained in:
NguyenThuyLan
2025-11-13 11:10:51 +07:00
committed by GitHub
parent ca15aadf0e
commit 139b528bda
7 changed files with 163 additions and 36 deletions

View File

@@ -0,0 +1,64 @@
using NPoco;
namespace Umbraco.Cms.Persistence.Sqlite.Mappers;
/// <summary>
/// Provides a custom POCO mapper for handling date and time only values when working with SQLite databases.
/// </summary>
public class SqlitePocoDateAndTimeOnlyMapper : DefaultMapper
{
/// <inheritdoc/>
public override Func<object, object?> 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()!);
}
}

View File

@@ -0,0 +1,26 @@
using System.Globalization;
using NPoco;
namespace Umbraco.Cms.Persistence.Sqlite.Mappers;
/// <summary>
/// Provides a custom POCO mapper for handling decimal values when working with SQLite databases.
/// </summary>
public class SqlitePocoDecimalMapper : DefaultMapper
{
/// <inheritdoc/>
public override Func<object, object?> 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);
}
}

View File

@@ -1,19 +1,18 @@
using System.Globalization;
using NPoco;
namespace Umbraco.Cms.Persistence.Sqlite.Mappers;
/// <summary>
/// Provides a custom POCO mapper for handling GUID values when working with SQLite databases.
/// </summary>
public class SqlitePocoGuidMapper : DefaultMapper
{
/// <inheritdoc/>
public override Func<object, object?> 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);
}
}

View File

@@ -12,5 +12,5 @@ public class SqliteSpecificMapperFactory : IProviderSpecificMapperFactory
public string ProviderName => Constants.ProviderName;
/// <inheritdoc />
public NPocoMapperCollection Mappers => new(() => new[] { new SqlitePocoGuidMapper() });
public NPocoMapperCollection Mappers => new(() => [new SqlitePocoGuidMapper(), new SqlitePocoDecimalMapper(), new SqlitePocoDateAndTimeOnlyMapper()]);
}

View File

@@ -1,30 +1,76 @@
using System.Globalization;
using System.Globalization;
using NPoco;
namespace Umbraco.Cms.Core.Mapping;
/// <summary>
/// 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.
/// </summary>
public class UmbracoDefaultMapper : DefaultMapper
{
/// <inheritdoc/>
public override Func<object, object?> 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 =>
return value => Convert.ToDecimal(value, CultureInfo.InvariantCulture);
}
if(IsDateOnlyType(destType))
{
var result = Convert.ToDecimal(value, CultureInfo.InvariantCulture);
return result;
};
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()!);
}
}

View File

@@ -100,6 +100,10 @@ public abstract class SqlSyntaxProviderBase<TSyntax> : ISqlSyntaxProvider
public string TimeColumnDefinition { get; protected set; } = "DATETIME";
public string DateOnlyColumnDefinition { get; protected set; } = "DATE";
public string TimeOnlyColumnDefinition { get; protected set; } = "TIME";
protected IList<Func<ColumnDefinition, string>> ClauseOrder { get; }
protected DbTypes DbTypeMap => _dbTypes.Value;
@@ -531,6 +535,10 @@ public abstract class SqlSyntaxProviderBase<TSyntax> : ISqlSyntaxProvider
dbTypeMap.Set<TimeSpan?>(DbType.Time, TimeColumnDefinition);
dbTypeMap.Set<DateTimeOffset>(DbType.DateTimeOffset, DateTimeOffsetColumnDefinition);
dbTypeMap.Set<DateTimeOffset?>(DbType.DateTimeOffset, DateTimeOffsetColumnDefinition);
dbTypeMap.Set<DateOnly>(DbType.Date, DateOnlyColumnDefinition);
dbTypeMap.Set<DateOnly?>(DbType.Date, DateOnlyColumnDefinition);
dbTypeMap.Set<TimeOnly>(DbType.Time, TimeOnlyColumnDefinition);
dbTypeMap.Set<TimeOnly?>(DbType.Time, TimeOnlyColumnDefinition);
dbTypeMap.Set<byte>(DbType.Byte, IntColumnDefinition);
dbTypeMap.Set<byte?>(DbType.Byte, IntColumnDefinition);

View File

@@ -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)
{