From 6e8911c9bf756c17d017d77aca7d81dd204ca71e Mon Sep 17 00:00:00 2001 From: Gunnar Liljas Date: Tue, 28 Oct 2025 15:59:04 +0100 Subject: [PATCH 1/3] DateOnly and TimeOnly support --- .../TypesTest/AbstractTimeOnlyTypeFixture.cs | 23 ++ .../AbstractTimeOnlyTypeWithScaleFixture.cs | 49 +++ .../TypesTest/DateOnlyAsDateTypeFixture.cs | 26 ++ .../TypesTest/GenericTypeFixtureBase.cs | 279 ++++++++++++++++++ .../TimeOnlyAsDateTimeTypeFixture.cs | 33 +++ .../TypesTest/TimeOnlyAsTicksTypeFixture.cs | 26 ++ .../TypesTest/TimeOnlyAsTimeTypeFixture.cs | 30 ++ .../DateTimePropertiesHqlGenerator.cs | 15 +- src/NHibernate/NHibernateUtil.cs | 22 ++ src/NHibernate/Type/AbstractDateOnlyType.cs | 88 ++++++ src/NHibernate/Type/AbstractTimeOnlyType.cs | 134 +++++++++ src/NHibernate/Type/DateOnlyAsDateType.cs | 37 +++ src/NHibernate/Type/TimeOnlyAsDateTimeType.cs | 67 +++++ src/NHibernate/Type/TimeOnlyAsTicksType.cs | 54 ++++ src/NHibernate/Type/TimeOnlyAsTimeType.cs | 78 +++++ src/NHibernate/Type/TypeFactory.cs | 52 ++++ 16 files changed, 1010 insertions(+), 3 deletions(-) create mode 100644 src/NHibernate.Test/TypesTest/AbstractTimeOnlyTypeFixture.cs create mode 100644 src/NHibernate.Test/TypesTest/AbstractTimeOnlyTypeWithScaleFixture.cs create mode 100644 src/NHibernate.Test/TypesTest/DateOnlyAsDateTypeFixture.cs create mode 100644 src/NHibernate.Test/TypesTest/GenericTypeFixtureBase.cs create mode 100644 src/NHibernate.Test/TypesTest/TimeOnlyAsDateTimeTypeFixture.cs create mode 100644 src/NHibernate.Test/TypesTest/TimeOnlyAsTicksTypeFixture.cs create mode 100644 src/NHibernate.Test/TypesTest/TimeOnlyAsTimeTypeFixture.cs create mode 100644 src/NHibernate/Type/AbstractDateOnlyType.cs create mode 100644 src/NHibernate/Type/AbstractTimeOnlyType.cs create mode 100644 src/NHibernate/Type/DateOnlyAsDateType.cs create mode 100644 src/NHibernate/Type/TimeOnlyAsDateTimeType.cs create mode 100644 src/NHibernate/Type/TimeOnlyAsTicksType.cs create mode 100644 src/NHibernate/Type/TimeOnlyAsTimeType.cs diff --git a/src/NHibernate.Test/TypesTest/AbstractTimeOnlyTypeFixture.cs b/src/NHibernate.Test/TypesTest/AbstractTimeOnlyTypeFixture.cs new file mode 100644 index 00000000000..4506484de13 --- /dev/null +++ b/src/NHibernate.Test/TypesTest/AbstractTimeOnlyTypeFixture.cs @@ -0,0 +1,23 @@ +#if NET6_0_OR_GREATER +using System; +using System.Collections.Generic; +using System.Linq.Expressions; +using NHibernate.Type; + +namespace NHibernate.Test.TypesTest +{ + public class AbstractTimeOnlyTypeFixture : GenericTypeFixtureBase where TType : IType + { + protected override IReadOnlyList TestValues => [new(12, 13, 14), new(23, 59, 59), new(0, 0, 0)]; + protected override IEnumerable>> PropertiesToTestWithLinq + { + get + { + yield return (TimeOnly x) => x.Hour; + yield return (TimeOnly x) => x.Minute; + yield return (TimeOnly x) => x.Second; + } + } + } +} +#endif diff --git a/src/NHibernate.Test/TypesTest/AbstractTimeOnlyTypeWithScaleFixture.cs b/src/NHibernate.Test/TypesTest/AbstractTimeOnlyTypeWithScaleFixture.cs new file mode 100644 index 00000000000..133b838be0a --- /dev/null +++ b/src/NHibernate.Test/TypesTest/AbstractTimeOnlyTypeWithScaleFixture.cs @@ -0,0 +1,49 @@ +#if NET6_0_OR_GREATER +using System; +using System.Collections.Generic; +using System.Linq; + +using NHibernate.Mapping.ByCode; +using NHibernate.Type; + +namespace NHibernate.Test.TypesTest +{ + public abstract class AbstractTimeOnlyTypeWithScaleFixture : AbstractTimeOnlyTypeFixture where TType : IType + { + private readonly bool _setMaxScale; + protected AbstractTimeOnlyTypeWithScaleFixture(bool setMaxScale) + { + _setMaxScale = setMaxScale; + } + + /// + /// The resolution used when setMaxScale is true + /// + protected virtual long MaxTimestampResolutionInTicks => Dialect.TimestampResolutionInTicks; + + /// + /// Add fractional seconds to the test values when setMaxScale is true + /// + protected override IReadOnlyList TestValues => [.. base.TestValues.Select(x => _setMaxScale ? AdjustTestValueWithFractionalSeconds(x) : x)]; + + private TimeOnly AdjustTestValueWithFractionalSeconds(TimeOnly value) + { + value = new TimeOnly(value.Hour,value.Minute,value.Second); + var ticks = value.Ticks + MaxTimestampResolutionInTicks; + if (ticks + MaxTimestampResolutionInTicks > TimeOnly.MaxValue.Ticks) + { + ticks = value.Ticks - MaxTimestampResolutionInTicks; + } + return new TimeOnly(ticks); + } + + protected override void ConfigurePropertyMapping(IPropertyMapper propertyMapper) + { + if (_setMaxScale) + { + propertyMapper.Scale((short) Math.Floor(Math.Log10(TimeSpan.TicksPerSecond / MaxTimestampResolutionInTicks))); + } + } + } +} +#endif diff --git a/src/NHibernate.Test/TypesTest/DateOnlyAsDateTypeFixture.cs b/src/NHibernate.Test/TypesTest/DateOnlyAsDateTypeFixture.cs new file mode 100644 index 00000000000..3c6cf47be02 --- /dev/null +++ b/src/NHibernate.Test/TypesTest/DateOnlyAsDateTypeFixture.cs @@ -0,0 +1,26 @@ +#if NET6_0_OR_GREATER +using System; +using System.Collections.Generic; +using System.Linq.Expressions; +using NHibernate.Type; + +namespace NHibernate.Test.TypesTest +{ + public class DateOnlyAsDateTypeFixture : GenericTypeFixtureBase + { + protected override IReadOnlyList TestValues => + [ + DateOnly.FromDateTime(Sfi.ConnectionProvider.Driver.MinDate.AddDays(1)), + DateOnly.FromDateTime(DateTime.Now), + DateOnly.MaxValue.AddDays(-1) + ]; + + protected override IList>> PropertiesToTestWithLinq => + [ + (DateOnly x) => x.Year, + (DateOnly x) => x.Month, + (DateOnly x) => x.Day + ]; + } +} +#endif diff --git a/src/NHibernate.Test/TypesTest/GenericTypeFixtureBase.cs b/src/NHibernate.Test/TypesTest/GenericTypeFixtureBase.cs new file mode 100644 index 00000000000..d473d70be5e --- /dev/null +++ b/src/NHibernate.Test/TypesTest/GenericTypeFixtureBase.cs @@ -0,0 +1,279 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Linq.Expressions; +using NHibernate.Cfg; +using NHibernate.Linq; +using NHibernate.Linq.Visitors; +using NHibernate.Mapping.ByCode; +using NHibernate.Type; +using NUnit.Framework; + +namespace NHibernate.Test.TypesTest +{ + /// + /// Base class for fixtures testing individual types, created to avoid + /// code duplication in derived classes. + /// + public abstract class GenericTypeFixtureBase : TestCase where TType : IType + { + + protected override string MappingsAssembly + { + get { return "NHibernate.Test"; } + } + + protected override string[] Mappings + { + get + { + return new string[] { }; + } + } + + /// + /// Creates a set of test values of type . + /// + /// The test values + /// If the type is IComparable, make sure that the first value + /// doesn't become invalid when incremented by + protected abstract IReadOnlyList TestValues { get; } + + /// + /// Override in order to adjust the value of a property before comparisons. + /// May be necessary when dealing with expected precision losses + /// + /// The value to adjust + /// The adjusted value + protected virtual TProperty AdjustValue(TProperty value) => value; + + /// + /// Sub properties of which should be queryable using LINQ + /// + protected virtual IEnumerable>> PropertiesToTestWithLinq => Enumerable.Empty>>(); + + /// + /// Methods of which should be queryable using LINQ + /// + protected virtual IEnumerable>> MethodsToTestWithLinq => Enumerable.Empty>>(); + + [Test] + public virtual void CanPersist() + { + var testValues = GetAllTestValues(); + + Dictionary expectedValues = []; + using (var session = OpenSession()) + using (var trans = session.BeginTransaction()) + { + foreach (var testValue in testValues) + { + var entity = new TestEntity { TestProperty = testValue }; + session.Save(entity); + expectedValues[entity.Id] = testValue; + } + trans.Commit(); + } + + using (var session = OpenSession()) + using (var trans = session.BeginTransaction()) + { + foreach (var expectedValue in expectedValues) + { + var entity = session.Get(expectedValue.Key); + Assert.That(entity, Is.Not.Null); + Assert.That(AdjustValue(entity.TestProperty), Is.EqualTo(AdjustValue(expectedValue.Value))); + } + } + } + + [Test] + public void CanQuery() + { + var testValue = GetFirstTestValue(); + Guid id; + using (var session = OpenSession()) + using (var trans = session.BeginTransaction()) + { + var entity = new TestEntity { TestProperty = testValue }; + session.Save(entity); + id = entity.Id; + + trans.Commit(); + } + + using (var session = OpenSession()) + using (var trans = session.BeginTransaction()) + { + var param = Expression.Parameter(typeof(TestEntity)); + var prop = Expression.Property(param, nameof(TestEntity.TestProperty)); + var value = Expression.Constant(testValue); + var where = Expression.Lambda>(Expression.Equal(prop, value), param); + var entity = session.Query().Single(where); + Assert.That(entity, Is.Not.Null); + Assert.That(entity.Id, Is.EqualTo(id)); + } + } + + private TProperty GetFirstTestValue() + { + var testValues = TestValues; + if (testValues.Count == 0) + { + Assert.Ignore("No test values provided"); + } + return testValues[0]; + } + + private IReadOnlyList GetAllTestValues() + { + var testValues = TestValues; + if (TestValues.Count == 0) + { + Assert.Ignore("No test values provided"); + } + return testValues; + } + + [Test] + public virtual void CanCompare() + { + if (!typeof(IComparable).IsAssignableFrom(typeof(TProperty))) + { + Assert.Ignore("Not IComparable"); + } + var testValues = GetAllTestValues(); + + if (testValues.Count < 2) + { + Assert.Fail("At least 2 test values required to test comparison"); + } + var testValue = testValues[0]; + var biggerTestValue = testValues[1]; + if (((IComparable) biggerTestValue).CompareTo(testValue) <= 0) + { + Assert.Fail("The second test value must be greater than the first"); + return; + } + Guid id; + using (var session = OpenSession()) + using (var trans = session.BeginTransaction()) + { + var entity = new TestEntity { TestProperty = testValue }; + session.Save(entity); + id = entity.Id; + entity = new TestEntity { TestProperty = biggerTestValue }; + session.Save(entity); + trans.Commit(); + } + + using (var session = OpenSession()) + using (var trans = session.BeginTransaction()) + { + var param = Expression.Parameter(typeof(TestEntity)); + var prop = Expression.Property(param, nameof(TestEntity.TestProperty)); + var smallerValue = Expression.Constant(testValue, typeof(TProperty)); + var biggerValue = Expression.Constant(biggerTestValue, typeof(TProperty)); + var smallerWhere = Expression.Lambda>(Expression.LessThan(prop, biggerValue), param); + var biggerWhere = Expression.Lambda>(Expression.GreaterThan(prop, smallerValue), param); + var smaller = session.Query().Single(smallerWhere); + var bigger = session.Query().Single(biggerWhere); + Assert.That(smaller, Is.Not.Null); + Assert.That(smaller.Id, Is.EqualTo(id)); + Assert.That(bigger, Is.Not.Null); + Assert.That(bigger.Id, Is.Not.EqualTo(id)); + } + } + + [Test] + public virtual void CanQueryProperties() + { + if (PropertiesToTestWithLinq?.Any() != true) + { + Assert.Ignore(); + } + + var testValue = GetFirstTestValue(); + Guid id; + using (var session = OpenSession()) + using (var trans = session.BeginTransaction()) + { + var entity = new TestEntity { TestProperty = testValue }; + session.Save(entity); + id = entity.Id; + trans.Commit(); + } + + using (var session = OpenSession()) + using (var trans = session.BeginTransaction()) + { + foreach (var property in PropertiesToTestWithLinq) + { + var param = Expression.Parameter(typeof(TestEntity)); + var body = property.Body; + if (body is UnaryExpression unaryExpression) + { + body = unaryExpression.Operand; + } + var member = body as MemberExpression; + if (member is null) + { + Assert.Fail(body + " did not expose a member"); + } + var prop = Expression.Property(param, nameof(TestEntity.TestProperty)); + var value = property.Compile()(testValue); + var where = Expression.Lambda>(Expression.Equal(body.Replace(property.Parameters[0], prop), Expression.Constant(value)), param); + TestEntity entity = null; + Assert.DoesNotThrow(() => entity = session.Query().FirstOrDefault(where), "Unable to query property " + member.Member.Name); + Assert.That(entity, Is.Not.Null, "Unable to query property " + member.Member.Name); + } + } + } + + protected override void AddMappings(Configuration configuration) + { + var mapper = new ModelMapper(); + + mapper.Class(m => + { + m.Table("TestEntity"); + m.EntityName("TestEntity"); + m.Id(p => p.Id, p => p.Generator(Generators.Guid)); + m.Property(p => p.TestProperty, + p => + { + p.Type(); + ConfigurePropertyMapping(p); + } + ); + }); + + var mapping = mapper.CompileMappingForAllExplicitlyAddedEntities(); + configuration.AddMapping(mapping); + } + + protected virtual void ConfigurePropertyMapping(IPropertyMapper propertyMapper) { } + + protected override void OnTearDown() + { + base.OnTearDown(); + + using var s = OpenSession(); + using var t = s.BeginTransaction(); + s.Query().Delete(); + t.Commit(); + } + public class TestEntity + { + public virtual Guid Id { get; set; } + public virtual TProperty TestProperty { get; set; } + } + + public class TypeConfiguration + { + public object Parameters { get; set; } + public short? Scale { get; set; } + public short? Precision { get; set; } + } + } +} diff --git a/src/NHibernate.Test/TypesTest/TimeOnlyAsDateTimeTypeFixture.cs b/src/NHibernate.Test/TypesTest/TimeOnlyAsDateTimeTypeFixture.cs new file mode 100644 index 00000000000..b3aba411508 --- /dev/null +++ b/src/NHibernate.Test/TypesTest/TimeOnlyAsDateTimeTypeFixture.cs @@ -0,0 +1,33 @@ +#if NET6_0_OR_GREATER +using System; +using System.Collections.Generic; +using System.Linq; +using NHibernate.Mapping.ByCode; +using NHibernate.Type; +using NUnit.Framework; + +namespace NHibernate.Test.TypesTest +{ + [TestFixture(null, false)] + [TestFixture("1900-01-01", false)] + [TestFixture(null, true)] + public class TimeOnlyAsDateTimeTypeFixture : AbstractTimeOnlyTypeWithScaleFixture + { + private readonly string _baseDate; + + public TimeOnlyAsDateTimeTypeFixture(string baseDate, bool setMaxScale) : base(setMaxScale) + { + _baseDate = baseDate; + } + + protected override void ConfigurePropertyMapping(IPropertyMapper propertyMapper) + { + base.ConfigurePropertyMapping(propertyMapper); + if (_baseDate != null) + { + propertyMapper.Type(new { BaseDateValue = _baseDate }); + } + } + } +} +#endif diff --git a/src/NHibernate.Test/TypesTest/TimeOnlyAsTicksTypeFixture.cs b/src/NHibernate.Test/TypesTest/TimeOnlyAsTicksTypeFixture.cs new file mode 100644 index 00000000000..7e7ca2bfff9 --- /dev/null +++ b/src/NHibernate.Test/TypesTest/TimeOnlyAsTicksTypeFixture.cs @@ -0,0 +1,26 @@ +#if NET6_0_OR_GREATER +using System; +using System.Collections.Generic; +using System.Linq.Expressions; + +using NHibernate.Type; + +using NUnit.Framework; + +namespace NHibernate.Test.TypesTest +{ + + [TestFixture(false)] + [TestFixture(true)] + public class TimeOnlyAsTicksTypeFixture : AbstractTimeOnlyTypeWithScaleFixture + { + public TimeOnlyAsTicksTypeFixture(bool setMaxScale) : base(setMaxScale) + { + } + + protected override long MaxTimestampResolutionInTicks => 1L; + + protected override IEnumerable>> PropertiesToTestWithLinq => null; + } +} +#endif diff --git a/src/NHibernate.Test/TypesTest/TimeOnlyAsTimeTypeFixture.cs b/src/NHibernate.Test/TypesTest/TimeOnlyAsTimeTypeFixture.cs new file mode 100644 index 00000000000..8a214a2bd95 --- /dev/null +++ b/src/NHibernate.Test/TypesTest/TimeOnlyAsTimeTypeFixture.cs @@ -0,0 +1,30 @@ +#if NET6_0_OR_GREATER +using NHibernate.Mapping.ByCode; +using NHibernate.Type; +using NUnit.Framework; + +namespace NHibernate.Test.TypesTest +{ + [TestFixture(null, false)] + [TestFixture("1900-01-01", false)] + [TestFixture(null, true)] + public class TimeOnlyAsTimeTypeFixture : AbstractTimeOnlyTypeWithScaleFixture + { + private readonly string _baseDate; + + public TimeOnlyAsTimeTypeFixture(string baseDate, bool setMaxScale) : base(setMaxScale) + { + _baseDate = baseDate; + } + + protected override void ConfigurePropertyMapping(IPropertyMapper propertyMapper) + { + base.ConfigurePropertyMapping(propertyMapper); + if (_baseDate != null) + { + propertyMapper.Type(new { BaseDateValue = _baseDate }); + } + } + } +} +#endif diff --git a/src/NHibernate/Linq/Functions/DateTimePropertiesHqlGenerator.cs b/src/NHibernate/Linq/Functions/DateTimePropertiesHqlGenerator.cs index e5b1e95d703..a67b06c87cb 100644 --- a/src/NHibernate/Linq/Functions/DateTimePropertiesHqlGenerator.cs +++ b/src/NHibernate/Linq/Functions/DateTimePropertiesHqlGenerator.cs @@ -1,7 +1,6 @@ using System; using System.Linq.Expressions; using System.Reflection; -using NHibernate.Engine; using NHibernate.Hql.Ast; using NHibernate.Linq.Visitors; using NHibernate.Util; @@ -21,7 +20,7 @@ public DateTimePropertiesHqlGenerator() ReflectHelper.GetProperty((DateTime x) => x.Minute), ReflectHelper.GetProperty((DateTime x) => x.Second), ReflectHelper.GetProperty((DateTime x) => x.Date), - + ReflectHelper.GetProperty((DateTimeOffset x) => x.Year), ReflectHelper.GetProperty((DateTimeOffset x) => x.Month), ReflectHelper.GetProperty((DateTimeOffset x) => x.Day), @@ -29,7 +28,17 @@ public DateTimePropertiesHqlGenerator() ReflectHelper.GetProperty((DateTimeOffset x) => x.Minute), ReflectHelper.GetProperty((DateTimeOffset x) => x.Second), ReflectHelper.GetProperty((DateTimeOffset x) => x.Date), - }; + +#if NET6_0_OR_GREATER + ReflectHelper.GetProperty((DateOnly x) => x.Year), + ReflectHelper.GetProperty((DateOnly x) => x.Month), + ReflectHelper.GetProperty((DateOnly x) => x.Day), + + ReflectHelper.GetProperty((TimeOnly x) => x.Hour), + ReflectHelper.GetProperty((TimeOnly x) => x.Minute), + ReflectHelper.GetProperty((TimeOnly x) => x.Second), +#endif + }; } public override HqlTreeNode BuildHql(MemberInfo member, Expression expression, HqlTreeBuilder treeBuilder, IHqlExpressionVisitor visitor) diff --git a/src/NHibernate/NHibernateUtil.cs b/src/NHibernate/NHibernateUtil.cs index 226d26bf67c..2cb1ff3e15d 100644 --- a/src/NHibernate/NHibernateUtil.cs +++ b/src/NHibernate/NHibernateUtil.cs @@ -137,6 +137,12 @@ public static IType GuessType(System.Type type) /// public static readonly DateType Date = new DateType(); +#if NET6_0_OR_GREATER + /// + /// NHibernate DateOnlyAsDate type + /// + public static readonly DateOnlyAsDateType DateOnlyAsDate = new(); +#endif /// /// NHibernate local date type /// @@ -244,6 +250,22 @@ public static IType GuessType(System.Type type) [Obsolete("Use DateTime instead.")] public static readonly TimestampType Timestamp = new TimestampType(); +#if NET6_0_OR_GREATER + /// + /// NHibernate TimeOnlyAsDateTime type + /// + public static readonly TimeOnlyAsDateTimeType TimeOnlyAsDateTime = new(); + + /// + /// NHibernate TimeOnlyAsTicks type + /// + public static readonly TimeOnlyAsTicksType TimeOnlyAsTicks = new(); + + /// + /// NHibernate TimeOnlyAsTime type + /// + public static readonly TimeOnlyAsTimeType TimeOnlyAsTime = new(); +#endif /// /// NHibernate Timestamp type, seeded db side. /// diff --git a/src/NHibernate/Type/AbstractDateOnlyType.cs b/src/NHibernate/Type/AbstractDateOnlyType.cs new file mode 100644 index 00000000000..644aed11186 --- /dev/null +++ b/src/NHibernate/Type/AbstractDateOnlyType.cs @@ -0,0 +1,88 @@ +#if NET6_0_OR_GREATER +using System; +using System.Data.Common; +using System.Globalization; +using NHibernate.Engine; +using NHibernate.SqlTypes; + +namespace NHibernate.Type +{ + /// + /// Base class for DateOnly types. + /// + [Serializable] + public abstract class AbstractDateOnlyType : PrimitiveType + { + protected AbstractDateOnlyType(SqlType sqlType) : base(sqlType) + { + } + + public override string Name => + GetType().Name.EndsWith("Type", StringComparison.Ordinal) + ? GetType().Name[..^4] + : GetType().Name; + + public override System.Type ReturnedClass => typeof(DateOnly); + + public override System.Type PrimitiveClass => typeof(DateOnly); + + public override object DefaultValue => DateOnly.MinValue; + + /// + public override object Get(DbDataReader rs, int index, ISessionImplementor session) + { + try + { + return GetDateOnlyFromReader(rs, index, session); + } + catch (Exception ex) when (ex is not FormatException) + { + throw new FormatException(string.Format("Input string '{0}' was not in the correct format.", rs[index]), ex); + } + } + + ///// + //public override object Get(DbDataReader rs, string name, ISessionImplementor session) + //{ + // return Get(rs, rs.GetOrdinal(name), session); + //} + + /// + public override void Set(DbCommand st, object value, int index, ISessionImplementor session) + { + st.Parameters[index].Value = GetParameterValueToSet((DateOnly) value,session); + } + + /// + /// Get the DateOnly value from the at index + /// + /// + /// + /// + /// + protected abstract DateOnly GetDateOnlyFromReader(DbDataReader rs, int index, ISessionImplementor session); + + /// + /// Convert into the which will be set on the parameter + /// + /// + /// + /// + protected abstract TParameter GetParameterValueToSet(DateOnly dateOnly, ISessionImplementor session); + + public override bool IsEqual(object x, object y) + { + if (ReferenceEquals(x, y)) return true; + if (x == null || y == null) return false; + return ((DateOnly) x).Equals((DateOnly) y); + } + + public override int GetHashCode(object x) => ((DateOnly) x).GetHashCode(); + + /// + public override string ToLoggableString(object value, ISessionFactoryImplementor factory) => + value == null ? null : ((DateOnly) value).ToString("yyyy-MM-dd", CultureInfo.InvariantCulture); + + } +} +#endif diff --git a/src/NHibernate/Type/AbstractTimeOnlyType.cs b/src/NHibernate/Type/AbstractTimeOnlyType.cs new file mode 100644 index 00000000000..74624533caa --- /dev/null +++ b/src/NHibernate/Type/AbstractTimeOnlyType.cs @@ -0,0 +1,134 @@ +#if NET6_0_OR_GREATER +using System; +using System.Data.Common; +using System.Globalization; +using NHibernate.Engine; +using NHibernate.SqlTypes; + +namespace NHibernate.Type +{ + /// + /// Base class for TimeOnly types. + /// Fractional seconds precision: + /// null => preserve full precision (no truncation) + /// 0..6 => truncate (floor) to that many fractional digits + /// >=7 => preserve full precision + /// + [Serializable] + public abstract class AbstractTimeOnlyType : PrimitiveType + { + private static readonly int[] TicksPerPrecision = { 10_000_000, 1_000_000, 100_000, 10_000, 1_000, 100, 10 }; + private readonly int _ticksForPrecision; + private readonly bool _truncate; + private readonly byte? _fractionalSecondsPrecision; + + protected AbstractTimeOnlyType(byte? fractionalSecondsPrecision, SqlType sqlType) : base(sqlType) + { + _fractionalSecondsPrecision = fractionalSecondsPrecision; + if (fractionalSecondsPrecision is >= 0 and <= 6) + { + _ticksForPrecision = TicksPerPrecision[fractionalSecondsPrecision.Value]; + _truncate = true; + } + else + { + // null or >=7 => full precision, no truncation + _ticksForPrecision = 1; + _truncate = false; + } + } + + + public override string Name => GetType().Name.EndsWith("Type", StringComparison.Ordinal) + ? GetType().Name[..^4] + : GetType().Name; + + public override System.Type ReturnedClass => typeof(TimeOnly); + + public override System.Type PrimitiveClass => typeof(TimeOnly); + + public override object DefaultValue => TimeOnly.MinValue; + + /// + /// Truncate (floor) fractional seconds beyond declared precision. + /// Override to change behavior (e.g., implement rounding). + /// + protected virtual TimeOnly AdjustTimeOnly(TimeOnly timeOnly) + { + if (!_truncate) + return timeOnly; + + long ticks = timeOnly.Ticks; + long remainder = ticks % _ticksForPrecision; + if (remainder == 0) + return timeOnly; + + return new TimeOnly(ticks - remainder); + } + + /// + /// Convert into the which will be set on the parameter + /// + /// + /// + /// + protected abstract TParameter GetParameterValueToSet(TimeOnly timeOnly, ISessionImplementor session); + + /// + public override void Set(DbCommand st, object value, int index, ISessionImplementor session) + { + st.Parameters[index].Value = GetParameterValueToSet(AdjustTimeOnly((TimeOnly) value), session); + } + + /// + public override object Get(DbDataReader rs, int index, ISessionImplementor session) + { + try + { + return AdjustTimeOnly(GetTimeOnlyFromReader(rs, index, session)); + } + catch (Exception ex) when (ex is not FormatException) + { + throw new FormatException(string.Format("Input string '{0}' was not in the correct format.", rs[index]), ex); + } + } + + /// + /// Get the value from the at index + /// + /// + /// + /// + /// + protected abstract TimeOnly GetTimeOnlyFromReader(DbDataReader rs, int index, ISessionImplementor session); + + public override bool IsEqual(object x, object y) + { + if (ReferenceEquals(x, y)) return true; + if (x == null || y == null) return false; + + var t1 = (TimeOnly) x; + var t2 = (TimeOnly) y; + + // Fast path: compare essential components first + if (t1.Hour != t2.Hour || t1.Minute != t2.Minute || t1.Second != t2.Second) + return false; + + // Precision handling + if (!_fractionalSecondsPrecision.HasValue) + return t1 == t2; // full precision exact match + + if (_fractionalSecondsPrecision == 0) + return true; // up to seconds already matched + + return AdjustTimeOnly(t1) == AdjustTimeOnly(t2); + } + + public override int GetHashCode(object x) => AdjustTimeOnly((TimeOnly) x).GetHashCode(); + + /// + public override string ToLoggableString(object value, ISessionFactoryImplementor factory) => + value == null ? null : ((TimeOnly) value).ToString("HH:mm:ss.fffffff", CultureInfo.InvariantCulture); + } +} +#endif diff --git a/src/NHibernate/Type/DateOnlyAsDateType.cs b/src/NHibernate/Type/DateOnlyAsDateType.cs new file mode 100644 index 00000000000..e45fc857fac --- /dev/null +++ b/src/NHibernate/Type/DateOnlyAsDateType.cs @@ -0,0 +1,37 @@ +#if NET6_0_OR_GREATER +using System; +using System.Data; +using System.Data.Common; +using NHibernate.Engine; +using NHibernate.SqlTypes; + +namespace NHibernate.Type +{ + /// + /// Maps a property to a column + /// + [Serializable] + public class DateOnlyAsDateType : AbstractDateOnlyType + { + /// + /// Default constructor. + /// + public DateOnlyAsDateType() : base(SqlTypeFactory.Date) + { + } + + protected override DateOnly GetDateOnlyFromReader(DbDataReader rs, int index, ISessionImplementor session) + { + return DateOnly.FromDateTime(rs.GetDateTime(index)); + } + + protected override DateTime GetParameterValueToSet(DateOnly dateOnly, ISessionImplementor session) => + dateOnly.ToDateTime(TimeOnly.MinValue); + + + /// + public override string ObjectToSQLString(object value, Dialect.Dialect dialect) => + "'" + ((DateOnly) value).ToString("yyyy-MM-dd") + "'"; + } +} +#endif diff --git a/src/NHibernate/Type/TimeOnlyAsDateTimeType.cs b/src/NHibernate/Type/TimeOnlyAsDateTimeType.cs new file mode 100644 index 00000000000..70ebdfb599a --- /dev/null +++ b/src/NHibernate/Type/TimeOnlyAsDateTimeType.cs @@ -0,0 +1,67 @@ +#if NET6_0_OR_GREATER +using System; +using System.Collections.Generic; +using System.Data.Common; +using System.Globalization; +using NHibernate.Engine; +using NHibernate.SqlTypes; +using NHibernate.UserTypes; + +namespace NHibernate.Type +{ + /// + /// Maps a Property to a DateTime column that only stores the + /// time part of the DateTime as significant. + /// + /// + /// + /// This defaults the Date to "1753-01-01" - which should not matter because + /// using this Type indicates that you don't care about the Date portion of the DateTime. + /// However, if you need to adjust this base value, you can specify the parameter 'BaseDateValue' on the type mapping, + /// using the date format 'yyyy-MM-dd' + /// + /// + [Serializable] + public class TimeOnlyAsDateTimeType : AbstractTimeOnlyType, IParameterizedType + { + private DateTime _baseDateValue = new(1753, 01, 01); + private readonly string _sqlFormat; + /// + /// Default constructor. Sets the fractional seconds precision (scale) to 0 + /// + public TimeOnlyAsDateTimeType() : this(0) + { + } + + /// + /// Constructor for specifying a time with a scale. + /// + /// The sql type to use for the type. + public TimeOnlyAsDateTimeType(byte fractionalSecondsPrecision) : base(fractionalSecondsPrecision, SqlTypeFactory.GetDateTime(fractionalSecondsPrecision)) + { + _sqlFormat = "yyyy-MM-dd HH:mm:ss" + (fractionalSecondsPrecision > 0 ? "." + new string('F', Math.Min(7, (int) fractionalSecondsPrecision)) : ""); + } + + protected override TimeOnly GetTimeOnlyFromReader(DbDataReader rs, int index, ISessionImplementor session) => + TimeOnly.FromTimeSpan(rs.GetDateTime(index).TimeOfDay); + + protected override DateTime GetParameterValueToSet(TimeOnly timeOnly, ISessionImplementor session) => + GetDateTimeFromTimeOnly(timeOnly); + + private DateTime GetDateTimeFromTimeOnly(TimeOnly timeOnly) => + _baseDateValue + timeOnly.ToTimeSpan(); + + void IParameterizedType.SetParameterValues(IDictionary parameters) + { + if (parameters?.TryGetValue("BaseDateValue", out var stringVal) == true + && !string.IsNullOrEmpty(stringVal)) + { + _baseDateValue = DateTime.ParseExact(stringVal, "yyyy-MM-dd", CultureInfo.InvariantCulture).Date; + } + } + + public override string ObjectToSQLString(object value, Dialect.Dialect dialect) => + "'" + GetDateTimeFromTimeOnly(AdjustTimeOnly((TimeOnly) value)).ToString(_sqlFormat) + "'"; + } +} +#endif diff --git a/src/NHibernate/Type/TimeOnlyAsTicksType.cs b/src/NHibernate/Type/TimeOnlyAsTicksType.cs new file mode 100644 index 00000000000..9715b6e52ec --- /dev/null +++ b/src/NHibernate/Type/TimeOnlyAsTicksType.cs @@ -0,0 +1,54 @@ +#if NET6_0_OR_GREATER +using System; +using System.Collections.Generic; +using System.Data; +using System.Data.Common; +using System.Linq.Expressions; +using System.Reflection; +using NHibernate.Engine; +using NHibernate.Hql.Ast; +using NHibernate.Linq.Functions; +using NHibernate.Linq.Visitors; +using NHibernate.SqlTypes; +using NHibernate.Util; + +namespace NHibernate.Type +{ + + /// + /// Maps a property to a column. + /// The value persisted is the Ticks property of the TimeOnly value. + /// + [Serializable] + public class TimeOnlyAsTicksType : AbstractTimeOnlyType + { + private static MemberInfo[] _supportedProperties = new MemberInfo[]{ + ReflectHelper.GetProperty((TimeOnly x) => x.Hour), + ReflectHelper.GetProperty((TimeOnly x) => x.Minute), + ReflectHelper.GetProperty((TimeOnly x) => x.Second) + }; + + /// + /// Default constructor. Sets the fractional seconds precision (scale) to 0 + /// + public TimeOnlyAsTicksType() : this(0) + { + } + + /// + /// Constructor for specifying a fractional seconds precision (scale). + /// + /// The fractional seconds precision. Any value beyond 7 is pointless, since it's the maximum precision allowed by .NET + public TimeOnlyAsTicksType(byte fractionalSecondsPrecision) : base(fractionalSecondsPrecision, SqlTypeFactory.Int64) + { + } + + protected override TimeOnly GetTimeOnlyFromReader(DbDataReader rs, int index, ISessionImplementor session) => new(rs.GetInt64(index)); + + protected override long GetParameterValueToSet(TimeOnly timeOnly, ISessionImplementor session) => timeOnly.Ticks; + + public override string ObjectToSQLString(object value, Dialect.Dialect dialect) => + AdjustTimeOnly((TimeOnly) value).Ticks.ToString(); + } +} +#endif diff --git a/src/NHibernate/Type/TimeOnlyAsTimeType.cs b/src/NHibernate/Type/TimeOnlyAsTimeType.cs new file mode 100644 index 00000000000..4b622cd2d71 --- /dev/null +++ b/src/NHibernate/Type/TimeOnlyAsTimeType.cs @@ -0,0 +1,78 @@ +#if NET6_0_OR_GREATER +using System; +using System.Collections.Generic; +using System.Data; +using System.Data.Common; +using System.Globalization; +using NHibernate.Engine; +using NHibernate.SqlTypes; +using NHibernate.UserTypes; + +namespace NHibernate.Type +{ + /// + /// Maps a Property to a column. + /// + /// + /// + /// Some DB drivers will use a value for the column, whereas + /// others will use a full . In the latter case, the date part defaults to + /// to "1753-01-01" - which should not matter because using this Type indicates that you don't care about the Date portion of the DateTime. + /// However, if you need to adjust this base value, you can specify the parameter 'BaseDateValue' on the type mapping, + /// using the date format 'yyyy-MM-dd' + /// + /// + [Serializable] + public class TimeOnlyAsTimeType : AbstractTimeOnlyType, IParameterizedType + { + private DateTime BaseDateValue = new(1753, 01, 01); + private readonly string _sqlFormat; + + /// + /// Default constructor. Sets the fractional seconds precision (scale) to 0 + /// + public TimeOnlyAsTimeType() : this(0) + { + } + + /// + /// Constructor for specifying a fractional seconds precision (scale). + /// + /// The fractional seconds precision. Any value beyond 7 is pointless, since it's the maximum precision allowed by .NET + public TimeOnlyAsTimeType(byte fractionalSecondsPrecision) : base(fractionalSecondsPrecision, SqlTypeFactory.GetTime(fractionalSecondsPrecision)) + { + _sqlFormat = "HH:mm:ss" + (fractionalSecondsPrecision > 0 ? "." + new string('F', Math.Min(7, (int) fractionalSecondsPrecision)) : ""); + } + + protected override TimeOnly GetTimeOnlyFromReader(DbDataReader rs, int index, ISessionImplementor session) + { + if (rs.GetFieldType(index) == typeof(TimeSpan)) //For those dialects where DbType.Time means TimeSpan. + { + return new TimeOnly(((TimeSpan)rs[index]).Ticks); + } + + var dbValue = rs.GetDateTime(index); + return TimeOnly.FromTimeSpan(dbValue.TimeOfDay); + } + + protected override object GetParameterValueToSet(TimeOnly timeOnly, ISessionImplementor session) + { + if (session.Factory.ConnectionProvider.Driver.RequiresTimeSpanForTime) + return timeOnly.ToTimeSpan(); + else + return BaseDateValue + timeOnly.ToTimeSpan(); + } + + void IParameterizedType.SetParameterValues(IDictionary parameters) + { + if (parameters.TryGetValue("BaseDateValue", out var stringVal) && !string.IsNullOrEmpty(stringVal)) + { + BaseDateValue = DateTime.ParseExact(stringVal, "yyyy-MM-dd", CultureInfo.InvariantCulture).Date; + } + } + + public override string ObjectToSQLString(object value, Dialect.Dialect dialect) => + "'" + (AdjustTimeOnly((TimeOnly) value)).ToString(_sqlFormat) + "'"; + } +} +#endif diff --git a/src/NHibernate/Type/TypeFactory.cs b/src/NHibernate/Type/TypeFactory.cs index 0fd384d25c9..da99061e1b2 100644 --- a/src/NHibernate/Type/TypeFactory.cs +++ b/src/NHibernate/Type/TypeFactory.cs @@ -343,6 +343,15 @@ private static void RegisterDefaultNetTypes() // object needs to have both class and serializable setup before it can // be created. RegisterType(typeof (Object), NHibernateUtil.Object, new[] {"object"}); + + +#if NET6_0_OR_GREATER + RegisterType(typeof(DateOnly), NHibernateUtil.DateOnlyAsDate, new[] { "dateonly", "dateonlyasdate" }); + + RegisterType(typeof(TimeOnly), NHibernateUtil.TimeOnlyAsTime, new[] { "timeonly", "timeonlyastime" }, + s => GetType(NHibernateUtil.TimeOnlyAsTime, s, scale => new TimeOnlyAsTimeType((byte) scale)), + false); +#endif } /// @@ -397,6 +406,17 @@ private static void RegisterBuiltInTypes() l => GetType(NHibernateUtil.Serializable, l, len => new SerializableType(typeof (object), SqlTypeFactory.GetBinary(len)))); + +#if NET6_0_OR_GREATER + RegisterType(NHibernateUtil.TimeOnlyAsDateTime, new[] { "timeonlyasdatetime" }, + s => GetType(NHibernateUtil.TimeOnlyAsDateTime, s, scale => new TimeOnlyAsDateTimeType((byte) scale)), + false); + + RegisterType(NHibernateUtil.TimeOnlyAsTicks, new[] { "timeonlyasticks" }, + s => GetType(NHibernateUtil.TimeOnlyAsTicks, s, scale => new TimeOnlyAsTicksType((byte) scale)), + false); +#endif + } private static ICollectionTypeFactory CollectionTypeFactory => @@ -916,6 +936,38 @@ public static NullableType GetTimeType(byte fractionalSecondsPrecision) var key = GetKeyForLengthOrScaleBased(NHibernateUtil.Time.Name, fractionalSecondsPrecision); return (NullableType)typeByTypeOfName.GetOrAdd(key, k => new TimeType(SqlTypeFactory.GetTime(fractionalSecondsPrecision))); } +#if NET6_0_OR_GREATER + /// + /// Gets a with desired fractional seconds precision. + /// + /// The NHibernate type. + public static NullableType GetTimeOnlyAsDateTimeType(byte fractionalSecondsPrecision) + { + var key = GetKeyForLengthOrScaleBased(NHibernateUtil.TimeOnlyAsDateTime.Name, fractionalSecondsPrecision); + return (NullableType) typeByTypeOfName.GetOrAdd(key, k => new TimeOnlyAsDateTimeType(fractionalSecondsPrecision)); + } + + /// + /// Gets a with desired fractional seconds precision. + /// + /// The NHibernate type. + public static NullableType GetTimeOnlyAsTicksType(byte fractionalSecondsPrecision) + { + var key = GetKeyForLengthOrScaleBased(NHibernateUtil.TimeOnlyAsTicks.Name, fractionalSecondsPrecision); + return (NullableType) typeByTypeOfName.GetOrAdd(key, k => new TimeOnlyAsTimeType(fractionalSecondsPrecision)); + } + + /// + /// Gets a with desired fractional seconds precision. + /// + /// The fractional seconds precision. + /// The NHibernate type. + public static NullableType GetTimeOnlyAsTimeType(byte fractionalSecondsPrecision) + { + var key = GetKeyForLengthOrScaleBased(NHibernateUtil.TimeOnlyAsTime.Name, fractionalSecondsPrecision); + return (NullableType) typeByTypeOfName.GetOrAdd(key, k => new TimeOnlyAsTimeType(fractionalSecondsPrecision)); + } +#endif // Association Types From 5d6b354342499c672de019b98eab8ab2c8f90a17 Mon Sep 17 00:00:00 2001 From: Gunnar Liljas Date: Sun, 2 Nov 2025 21:15:09 +0100 Subject: [PATCH 2/3] Apply suggestions from code review MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Frédéric Delaporte <12201973+fredericDelaporte@users.noreply.github.com> --- src/NHibernate.Test/TypesTest/GenericTypeFixtureBase.cs | 3 ++- src/NHibernate.Test/TypesTest/TimeOnlyAsTicksTypeFixture.cs | 1 - src/NHibernate/Type/AbstractDateOnlyType.cs | 5 ++--- src/NHibernate/Type/AbstractTimeOnlyType.cs | 5 ++--- src/NHibernate/Type/DateOnlyAsDateType.cs | 3 +-- src/NHibernate/Type/TimeOnlyAsDateTimeType.cs | 2 +- src/NHibernate/Type/TimeOnlyAsTicksType.cs | 3 +-- src/NHibernate/Type/TimeOnlyAsTimeType.cs | 4 ++-- src/NHibernate/Type/TypeFactory.cs | 4 ++-- 9 files changed, 13 insertions(+), 17 deletions(-) diff --git a/src/NHibernate.Test/TypesTest/GenericTypeFixtureBase.cs b/src/NHibernate.Test/TypesTest/GenericTypeFixtureBase.cs index d473d70be5e..7e1fbe6a8db 100644 --- a/src/NHibernate.Test/TypesTest/GenericTypeFixtureBase.cs +++ b/src/NHibernate.Test/TypesTest/GenericTypeFixtureBase.cs @@ -224,7 +224,8 @@ public virtual void CanQueryProperties() var value = property.Compile()(testValue); var where = Expression.Lambda>(Expression.Equal(body.Replace(property.Parameters[0], prop), Expression.Constant(value)), param); TestEntity entity = null; - Assert.DoesNotThrow(() => entity = session.Query().FirstOrDefault(where), "Unable to query property " + member.Member.Name); + Assert.That(() => entity = session.Query().FirstOrDefault(where), Throws.Nothing, + "Unable to query property " + member.Member.Name); Assert.That(entity, Is.Not.Null, "Unable to query property " + member.Member.Name); } } diff --git a/src/NHibernate.Test/TypesTest/TimeOnlyAsTicksTypeFixture.cs b/src/NHibernate.Test/TypesTest/TimeOnlyAsTicksTypeFixture.cs index 7e7ca2bfff9..cb837a09fb1 100644 --- a/src/NHibernate.Test/TypesTest/TimeOnlyAsTicksTypeFixture.cs +++ b/src/NHibernate.Test/TypesTest/TimeOnlyAsTicksTypeFixture.cs @@ -9,7 +9,6 @@ namespace NHibernate.Test.TypesTest { - [TestFixture(false)] [TestFixture(true)] public class TimeOnlyAsTicksTypeFixture : AbstractTimeOnlyTypeWithScaleFixture diff --git a/src/NHibernate/Type/AbstractDateOnlyType.cs b/src/NHibernate/Type/AbstractDateOnlyType.cs index 644aed11186..37858407d48 100644 --- a/src/NHibernate/Type/AbstractDateOnlyType.cs +++ b/src/NHibernate/Type/AbstractDateOnlyType.cs @@ -37,7 +37,7 @@ public override object Get(DbDataReader rs, int index, ISessionImplementor sessi } catch (Exception ex) when (ex is not FormatException) { - throw new FormatException(string.Format("Input string '{0}' was not in the correct format.", rs[index]), ex); + throw new FormatException(string.Format("Input '{0}' was not convertible to DateOnly.", rs[index]), ex); } } @@ -50,7 +50,7 @@ public override object Get(DbDataReader rs, int index, ISessionImplementor sessi /// public override void Set(DbCommand st, object value, int index, ISessionImplementor session) { - st.Parameters[index].Value = GetParameterValueToSet((DateOnly) value,session); + st.Parameters[index].Value = GetParameterValueToSet((DateOnly) value, session); } /// @@ -82,7 +82,6 @@ public override bool IsEqual(object x, object y) /// public override string ToLoggableString(object value, ISessionFactoryImplementor factory) => value == null ? null : ((DateOnly) value).ToString("yyyy-MM-dd", CultureInfo.InvariantCulture); - } } #endif diff --git a/src/NHibernate/Type/AbstractTimeOnlyType.cs b/src/NHibernate/Type/AbstractTimeOnlyType.cs index 74624533caa..28541548bc7 100644 --- a/src/NHibernate/Type/AbstractTimeOnlyType.cs +++ b/src/NHibernate/Type/AbstractTimeOnlyType.cs @@ -38,7 +38,6 @@ protected AbstractTimeOnlyType(byte? fractionalSecondsPrecision, SqlType sqlType } } - public override string Name => GetType().Name.EndsWith("Type", StringComparison.Ordinal) ? GetType().Name[..^4] : GetType().Name; @@ -50,7 +49,7 @@ protected AbstractTimeOnlyType(byte? fractionalSecondsPrecision, SqlType sqlType public override object DefaultValue => TimeOnly.MinValue; /// - /// Truncate (floor) fractional seconds beyond declared precision. + /// Truncate (floor) fractional seconds according to the declared precision. /// Override to change behavior (e.g., implement rounding). /// protected virtual TimeOnly AdjustTimeOnly(TimeOnly timeOnly) @@ -89,7 +88,7 @@ public override object Get(DbDataReader rs, int index, ISessionImplementor sessi } catch (Exception ex) when (ex is not FormatException) { - throw new FormatException(string.Format("Input string '{0}' was not in the correct format.", rs[index]), ex); + throw new FormatException(string.Format("Input '{0}' was not convertible to TimeOnly.", rs[index]), ex); } } diff --git a/src/NHibernate/Type/DateOnlyAsDateType.cs b/src/NHibernate/Type/DateOnlyAsDateType.cs index e45fc857fac..b15d8a075c5 100644 --- a/src/NHibernate/Type/DateOnlyAsDateType.cs +++ b/src/NHibernate/Type/DateOnlyAsDateType.cs @@ -28,10 +28,9 @@ protected override DateOnly GetDateOnlyFromReader(DbDataReader rs, int index, IS protected override DateTime GetParameterValueToSet(DateOnly dateOnly, ISessionImplementor session) => dateOnly.ToDateTime(TimeOnly.MinValue); - /// public override string ObjectToSQLString(object value, Dialect.Dialect dialect) => - "'" + ((DateOnly) value).ToString("yyyy-MM-dd") + "'"; + dialect.ToStringLiteral(((DateOnly) value).ToString("yyyy-MM-dd"), SqlTypeFactory.GetAnsiString(50)); } } #endif diff --git a/src/NHibernate/Type/TimeOnlyAsDateTimeType.cs b/src/NHibernate/Type/TimeOnlyAsDateTimeType.cs index 70ebdfb599a..4a99aa4e12f 100644 --- a/src/NHibernate/Type/TimeOnlyAsDateTimeType.cs +++ b/src/NHibernate/Type/TimeOnlyAsDateTimeType.cs @@ -61,7 +61,7 @@ void IParameterizedType.SetParameterValues(IDictionary parameter } public override string ObjectToSQLString(object value, Dialect.Dialect dialect) => - "'" + GetDateTimeFromTimeOnly(AdjustTimeOnly((TimeOnly) value)).ToString(_sqlFormat) + "'"; + dialect.ToStringLiteral(GetDateTimeFromTimeOnly(AdjustTimeOnly((TimeOnly) value)).ToString(_sqlFormat), SqlTypeFactory.GetAnsiString(50)); } } #endif diff --git a/src/NHibernate/Type/TimeOnlyAsTicksType.cs b/src/NHibernate/Type/TimeOnlyAsTicksType.cs index 9715b6e52ec..64610091765 100644 --- a/src/NHibernate/Type/TimeOnlyAsTicksType.cs +++ b/src/NHibernate/Type/TimeOnlyAsTicksType.cs @@ -14,7 +14,6 @@ namespace NHibernate.Type { - /// /// Maps a property to a column. /// The value persisted is the Ticks property of the TimeOnly value. @@ -48,7 +47,7 @@ public TimeOnlyAsTicksType(byte fractionalSecondsPrecision) : base(fractionalSec protected override long GetParameterValueToSet(TimeOnly timeOnly, ISessionImplementor session) => timeOnly.Ticks; public override string ObjectToSQLString(object value, Dialect.Dialect dialect) => - AdjustTimeOnly((TimeOnly) value).Ticks.ToString(); + dialect.ToStringLiteral(AdjustTimeOnly((TimeOnly) value).Ticks.ToString(), SqlTypeFactory.GetAnsiString(50)); } } #endif diff --git a/src/NHibernate/Type/TimeOnlyAsTimeType.cs b/src/NHibernate/Type/TimeOnlyAsTimeType.cs index 4b622cd2d71..599f4996efe 100644 --- a/src/NHibernate/Type/TimeOnlyAsTimeType.cs +++ b/src/NHibernate/Type/TimeOnlyAsTimeType.cs @@ -48,7 +48,7 @@ protected override TimeOnly GetTimeOnlyFromReader(DbDataReader rs, int index, IS { if (rs.GetFieldType(index) == typeof(TimeSpan)) //For those dialects where DbType.Time means TimeSpan. { - return new TimeOnly(((TimeSpan)rs[index]).Ticks); + return new TimeOnly(((TimeSpan) rs[index]).Ticks); } var dbValue = rs.GetDateTime(index); @@ -72,7 +72,7 @@ void IParameterizedType.SetParameterValues(IDictionary parameter } public override string ObjectToSQLString(object value, Dialect.Dialect dialect) => - "'" + (AdjustTimeOnly((TimeOnly) value)).ToString(_sqlFormat) + "'"; + dialect.ToStringLiteral(AdjustTimeOnly((TimeOnly) value).ToString(_sqlFormat), SqlTypeFactory.GetAnsiString(50)); } } #endif diff --git a/src/NHibernate/Type/TypeFactory.cs b/src/NHibernate/Type/TypeFactory.cs index da99061e1b2..c2d4f08215e 100644 --- a/src/NHibernate/Type/TypeFactory.cs +++ b/src/NHibernate/Type/TypeFactory.cs @@ -342,8 +342,7 @@ private static void RegisterDefaultNetTypes() // object needs to have both class and serializable setup before it can // be created. - RegisterType(typeof (Object), NHibernateUtil.Object, new[] {"object"}); - +RegisterType(typeof (Object), NHibernateUtil.Object, new[] {"object"}); #if NET6_0_OR_GREATER RegisterType(typeof(DateOnly), NHibernateUtil.DateOnlyAsDate, new[] { "dateonly", "dateonlyasdate" }); @@ -936,6 +935,7 @@ public static NullableType GetTimeType(byte fractionalSecondsPrecision) var key = GetKeyForLengthOrScaleBased(NHibernateUtil.Time.Name, fractionalSecondsPrecision); return (NullableType)typeByTypeOfName.GetOrAdd(key, k => new TimeType(SqlTypeFactory.GetTime(fractionalSecondsPrecision))); } + #if NET6_0_OR_GREATER /// /// Gets a with desired fractional seconds precision. From 1a353fff1997136936078f664be2dafc48393266 Mon Sep 17 00:00:00 2001 From: Gunnar Liljas Date: Sun, 2 Nov 2025 21:15:34 +0100 Subject: [PATCH 3/3] More suggestions from code review --- src/NHibernate/Type/TimeOnlyAsDateTimeType.cs | 4 ++-- src/NHibernate/Type/TimeOnlyAsTimeType.cs | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/NHibernate/Type/TimeOnlyAsDateTimeType.cs b/src/NHibernate/Type/TimeOnlyAsDateTimeType.cs index 4a99aa4e12f..8624efe5874 100644 --- a/src/NHibernate/Type/TimeOnlyAsDateTimeType.cs +++ b/src/NHibernate/Type/TimeOnlyAsDateTimeType.cs @@ -15,7 +15,7 @@ namespace NHibernate.Type /// /// /// - /// This defaults the Date to "1753-01-01" - which should not matter because + /// This defaults the Date to "2000-01-01" - which should not matter because /// using this Type indicates that you don't care about the Date portion of the DateTime. /// However, if you need to adjust this base value, you can specify the parameter 'BaseDateValue' on the type mapping, /// using the date format 'yyyy-MM-dd' @@ -24,7 +24,7 @@ namespace NHibernate.Type [Serializable] public class TimeOnlyAsDateTimeType : AbstractTimeOnlyType, IParameterizedType { - private DateTime _baseDateValue = new(1753, 01, 01); + private DateTime _baseDateValue = new(2000, 01, 01); private readonly string _sqlFormat; /// /// Default constructor. Sets the fractional seconds precision (scale) to 0 diff --git a/src/NHibernate/Type/TimeOnlyAsTimeType.cs b/src/NHibernate/Type/TimeOnlyAsTimeType.cs index 599f4996efe..3283fdc6782 100644 --- a/src/NHibernate/Type/TimeOnlyAsTimeType.cs +++ b/src/NHibernate/Type/TimeOnlyAsTimeType.cs @@ -17,7 +17,7 @@ namespace NHibernate.Type /// /// Some DB drivers will use a value for the column, whereas /// others will use a full . In the latter case, the date part defaults to - /// to "1753-01-01" - which should not matter because using this Type indicates that you don't care about the Date portion of the DateTime. + /// to "2000-01-01" - which should not matter because using this Type indicates that you don't care about the Date portion of the DateTime. /// However, if you need to adjust this base value, you can specify the parameter 'BaseDateValue' on the type mapping, /// using the date format 'yyyy-MM-dd' /// @@ -25,7 +25,7 @@ namespace NHibernate.Type [Serializable] public class TimeOnlyAsTimeType : AbstractTimeOnlyType, IParameterizedType { - private DateTime BaseDateValue = new(1753, 01, 01); + private DateTime BaseDateValue = new(2000, 01, 01); private readonly string _sqlFormat; ///