From b39dd6144973e4f686ac9aa844a1d807fa103c7e Mon Sep 17 00:00:00 2001 From: Gunnar Liljas Date: Mon, 23 Dec 2024 11:32:06 +0100 Subject: [PATCH 1/4] Fix Linq .Second bug for PostgreSQL et al. Refactor tests --- .../Async/Linq/DateTimeTests.cs | 249 +++++++++++++++--- src/NHibernate.Test/Linq/DateTimeTests.cs | 248 +++++++++++++---- src/NHibernate.Test/TestDialect.cs | 6 +- src/NHibernate/Dialect/FirebirdDialect.cs | 1 + .../Dialect/Function/SQLFunctionRegistry.cs | 13 +- src/NHibernate/Dialect/Oracle8iDialect.cs | 2 + src/NHibernate/Dialect/PostgreSQLDialect.cs | 2 + .../DateTimePropertiesHqlGenerator.cs | 12 +- 8 files changed, 439 insertions(+), 94 deletions(-) diff --git a/src/NHibernate.Test/Async/Linq/DateTimeTests.cs b/src/NHibernate.Test/Async/Linq/DateTimeTests.cs index 8d0fd7c1598..4b67a5ebbcb 100644 --- a/src/NHibernate.Test/Async/Linq/DateTimeTests.cs +++ b/src/NHibernate.Test/Async/Linq/DateTimeTests.cs @@ -9,98 +9,261 @@ using System; +using System.Data; using System.Linq; +using NHibernate.Cfg; +using NHibernate.SqlTypes; +using NHibernate.Mapping.ByCode; using NUnit.Framework; +using NHibernate.Type; using NHibernate.Linq; +using System.Linq.Expressions; namespace NHibernate.Test.Linq { using System.Threading.Tasks; + using System.Threading; [TestFixture] - public class DateTimeTestsAsync : LinqTestCase + public class DateTimeTestsAsync : TestCase { + private bool DialectSupportsDateTimeOffset => TestDialect.SupportsSqlType(new SqlType(DbType.DateTimeOffset)); + private bool DialectSupportsDateTimeWithScale => TestDialect.SupportsSqlType(new SqlType(DbType.DateTime,(byte) 2)); + private readonly DateTimeTestsClass[] _referenceEntities = + [ + new() {Id =1, DateTimeValue = new DateTime(1998, 02, 26)}, + new() {Id =2, DateTimeValue = new DateTime(1998, 02, 26)}, + new() {Id =3, DateTimeValue = new DateTime(1998, 02, 26, 01, 01, 01)}, + new() {Id =4, DateTimeValue = new DateTime(1998, 02, 26, 02, 02, 02)}, + new() {Id =5, DateTimeValue = new DateTime(1998, 02, 26, 03, 03, 03)}, + new() {Id =6, DateTimeValue = new DateTime(1998, 02, 26, 04, 04, 04)}, + new() {Id =7, DateTimeValue = new DateTime(1998, 03, 01)}, + new() {Id =8, DateTimeValue = new DateTime(2000, 01, 01)} + ]; + + protected override string[] Mappings => default; + protected override void AddMappings(Configuration configuration) + { + var modelMapper = new ModelMapper(); + + modelMapper.Class(m => + { + m.Table("datetimetests"); + m.Lazy(false); + m.Id(p => p.Id, p => p.Generator(Generators.Assigned)); + m.Property(p => p.DateValue, c => c.Type()); + m.Property(p => p.DateTimeValue); + m.Property(p => p.DateTimeValueWithScale, c => c.Scale(2)); + if (DialectSupportsDateTimeOffset) + { + m.Property(p => p.DateTimeOffsetValue); + m.Property(p => p.DateTimeOffsetValueWithScale, c => c.Scale(2)); + } + }); + var mapping = modelMapper.CompileMappingForAllExplicitlyAddedEntities(); + configuration.AddMapping(mapping); + } + + protected override void OnSetUp() + { + foreach (var entity in _referenceEntities) + { + entity.DateValue = entity.DateTimeValue.Date; + entity.DateTimeValueWithScale = entity.DateTimeValue.AddSeconds(0.9); + entity.DateTimeOffsetValue = new DateTimeOffset(entity.DateTimeValue, TimeSpan.FromHours(3)); + entity.DateTimeOffsetValueWithScale = new DateTimeOffset(entity.DateTimeValue, TimeSpan.FromHours(3)); + } + + using (var session = OpenSession()) + using (var trans = session.BeginTransaction()) + { + foreach (var entity in _referenceEntities) + { + session.Save(entity); + } + trans.Commit(); + } + } + + protected override void OnTearDown() + { + using (var session = OpenSession()) + using (var trans = session.BeginTransaction()) + { + session.Query().Delete(); + trans.Commit(); + } + } + + private void AssertDateTimeOffsetSupported() + { + if (!DialectSupportsDateTimeOffset) + { + Assert.Ignore("Dialect doesn't support DateTimeOffset"); + } + } + + private async Task AssertDateTimeWithScaleSupportedAsync(CancellationToken cancellationToken = default(CancellationToken)) + { + if (!DialectSupportsDateTimeWithScale) + { + Assert.Ignore("Dialect doesn't support DateTime with scale (2)"); + } + using (var session = OpenSession()) + using (var trans = session.BeginTransaction()) + { + var entity1 = await (session.GetAsync(_referenceEntities[0].Id, cancellationToken)); + if (entity1.DateTimeValueWithScale != entity1.DateTimeValue.AddSeconds(0.9)) + { + Assert.Ignore("Current setup doesn't support DateTime with scale (2)"); + } + } + + } + + private Task AssertQueryAsync(Expression> where, CancellationToken cancellationToken = default(CancellationToken)) => AssertQueryAsync(where, x => x.Id, cancellationToken); + + private async Task AssertQueryAsync(Expression> where, Expression> select, CancellationToken cancellationToken = default(CancellationToken)) + { + using var session = OpenSession(); + var fromDb = await (session.Query().Where(where).Select(select).ToListAsync(cancellationToken)); + var fromMemory = _referenceEntities.AsQueryable().Where(where).Select(select).AsEnumerable().ToList(); //AsEnumerable added to avoid async generator + Assert.That(fromMemory, Has.Count.GreaterThan(0), "Inconclusive, since the query doesn't match anything in the defined set"); + Assert.That(fromDb, Has.Count.EqualTo(fromMemory.Count)); + Assert.That(fromDb, Is.EquivalentTo(fromMemory)); + } + [Test] public async Task CanQueryByYearAsync() { - var x = await ((from o in db.Orders - where o.OrderDate.Value.Year == 1998 - select o).ToListAsync()); + await (AssertQueryAsync(o => o.DateTimeValue.Year == 1998)); + } - Assert.AreEqual(270, x.Count()); + [Test] + public async Task CanQueryDateTimeBySecondAsync() + { + await (AssertQueryAsync(o => o.DateTimeValue.Second == 4)); } [Test] - public async Task CanQueryByDateAsync() + public async Task CanQueryDateTimeByMinuteAsync() { - var x = await ((from o in db.Orders - where o.OrderDate.Value.Date == new DateTime(1998, 02, 26) - select o).ToListAsync()); + await (AssertQueryAsync(o => o.DateTimeValue.Minute == 4)); + } - Assert.AreEqual(6, x.Count()); + [Test] + public async Task CanQueryDateTimeByHourAsync() + { + await (AssertQueryAsync(o => o.DateTimeValue.Hour == 4)); } [Test] - public async Task CanQueryByDateTimeAsync() + public async Task CanQueryDateTimeBySecondWhenValueContainsFractionalSecondsAsync() { - var x = await ((from o in db.Orders - where o.OrderDate.Value == new DateTime(1998, 02, 26) - select o).ToListAsync()); + await (AssertDateTimeWithScaleSupportedAsync()); + await (AssertQueryAsync(o => o.DateTimeValueWithScale.Second == 4)); + } - Assert.AreEqual(5, x.Count()); + [Test] + public async Task CanQueryDateTimeOffsetBySecondAsync() + { + AssertDateTimeOffsetSupported(); + await (AssertQueryAsync(o => o.DateTimeOffsetValue.Second == 4)); } [Test] - public async Task CanQueryByDateTime2Async() + public async Task CanQueryDateTimeOffsetByMinuteAsync() { - var x = await ((from o in db.Orders - where o.OrderDate.Value == new DateTime(1998, 02, 26, 0, 1, 0) - select o).ToListAsync()); + AssertDateTimeOffsetSupported(); + await (AssertQueryAsync(o => o.DateTimeOffsetValue.Minute == 4)); + } - Assert.AreEqual(1, x.Count()); + [Test] + public async Task CanQueryDateTimeOffsetByHourAsync() + { + AssertDateTimeOffsetSupported(); + await (AssertQueryAsync(o => o.DateTimeOffsetValue.Hour == 4)); } [Test] - public async Task CanSelectYearAsync() + public async Task CanQueryDateTimeOffsetBySecondWhenValueContainsFractionalSecondsAsync() { - var x = await ((from o in db.Orders - where o.OrderDate.Value.Year == 1998 - select o.OrderDate.Value.Year).ToListAsync()); + AssertDateTimeOffsetSupported(); + await (AssertQueryAsync(o => o.DateTimeOffsetValueWithScale.Second == 4)); + } - Assert.That(x, Has.All.EqualTo(1998)); - Assert.AreEqual(270, x.Count()); + [Test] + public async Task CanQueryByDateAsync() + { + await (AssertQueryAsync(o => o.DateTimeValue.Date == new DateTime(1998, 02, 26))); } [Test] - public async Task CanSelectDateAsync() + public async Task CanQueryByDateTimeAsync() { - var x = await ((from o in db.Orders - where o.OrderDate.Value.Date == new DateTime(1998, 02, 26) - select o.OrderDate.Value.Date).ToListAsync()); + await (AssertQueryAsync(o => o.DateTimeValue == new DateTime(1998, 02, 26))); + } - Assert.That(x, Has.All.EqualTo(new DateTime(1998, 02, 26))); - Assert.AreEqual(6, x.Count()); + [Test] + public async Task CanQueryByDateTime2Async() + { + await (AssertQueryAsync(o => o.DateTimeValue == new DateTime(1998, 02, 26, 1, 1, 1))); } [Test] - public async Task CanSelectDateTimeAsync() + public async Task CanSelectYearAsync() + { + await (AssertQueryAsync(o => o.DateTimeValue.Year == 1998, o => o.DateTimeValue.Year)); + } + + [Test] + public async Task CanSelectDateAsync() { - var x = await ((from o in db.Orders - where o.OrderDate.Value == new DateTime(1998, 02, 26) - select o.OrderDate.Value).ToListAsync()); + await (AssertQueryAsync(o => o.DateTimeValue.Date == new DateTime(1998, 02, 26), o => o.DateTimeValue.Date)); + } - Assert.That(x, Has.All.EqualTo(new DateTime(1998, 02, 26))); - Assert.AreEqual(5, x.Count()); + [Test] + public async Task CanSelectDateTimeAsync() + { + await (AssertQueryAsync(o => o.DateTimeValue == new DateTime(1998, 02, 26), o => o.DateTimeValue)); } [Test] public async Task CanSelectDateTime2Async() { - var x = await ((from o in db.Orders - where o.OrderDate.Value == new DateTime(1998, 02, 26, 0, 1, 0) - select o.OrderDate.Value).ToListAsync()); + await (AssertQueryAsync(o => o.DateTimeValue == new DateTime(1998, 02, 26, 1, 1, 1), o => o.DateTimeValue)); + } + + [Test] + public async Task CanSelectDateTimeWithScaleAsync() + { + await (AssertDateTimeWithScaleSupportedAsync()); + await (AssertQueryAsync(o => o.DateTimeValueWithScale == _referenceEntities[0].DateTimeValueWithScale, o => o.DateTimeValueWithScale)); + } + + public class DateTimeTestsClass : IEquatable + { + public int Id { get; set; } + public DateTime DateTimeValue { get; set; } + public DateTime DateTimeValueWithScale { get; set; } + public DateTimeOffset DateTimeOffsetValue { get; set; } + public DateTimeOffset DateTimeOffsetValueWithScale { get; set; } + public DateTime DateValue { get; set; } + + public override bool Equals(object obj) + { + return Equals(obj as DateTimeTestsClass); + } + + public bool Equals(DateTimeTestsClass other) + { + return other is not null && + Id.Equals(other.Id); + } - Assert.That(x, Has.All.EqualTo(new DateTime(1998, 02, 26, 0, 1, 0))); - Assert.AreEqual(1, x.Count()); + public override int GetHashCode() + { + return HashCode.Combine(Id); + } } } } diff --git a/src/NHibernate.Test/Linq/DateTimeTests.cs b/src/NHibernate.Test/Linq/DateTimeTests.cs index 20ec87ff802..dc15154d845 100644 --- a/src/NHibernate.Test/Linq/DateTimeTests.cs +++ b/src/NHibernate.Test/Linq/DateTimeTests.cs @@ -1,94 +1,254 @@ -using System; +using System; +using System.Data; using System.Linq; +using System.Linq.Expressions; +using NHibernate.Cfg; +using NHibernate.Linq; +using NHibernate.Mapping.ByCode; +using NHibernate.SqlTypes; +using NHibernate.Type; using NUnit.Framework; namespace NHibernate.Test.Linq { [TestFixture] - public class DateTimeTests : LinqTestCase + public class DateTimeTests : TestCase { + private bool DialectSupportsDateTimeOffset => TestDialect.SupportsSqlType(new SqlType(DbType.DateTimeOffset)); + private readonly DateTimeTestsClass[] _referenceEntities = + [ + new() {Id =1, DateTimeValue = new DateTime(1998, 02, 26)}, + new() {Id =2, DateTimeValue = new DateTime(1998, 02, 26)}, + new() {Id =3, DateTimeValue = new DateTime(1998, 02, 26, 01, 01, 01)}, + new() {Id =4, DateTimeValue = new DateTime(1998, 02, 26, 02, 02, 02)}, + new() {Id =5, DateTimeValue = new DateTime(1998, 02, 26, 03, 03, 03)}, + new() {Id =6, DateTimeValue = new DateTime(1998, 02, 26, 04, 04, 04)}, + new() {Id =7, DateTimeValue = new DateTime(1998, 03, 01)}, + new() {Id =8, DateTimeValue = new DateTime(2000, 01, 01)} + ]; + + private TimeSpan FractionalSecondsAdded => TimeSpan.FromMilliseconds(900); + + protected override string[] Mappings => default; + protected override void AddMappings(Configuration configuration) + { + var modelMapper = new ModelMapper(); + + modelMapper.Class(m => + { + m.Table("datetimetests"); + m.Lazy(false); + m.Id(p => p.Id, p => p.Generator(Generators.Assigned)); + m.Property(p => p.DateValue, c => c.Type()); + m.Property(p => p.DateTimeValue); + m.Property(p => p.DateTimeValueWithScale, c => c.Scale(2)); + if (DialectSupportsDateTimeOffset) + { + m.Property(p => p.DateTimeOffsetValue); + m.Property(p => p.DateTimeOffsetValueWithScale, c => c.Scale(2)); + } + }); + var mapping = modelMapper.CompileMappingForAllExplicitlyAddedEntities(); + configuration.AddMapping(mapping); + } + + protected override void OnSetUp() + { + foreach (var entity in _referenceEntities) + { + entity.DateValue = entity.DateTimeValue.Date; + entity.DateTimeValueWithScale = entity.DateTimeValue + FractionalSecondsAdded; + entity.DateTimeOffsetValue = new DateTimeOffset(entity.DateTimeValue, TimeSpan.FromHours(3)); + entity.DateTimeOffsetValueWithScale = new DateTimeOffset(entity.DateTimeValueWithScale, TimeSpan.FromHours(3)); + } + + using var session = OpenSession(); + using var trans = session.BeginTransaction(); + foreach (var entity in _referenceEntities) + { + session.Save(entity); + } + trans.Commit(); + } + + protected override void OnTearDown() + { + using var session = OpenSession(); + using var trans = session.BeginTransaction(); + session.Query().Delete(); + trans.Commit(); + } + + private void AssertDateTimeOffsetSupported() + { + if (!DialectSupportsDateTimeOffset) + { + Assert.Ignore("Dialect doesn't support DateTimeOffset"); + } + } + + private void AssertDateTimeWithFractionalSecondsSupported() + { + //Ideally, the dialect should know whether this is supported or not + if (!TestDialect.SupportsDateTimeWithFractionalSeconds) + { + Assert.Ignore("Dialect doesn't support DateTime with factional seconds"); + } + + //But it sometimes doesn't + using var session = OpenSession(); + using var trans = session.BeginTransaction(); + var entity1 = session.Get(_referenceEntities[0].Id); + if (entity1.DateTimeValueWithScale != entity1.DateTimeValue + FractionalSecondsAdded) + { + Assert.Ignore("Current setup doesn't support DateTime with scale (2)"); + } + } + + private void AssertQuery(Expression> where) => AssertQuery(where, x => x.Id); + + private void AssertQuery(Expression> where, Expression> select) + { + using var session = OpenSession(); + var fromDb = session.Query().Where(where).Select(select).ToList(); + var fromMemory = _referenceEntities.AsQueryable().Where(where).Select(select).AsEnumerable().ToList(); //AsEnumerable added to avoid async generator + Assert.That(fromMemory, Has.Count.GreaterThan(0), "Inconclusive, since the query doesn't match anything in the defined set"); + Assert.That(fromDb, Has.Count.EqualTo(fromMemory.Count)); + Assert.That(fromDb, Is.EquivalentTo(fromMemory)); + } + [Test] public void CanQueryByYear() { - var x = (from o in db.Orders - where o.OrderDate.Value.Year == 1998 - select o).ToList(); + AssertQuery(o => o.DateTimeValue.Year == 1998); + } - Assert.AreEqual(270, x.Count()); + [Test] + public void CanQueryDateTimeBySecond() + { + AssertQuery(o => o.DateTimeValue.Second == 4); } [Test] - public void CanQueryByDate() + public void CanQueryDateTimeByMinute() { - var x = (from o in db.Orders - where o.OrderDate.Value.Date == new DateTime(1998, 02, 26) - select o).ToList(); + AssertQuery(o => o.DateTimeValue.Minute == 4); + } - Assert.AreEqual(6, x.Count()); + [Test] + public void CanQueryDateTimeByHour() + { + AssertQuery(o => o.DateTimeValue.Hour == 4); } [Test] - public void CanQueryByDateTime() + public void CanQueryDateTimeBySecondWhenValueContainsFractionalSeconds() { - var x = (from o in db.Orders - where o.OrderDate.Value == new DateTime(1998, 02, 26) - select o).ToList(); + AssertDateTimeWithFractionalSecondsSupported(); + AssertQuery(o => o.DateTimeValueWithScale.Second == 4); + } - Assert.AreEqual(5, x.Count()); + [Test] + public void CanQueryDateTimeOffsetBySecond() + { + AssertDateTimeOffsetSupported(); + AssertQuery(o => o.DateTimeOffsetValue.Second == 4); } [Test] - public void CanQueryByDateTime2() + public void CanQueryDateTimeOffsetByMinute() { - var x = (from o in db.Orders - where o.OrderDate.Value == new DateTime(1998, 02, 26, 0, 1, 0) - select o).ToList(); + AssertDateTimeOffsetSupported(); + AssertQuery(o => o.DateTimeOffsetValue.Minute == 4); + } - Assert.AreEqual(1, x.Count()); + [Test] + public void CanQueryDateTimeOffsetByHour() + { + AssertDateTimeOffsetSupported(); + AssertQuery(o => o.DateTimeOffsetValue.Hour == 4); } [Test] - public void CanSelectYear() + public void CanQueryDateTimeOffsetBySecondWhenValueContainsFractionalSeconds() { - var x = (from o in db.Orders - where o.OrderDate.Value.Year == 1998 - select o.OrderDate.Value.Year).ToList(); + AssertDateTimeOffsetSupported(); + AssertQuery(o => o.DateTimeOffsetValueWithScale.Second == 4); + } - Assert.That(x, Has.All.EqualTo(1998)); - Assert.AreEqual(270, x.Count()); + [Test] + public void CanQueryByDate() + { + AssertQuery(o => o.DateTimeValue.Date == new DateTime(1998, 02, 26)); } [Test] - public void CanSelectDate() + public void CanQueryByDateTime() { - var x = (from o in db.Orders - where o.OrderDate.Value.Date == new DateTime(1998, 02, 26) - select o.OrderDate.Value.Date).ToList(); + AssertQuery(o => o.DateTimeValue == new DateTime(1998, 02, 26)); + } - Assert.That(x, Has.All.EqualTo(new DateTime(1998, 02, 26))); - Assert.AreEqual(6, x.Count()); + [Test] + public void CanQueryByDateTime2() + { + AssertQuery(o => o.DateTimeValue == new DateTime(1998, 02, 26, 1, 1, 1)); } [Test] - public void CanSelectDateTime() + public void CanSelectYear() { - var x = (from o in db.Orders - where o.OrderDate.Value == new DateTime(1998, 02, 26) - select o.OrderDate.Value).ToList(); + AssertQuery(o => o.DateTimeValue.Year == 1998, o => o.DateTimeValue.Year); + } - Assert.That(x, Has.All.EqualTo(new DateTime(1998, 02, 26))); - Assert.AreEqual(5, x.Count()); + [Test] + public void CanSelectDate() + { + AssertQuery(o => o.DateTimeValue.Date == new DateTime(1998, 02, 26), o => o.DateTimeValue.Date); + } + + [Test] + public void CanSelectDateTime() + { + AssertQuery(o => o.DateTimeValue == new DateTime(1998, 02, 26), o => o.DateTimeValue); } [Test] public void CanSelectDateTime2() { - var x = (from o in db.Orders - where o.OrderDate.Value == new DateTime(1998, 02, 26, 0, 1, 0) - select o.OrderDate.Value).ToList(); + AssertQuery(o => o.DateTimeValue == new DateTime(1998, 02, 26, 1, 1, 1), o => o.DateTimeValue); + } - Assert.That(x, Has.All.EqualTo(new DateTime(1998, 02, 26, 0, 1, 0))); - Assert.AreEqual(1, x.Count()); + [Test] + public void CanSelectDateTimeWithScale() + { + AssertDateTimeWithFractionalSecondsSupported(); + AssertQuery(o => o.DateTimeValueWithScale == _referenceEntities[0].DateTimeValueWithScale, o => o.DateTimeValueWithScale); + } + + public class DateTimeTestsClass : IEquatable + { + public int Id { get; set; } + public DateTime DateTimeValue { get; set; } + public DateTime DateTimeValueWithScale { get; set; } + public DateTimeOffset DateTimeOffsetValue { get; set; } + public DateTimeOffset DateTimeOffsetValueWithScale { get; set; } + public DateTime DateValue { get; set; } + + public override bool Equals(object obj) + { + return Equals(obj as DateTimeTestsClass); + } + + public bool Equals(DateTimeTestsClass other) + { + return other is not null && + Id.Equals(other.Id); + } + + public override int GetHashCode() + { + return HashCode.Combine(Id); + } } } } diff --git a/src/NHibernate.Test/TestDialect.cs b/src/NHibernate.Test/TestDialect.cs index 316d7b68fd8..ec099144dde 100644 --- a/src/NHibernate.Test/TestDialect.cs +++ b/src/NHibernate.Test/TestDialect.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Data; using NHibernate.Hql.Ast.ANTLR; using NHibernate.Id; @@ -61,7 +61,7 @@ public bool NativeGeneratorSupportsBulkInsertion /// Some databases do not support SELECT FOR UPDATE /// public virtual bool SupportsSelectForUpdate => true; - + /// /// Some databases do not support SELECT FOR UPDATE with paging /// @@ -218,5 +218,7 @@ public virtual bool SupportsSqlType(SqlType sqlType) /// Some databases (MySql) don't support using main table aliases in subquery inside join ON clause /// public virtual bool SupportsCorrelatedColumnsInSubselectJoin => true; + + public virtual bool SupportsDateTimeWithFractionalSeconds => _dialect.TimestampResolutionInTicks < TimeSpan.TicksPerSecond && SupportsSqlType(new SqlType(DbType.DateTime, (byte) 2)); } } diff --git a/src/NHibernate/Dialect/FirebirdDialect.cs b/src/NHibernate/Dialect/FirebirdDialect.cs index 7109480335c..7d688ca5fe3 100644 --- a/src/NHibernate/Dialect/FirebirdDialect.cs +++ b/src/NHibernate/Dialect/FirebirdDialect.cs @@ -537,6 +537,7 @@ private void RegisterDateTimeFunctions() RegisterFunction("addweek", new StandardSQLFunction("addweek", NHibernateUtil.DateTime)); RegisterFunction("addyear", new StandardSQLFunction("addyear", NHibernateUtil.DateTime)); RegisterFunction("getexacttimestamp", new NoArgSQLFunction("getexacttimestamp", NHibernateUtil.DateTime)); + RegisterFunction("secondtruncated", new SQLFunctionTemplate(NHibernateUtil.Int32, "cast(floor(extract(second from ?1)) as int)")); } private void RegisterStringAndCharFunctions() diff --git a/src/NHibernate/Dialect/Function/SQLFunctionRegistry.cs b/src/NHibernate/Dialect/Function/SQLFunctionRegistry.cs index 701dadd64f7..b611ccfdad6 100644 --- a/src/NHibernate/Dialect/Function/SQLFunctionRegistry.cs +++ b/src/NHibernate/Dialect/Function/SQLFunctionRegistry.cs @@ -7,6 +7,8 @@ public class SQLFunctionRegistry { private readonly Dialect dialect; private readonly IDictionary userFunctions; + //Temporary alias support + private static Dictionary _functionAliases = new Dictionary(StringComparer.OrdinalIgnoreCase) { { "secondtruncated", "second" } }; public SQLFunctionRegistry(Dialect dialect, IDictionary userFunctions) { @@ -20,8 +22,11 @@ public SQLFunctionRegistry(Dialect dialect, IDictionary us /// public ISQLFunction FindSQLFunction(string functionName) { - ISQLFunction result; - if (!userFunctions.TryGetValue(functionName, out result)) + if (!userFunctions.ContainsKey(functionName) && !dialect.Functions.ContainsKey(functionName) && _functionAliases.TryGetValue(functionName, out var sqlFunction)) + { + functionName = sqlFunction; + } + if (!userFunctions.TryGetValue(functionName, out ISQLFunction result)) { dialect.Functions.TryGetValue(functionName, out result); } @@ -30,6 +35,10 @@ public ISQLFunction FindSQLFunction(string functionName) public bool HasFunction(string functionName) { + if (!userFunctions.ContainsKey(functionName) && !dialect.Functions.ContainsKey(functionName) && _functionAliases.TryGetValue(functionName, out var sqlFunction)) + { + functionName = sqlFunction; + } return userFunctions.ContainsKey(functionName) || dialect.Functions.ContainsKey(functionName); } } diff --git a/src/NHibernate/Dialect/Oracle8iDialect.cs b/src/NHibernate/Dialect/Oracle8iDialect.cs index bd11e9bcff5..c697339df73 100644 --- a/src/NHibernate/Dialect/Oracle8iDialect.cs +++ b/src/NHibernate/Dialect/Oracle8iDialect.cs @@ -265,9 +265,11 @@ protected virtual void RegisterFunctions() // Cast is needed because EXTRACT treats DATE not as legacy Oracle DATE but as ANSI DATE, without time elements. // Therefore, you can extract only YEAR, MONTH, and DAY from a DATE value. + // Oracle returns the seconds with fractional precision. It has to be truncated to return the actual second part RegisterFunction("second", new SQLFunctionTemplate(NHibernateUtil.Int32, "extract(second from cast(?1 as timestamp))")); RegisterFunction("minute", new SQLFunctionTemplate(NHibernateUtil.Int32, "extract(minute from cast(?1 as timestamp))")); RegisterFunction("hour", new SQLFunctionTemplate(NHibernateUtil.Int32, "extract(hour from cast(?1 as timestamp))")); + RegisterFunction("secondtruncated", new SQLFunctionTemplate(NHibernateUtil.Int32, "cast(floor(extract(second from cast(?1 as timestamp))) as int)")); RegisterFunction("date", new StandardSQLFunction("trunc", NHibernateUtil.Date)); diff --git a/src/NHibernate/Dialect/PostgreSQLDialect.cs b/src/NHibernate/Dialect/PostgreSQLDialect.cs index 725d530a22a..3053ce17267 100644 --- a/src/NHibernate/Dialect/PostgreSQLDialect.cs +++ b/src/NHibernate/Dialect/PostgreSQLDialect.cs @@ -105,6 +105,8 @@ public PostgreSQLDialect() // and NHibernate.TestDatabaseSetup does install it. RegisterFunction("new_uuid", new NoArgSQLFunction("uuid_generate_v4", NHibernateUtil.Guid)); + RegisterFunction("secondtruncated", new SQLFunctionTemplate(NHibernateUtil.Int32, "cast(floor(extract(second from ?1)) as int)")); + RegisterKeywords(); } diff --git a/src/NHibernate/Linq/Functions/DateTimePropertiesHqlGenerator.cs b/src/NHibernate/Linq/Functions/DateTimePropertiesHqlGenerator.cs index a8b26787f47..e5b1e95d703 100644 --- a/src/NHibernate/Linq/Functions/DateTimePropertiesHqlGenerator.cs +++ b/src/NHibernate/Linq/Functions/DateTimePropertiesHqlGenerator.cs @@ -1,6 +1,7 @@ -using System; +using System; using System.Linq.Expressions; using System.Reflection; +using NHibernate.Engine; using NHibernate.Hql.Ast; using NHibernate.Linq.Visitors; using NHibernate.Util; @@ -33,8 +34,13 @@ public DateTimePropertiesHqlGenerator() public override HqlTreeNode BuildHql(MemberInfo member, Expression expression, HqlTreeBuilder treeBuilder, IHqlExpressionVisitor visitor) { - return treeBuilder.MethodCall(member.Name.ToLowerInvariant(), + var functionName = member.Name.ToLowerInvariant(); + if (functionName == "second") + { + functionName = "secondtruncated"; + } + return treeBuilder.MethodCall(functionName, visitor.Visit(expression).AsExpression()); } } -} \ No newline at end of file +} From 06d14de413ba3adf99d7d05354df7f02c145f876 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Sat, 28 Dec 2024 17:49:24 +0000 Subject: [PATCH 2/4] Generate async files --- .../Async/Linq/DateTimeTests.cs | 63 +++++++++---------- 1 file changed, 30 insertions(+), 33 deletions(-) diff --git a/src/NHibernate.Test/Async/Linq/DateTimeTests.cs b/src/NHibernate.Test/Async/Linq/DateTimeTests.cs index 4b67a5ebbcb..1eb96020e3b 100644 --- a/src/NHibernate.Test/Async/Linq/DateTimeTests.cs +++ b/src/NHibernate.Test/Async/Linq/DateTimeTests.cs @@ -11,13 +11,13 @@ using System; using System.Data; using System.Linq; +using System.Linq.Expressions; using NHibernate.Cfg; -using NHibernate.SqlTypes; +using NHibernate.Linq; using NHibernate.Mapping.ByCode; -using NUnit.Framework; +using NHibernate.SqlTypes; using NHibernate.Type; -using NHibernate.Linq; -using System.Linq.Expressions; +using NUnit.Framework; namespace NHibernate.Test.Linq { @@ -27,7 +27,6 @@ namespace NHibernate.Test.Linq public class DateTimeTestsAsync : TestCase { private bool DialectSupportsDateTimeOffset => TestDialect.SupportsSqlType(new SqlType(DbType.DateTimeOffset)); - private bool DialectSupportsDateTimeWithScale => TestDialect.SupportsSqlType(new SqlType(DbType.DateTime,(byte) 2)); private readonly DateTimeTestsClass[] _referenceEntities = [ new() {Id =1, DateTimeValue = new DateTime(1998, 02, 26)}, @@ -40,6 +39,8 @@ public class DateTimeTestsAsync : TestCase new() {Id =8, DateTimeValue = new DateTime(2000, 01, 01)} ]; + private TimeSpan FractionalSecondsAdded => TimeSpan.FromMilliseconds(900); + protected override string[] Mappings => default; protected override void AddMappings(Configuration configuration) { @@ -68,30 +69,26 @@ protected override void OnSetUp() foreach (var entity in _referenceEntities) { entity.DateValue = entity.DateTimeValue.Date; - entity.DateTimeValueWithScale = entity.DateTimeValue.AddSeconds(0.9); + entity.DateTimeValueWithScale = entity.DateTimeValue + FractionalSecondsAdded; entity.DateTimeOffsetValue = new DateTimeOffset(entity.DateTimeValue, TimeSpan.FromHours(3)); - entity.DateTimeOffsetValueWithScale = new DateTimeOffset(entity.DateTimeValue, TimeSpan.FromHours(3)); + entity.DateTimeOffsetValueWithScale = new DateTimeOffset(entity.DateTimeValueWithScale, TimeSpan.FromHours(3)); } - using (var session = OpenSession()) - using (var trans = session.BeginTransaction()) + using var session = OpenSession(); + using var trans = session.BeginTransaction(); + foreach (var entity in _referenceEntities) { - foreach (var entity in _referenceEntities) - { - session.Save(entity); - } - trans.Commit(); + session.Save(entity); } + trans.Commit(); } protected override void OnTearDown() { - using (var session = OpenSession()) - using (var trans = session.BeginTransaction()) - { - session.Query().Delete(); - trans.Commit(); - } + using var session = OpenSession(); + using var trans = session.BeginTransaction(); + session.Query().Delete(); + trans.Commit(); } private void AssertDateTimeOffsetSupported() @@ -102,22 +99,22 @@ private void AssertDateTimeOffsetSupported() } } - private async Task AssertDateTimeWithScaleSupportedAsync(CancellationToken cancellationToken = default(CancellationToken)) + private async Task AssertDateTimeWithFractionalSecondsSupportedAsync(CancellationToken cancellationToken = default(CancellationToken)) { - if (!DialectSupportsDateTimeWithScale) + //Ideally, the dialect should know whether this is supported or not + if (!TestDialect.SupportsDateTimeWithFractionalSeconds) { - Assert.Ignore("Dialect doesn't support DateTime with scale (2)"); + Assert.Ignore("Dialect doesn't support DateTime with factional seconds"); } - using (var session = OpenSession()) - using (var trans = session.BeginTransaction()) + + //But it sometimes doesn't + using var session = OpenSession(); + using var trans = session.BeginTransaction(); + var entity1 = await (session.GetAsync(_referenceEntities[0].Id, cancellationToken)); + if (entity1.DateTimeValueWithScale != entity1.DateTimeValue + FractionalSecondsAdded) { - var entity1 = await (session.GetAsync(_referenceEntities[0].Id, cancellationToken)); - if (entity1.DateTimeValueWithScale != entity1.DateTimeValue.AddSeconds(0.9)) - { - Assert.Ignore("Current setup doesn't support DateTime with scale (2)"); - } + Assert.Ignore("Current setup doesn't support DateTime with scale (2)"); } - } private Task AssertQueryAsync(Expression> where, CancellationToken cancellationToken = default(CancellationToken)) => AssertQueryAsync(where, x => x.Id, cancellationToken); @@ -159,7 +156,7 @@ public async Task CanQueryDateTimeByHourAsync() [Test] public async Task CanQueryDateTimeBySecondWhenValueContainsFractionalSecondsAsync() { - await (AssertDateTimeWithScaleSupportedAsync()); + await (AssertDateTimeWithFractionalSecondsSupportedAsync()); await (AssertQueryAsync(o => o.DateTimeValueWithScale.Second == 4)); } @@ -236,7 +233,7 @@ public async Task CanSelectDateTime2Async() [Test] public async Task CanSelectDateTimeWithScaleAsync() { - await (AssertDateTimeWithScaleSupportedAsync()); + await (AssertDateTimeWithFractionalSecondsSupportedAsync()); await (AssertQueryAsync(o => o.DateTimeValueWithScale == _referenceEntities[0].DateTimeValueWithScale, o => o.DateTimeValueWithScale)); } From 773593c2475253e9f71a2450e9a07188d0a77583 Mon Sep 17 00:00:00 2001 From: Gunnar Liljas Date: Mon, 6 Jan 2025 13:22:06 +0100 Subject: [PATCH 3/4] Update src/NHibernate/Dialect/Function/SQLFunctionRegistry.cs 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/Dialect/Function/SQLFunctionRegistry.cs | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/src/NHibernate/Dialect/Function/SQLFunctionRegistry.cs b/src/NHibernate/Dialect/Function/SQLFunctionRegistry.cs index b611ccfdad6..7baa5a7d22c 100644 --- a/src/NHibernate/Dialect/Function/SQLFunctionRegistry.cs +++ b/src/NHibernate/Dialect/Function/SQLFunctionRegistry.cs @@ -22,13 +22,10 @@ public SQLFunctionRegistry(Dialect dialect, IDictionary us /// public ISQLFunction FindSQLFunction(string functionName) { - if (!userFunctions.ContainsKey(functionName) && !dialect.Functions.ContainsKey(functionName) && _functionAliases.TryGetValue(functionName, out var sqlFunction)) - { - functionName = sqlFunction; - } - if (!userFunctions.TryGetValue(functionName, out ISQLFunction result)) + if (!userFunctions.TryGetValue(functionName, out ISQLFunction result) && !dialect.Functions.TryGetValue(functionName, out result)) { - dialect.Functions.TryGetValue(functionName, out result); + if (_functionAliases.TryGetValue(functionName, out var sqlFunction) && !_functionAliases.ContainsKey(sqlFunction)) + return FindSQLFunction(sqlFunction); } return result; } From 8ce04b14add4d3ece7f0fd7bea4d19f74423cf40 Mon Sep 17 00:00:00 2001 From: Gunnar Liljas Date: Mon, 6 Jan 2025 13:22:19 +0100 Subject: [PATCH 4/4] Update src/NHibernate/Dialect/Function/SQLFunctionRegistry.cs 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/Dialect/Function/SQLFunctionRegistry.cs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/NHibernate/Dialect/Function/SQLFunctionRegistry.cs b/src/NHibernate/Dialect/Function/SQLFunctionRegistry.cs index 7baa5a7d22c..f61468b9c75 100644 --- a/src/NHibernate/Dialect/Function/SQLFunctionRegistry.cs +++ b/src/NHibernate/Dialect/Function/SQLFunctionRegistry.cs @@ -32,11 +32,11 @@ public ISQLFunction FindSQLFunction(string functionName) public bool HasFunction(string functionName) { - if (!userFunctions.ContainsKey(functionName) && !dialect.Functions.ContainsKey(functionName) && _functionAliases.TryGetValue(functionName, out var sqlFunction)) - { - functionName = sqlFunction; - } - return userFunctions.ContainsKey(functionName) || dialect.Functions.ContainsKey(functionName); + if (userFunctions.ContainsKey(functionName) || dialect.Functions.ContainsKey(functionName)) + return true; + if (_functionAliases.TryGetValue(functionName, out var sqlFunction) && !_functionAliases.ContainsKey(sqlFunction)) + return HasFunction(sqlFunction); + return false; } } }