Skip to content

Commit 32537ba

Browse files
Add an UtcDbTimestampType
Fixes #1631 (along with previous commit)
1 parent 3fd4d65 commit 32537ba

19 files changed

+265
-18
lines changed

doc/reference/modules/basic_mapping.xml

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3643,9 +3643,10 @@
36433643
<literal>DbType.DateTime</literal> / <literal>DbType.DateTime2</literal><coref linkend="basic_mapping.datetime-co" />
36443644
</entry>
36453645
<entry>
3646-
<literal>type="DbTimestamp"</literal> must be specified. When used as a
3646+
<literal>type="DbTimestamp"</literal> must be specified. When used as a
36473647
<literal>version</literal> field, uses the database's current time retrieved
3648-
in dedicated queries, rather than the client's current time.
3648+
in dedicated queries, rather than the client's current time. In case of lack of
3649+
database support, it falls back on the client's current time.
36493650
</entry>
36503651
</row>
36513652
<row>
@@ -3815,6 +3816,19 @@
38153816
Available since NHibernate v5.0.
38163817
</entry>
38173818
</row>
3819+
<row>
3820+
<entry><literal>UtcDbTimestamp</literal></entry>
3821+
<entry><literal>System.DateTime</literal></entry>
3822+
<entry>
3823+
<literal>DbType.DateTime</literal> / <literal>DbType.DateTime2</literal><coref linkend="basic_mapping.datetime-co" />
3824+
</entry>
3825+
<entry>
3826+
<literal>type="UtcDbTimestamp"</literal> must be specified. When used as a
3827+
<literal>version</literal> field, uses the database's current UTC time retrieved
3828+
in dedicated queries, rather than the client's current time. In case of lack of
3829+
database support, it falls back on the client's current time.
3830+
</entry>
3831+
</row>
38183832
<row>
38193833
<entry><literal>UtcTicks</literal></entry>
38203834
<entry><literal>System.DateTime</literal></entry>

src/NHibernate.Test/Async/TypesTest/AbstractDateTimeTypeFixture.cs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -88,7 +88,10 @@ protected override void DropSchema()
8888
[Test]
8989
public async Task NextAsync()
9090
{
91-
var current = Now.Subtract(TimeSpan.FromTicks(DateAccuracyInTicks));
91+
// Take some margin, as DbTimestampType takes its next value from the database, which
92+
// may have its clock a bit shifted even if running on the same server. (Seen with PostgreSQL,
93+
// off by a few seconds, and with SAP HANA running in a vm, off by twenty seconds.)
94+
var current = Now.Subtract(TimeSpan.FromMinutes(2));
9295
var next = await (Type.NextAsync(current, null, CancellationToken.None));
9396

9497
Assert.That(next, Is.TypeOf<DateTime>(), "next should be DateTime");
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
//------------------------------------------------------------------------------
2+
// <auto-generated>
3+
// This code was generated by AsyncGenerator.
4+
//
5+
// Changes to this file may cause incorrect behavior and will be lost if
6+
// the code is regenerated.
7+
// </auto-generated>
8+
//------------------------------------------------------------------------------
9+
10+
11+
using System;
12+
using NHibernate.Type;
13+
using NUnit.Framework;
14+
15+
namespace NHibernate.Test.TypesTest
16+
{
17+
using System.Threading.Tasks;
18+
[TestFixture]
19+
public class UtcDbTimestampTypeFixtureAsync : AbstractDateTimeTypeFixtureAsync
20+
{
21+
protected override string TypeName => "UtcDbTimestamp";
22+
protected override AbstractDateTimeType Type => NHibernateUtil.UtcDbTimestamp;
23+
protected override DateTime Now => (DateTime) Type.Seed(_session?.GetSessionImplementation());
24+
private ISession _session;
25+
26+
protected override void OnSetUp()
27+
{
28+
_session = OpenSession();
29+
base.OnSetUp();
30+
}
31+
32+
protected override void OnTearDown()
33+
{
34+
base.OnTearDown();
35+
_session.Dispose();
36+
_session = null;
37+
}
38+
}
39+
}

src/NHibernate.Test/TypesTest/AbstractDateTimeTypeFixture.cs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,10 @@ protected override void DropSchema()
7676
[Test]
7777
public void Next()
7878
{
79-
var current = Now.Subtract(TimeSpan.FromTicks(DateAccuracyInTicks));
79+
// Take some margin, as DbTimestampType takes its next value from the database, which
80+
// may have its clock a bit shifted even if running on the same server. (Seen with PostgreSQL,
81+
// off by a few seconds, and with SAP HANA running in a vm, off by twenty seconds.)
82+
var current = Now.Subtract(TimeSpan.FromMinutes(2));
8083
var next = Type.Next(current, null);
8184

8285
Assert.That(next, Is.TypeOf<DateTime>(), "next should be DateTime");
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
<?xml version="1.0" encoding="utf-8"?>
2+
3+
<hibernate-mapping xmlns="urn:nhibernate-mapping-2.2" default-lazy="false">
4+
<class
5+
name="NHibernate.Test.TypesTest.DateTimeClass, NHibernate.Test"
6+
table="bc_datetime">
7+
<id name="Id" column="id">
8+
<generator class="assigned"/>
9+
</id>
10+
<version name="Revision" column="`Revision`" type="utcdbtimestamp"/>
11+
<property name="Value" column="`Value`" type="utcdbtimestamp"/>
12+
<property name="NullableValue" type="utcdbtimestamp"/>
13+
</class>
14+
</hibernate-mapping>
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
using System;
2+
using NHibernate.Type;
3+
using NUnit.Framework;
4+
5+
namespace NHibernate.Test.TypesTest
6+
{
7+
[TestFixture]
8+
public class UtcDbTimestampTypeFixture : AbstractDateTimeTypeFixture
9+
{
10+
protected override string TypeName => "UtcDbTimestamp";
11+
protected override AbstractDateTimeType Type => NHibernateUtil.UtcDbTimestamp;
12+
protected override DateTime Now => (DateTime) Type.Seed(_session?.GetSessionImplementation());
13+
private ISession _session;
14+
15+
protected override void OnSetUp()
16+
{
17+
_session = OpenSession();
18+
base.OnSetUp();
19+
}
20+
21+
protected override void OnTearDown()
22+
{
23+
base.OnTearDown();
24+
_session.Dispose();
25+
_session = null;
26+
}
27+
}
28+
}

src/NHibernate/Async/Type/DbTimestampType.cs

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -33,22 +33,28 @@ public override async Task<object> SeedAsync(ISessionImplementor session, Cancel
3333
log.Debug("incoming session was null; using current application host time");
3434
return await (base.SeedAsync(null, cancellationToken)).ConfigureAwait(false);
3535
}
36-
if (!session.Factory.Dialect.SupportsCurrentTimestampSelection)
36+
if (!SupportsCurrentTimestampSelection(session.Factory.Dialect))
3737
{
3838
log.Info("falling back to application host based timestamp, as dialect does not support current timestamp selection");
3939
return await (base.SeedAsync(session, cancellationToken)).ConfigureAwait(false);
4040
}
4141
return await (GetCurrentTimestampAsync(session, cancellationToken)).ConfigureAwait(false);
4242
}
4343

44+
/// <summary>
45+
/// Retrieves the current timestamp in database.
46+
/// </summary>
47+
/// <param name="session">The session to use for retrieving the timestamp.</param>
48+
/// <param name="cancellationToken">A cancellation token that can be used to cancel the work</param>
49+
/// <returns>A datetime.</returns>
4450
protected virtual async Task<DateTime> GetCurrentTimestampAsync(ISessionImplementor session, CancellationToken cancellationToken)
4551
{
4652
cancellationToken.ThrowIfCancellationRequested();
4753
var dialect = session.Factory.Dialect;
4854
// Need to round notably for Sql Server DateTime with Odbc, which has a 3.33ms resolution,
4955
// causing stale data update failure 2/3 of times if not rounded to 10ms.
5056
return Round(
51-
await (UsePreparedStatementAsync(dialect.CurrentTimestampSelectString, session, cancellationToken)).ConfigureAwait(false),
57+
await (UsePreparedStatementAsync(GetCurrentTimestampSelectString(dialect), session, cancellationToken)).ConfigureAwait(false),
5258
dialect.TimestampResolutionInTicks);
5359
}
5460

@@ -66,8 +72,8 @@ protected virtual async Task<DateTime> UsePreparedStatementAsync(string timestam
6672
rs = await (session.Batcher.ExecuteReaderAsync(ps, cancellationToken)).ConfigureAwait(false);
6773
await (rs.ReadAsync(cancellationToken)).ConfigureAwait(false);
6874
var ts = rs.GetDateTime(0);
69-
log.Debug("current timestamp retreived from db : {0} (ticks={1})", ts, ts.Ticks);
70-
return ts;
75+
log.Debug("current timestamp retrieved from db : {0} (ticks={1})", ts, ts.Ticks);
76+
return AdjustDateTime(ts);
7177
}
7278
catch (DbException sqle)
7379
{

src/NHibernate/Dialect/Dialect.cs

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -738,6 +738,9 @@ public virtual bool SupportsCurrentTimestampSelection
738738
get { return false; }
739739
}
740740

741+
/// <summary> Does this dialect support a way to retrieve the database's current UTC timestamp value? </summary>
742+
public virtual bool SupportsCurrentUtcTimestampSelection => false;
743+
741744
/// <summary>
742745
/// Gives the best resolution that the database can use for storing
743746
/// date/time values, in ticks.
@@ -2434,8 +2437,8 @@ public virtual bool IsCurrentTimestampSelectStringCallable
24342437
get { throw new NotSupportedException("Database not known to define a current timestamp function"); }
24352438
}
24362439

2437-
/// <summary>
2438-
/// Retrieve the command used to retrieve the current timestammp from the database.
2440+
/// <summary>
2441+
/// Retrieve the command used to retrieve the current timestamp from the database.
24392442
/// </summary>
24402443
public virtual string CurrentTimestampSelectString
24412444
{
@@ -2451,6 +2454,20 @@ public virtual string CurrentTimestampSQLFunctionName
24512454
get { return "current_timestamp"; }
24522455
}
24532456

2457+
/// <summary>
2458+
/// Retrieve the command used to retrieve the current UTC timestamp from the database.
2459+
/// </summary>
2460+
public virtual string CurrentUtcTimestampSelectString =>
2461+
throw new NotSupportedException("Database not known to define a current UTC timestamp function");
2462+
2463+
/// <summary>
2464+
/// The name of the database-specific SQL function for retrieving the
2465+
/// current UTC timestamp.
2466+
/// </summary>
2467+
public virtual string CurrentUtcTimestampSQLFunctionName =>
2468+
// It seems there are no SQL ANSI function for UTC
2469+
throw new NotSupportedException("Database not known to define a current UTC timestamp function");
2470+
24542471
public virtual IViolatedConstraintNameExtracter ViolatedConstraintNameExtracter
24552472
{
24562473
get { return Extracter; }

src/NHibernate/Dialect/HanaDialectBase.cs

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -680,6 +680,9 @@ public override int RegisterResultSetOutParameter(DbCommand statement, int posit
680680
/// <inheritdoc />
681681
public override bool SupportsCurrentTimestampSelection => true;
682682

683+
/// <inheritdoc />
684+
public override bool SupportsCurrentUtcTimestampSelection => true;
685+
683686
/// <inheritdoc />
684687
public override long TimestampResolutionInTicks
685688
// According to https://help.sap.com/viewer/4fe29514fd584807ac9f2a04f6754767/2.0.02/en-US/3f81ccc7e35d44cbbc595c7d552c202a.html,
@@ -929,6 +932,14 @@ public override string CurrentTimestampSelectString
929932
// SYS.DUMMY is a system table having normally one row. If someone has fiddled with it, this will cause failures...
930933
=> "select current_timestamp from sys.dummy";
931934

935+
/// <inheritdoc />
936+
public override string CurrentUtcTimestampSelectString
937+
// SYS.DUMMY is a system table having normally one row. If someone has fiddled with it, this will cause failures...
938+
=> $"select {CurrentUtcTimestampSQLFunctionName} from sys.dummy";
939+
940+
/// <inheritdoc />
941+
public override string CurrentUtcTimestampSQLFunctionName => "current_utctimestamp";
942+
932943
/// <inheritdoc />
933944
public override int MaxAliasLength => 128;
934945

src/NHibernate/Dialect/MsSql2000Dialect.cs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -435,6 +435,16 @@ public override bool SupportsCurrentTimestampSelection
435435
get { return true; }
436436
}
437437

438+
/// <inheritdoc />
439+
public override string CurrentUtcTimestampSQLFunctionName => "GETUTCDATE()";
440+
441+
/// <inheritdoc />
442+
public override string CurrentUtcTimestampSelectString =>
443+
"SELECT " + CurrentUtcTimestampSQLFunctionName;
444+
445+
/// <inheritdoc />
446+
public override bool SupportsCurrentUtcTimestampSelection => true;
447+
438448
public override bool QualifyIndexName
439449
{
440450
get { return false; }

src/NHibernate/Dialect/MsSql2008Dialect.cs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,10 @@ protected override void RegisterDefaultProperties()
7575
public override string CurrentTimestampSQLFunctionName =>
7676
KeepDateTime ? base.CurrentTimestampSQLFunctionName : "SYSDATETIME()";
7777

78+
/// <inheritdoc />
79+
public override string CurrentUtcTimestampSQLFunctionName =>
80+
KeepDateTime ? base.CurrentUtcTimestampSQLFunctionName : "SYSUTCDATETIME()";
81+
7882
public override long TimestampResolutionInTicks =>
7983
KeepDateTime
8084
? base.TimestampResolutionInTicks

src/NHibernate/Dialect/Oracle9iDialect.cs

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,17 @@ public override string CurrentTimestampSQLFunctionName
2020
}
2121
}
2222

23+
// Current_timestamp is a timestamp with time zone, so it can always be converted back to UTC.
24+
/// <inheritdoc />
25+
public override string CurrentUtcTimestampSQLFunctionName => "SYS_EXTRACT_UTC(current_timestamp)";
26+
27+
/// <inheritdoc />
28+
public override string CurrentUtcTimestampSelectString =>
29+
$"select {CurrentUtcTimestampSQLFunctionName} from dual";
30+
31+
/// <inheritdoc />
32+
public override bool SupportsCurrentUtcTimestampSelection => true;
33+
2334
protected override void RegisterDateTimeTypeMappings()
2435
{
2536
RegisterColumnType(DbType.Date, "DATE");

src/NHibernate/Dialect/SybaseASE15Dialect.cs

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -162,7 +162,14 @@ public override string CurrentTimestampSelectString
162162
{
163163
get { return "select getdate()"; }
164164
}
165-
165+
166+
/// <inheritdoc />
167+
public override string CurrentUtcTimestampSelectString =>
168+
"SELECT " + CurrentUtcTimestampSQLFunctionName;
169+
170+
/// <inheritdoc />
171+
public override bool SupportsCurrentUtcTimestampSelection => true;
172+
166173
/// <summary>
167174
/// Sybase ASE 15 temporary tables are not supported
168175
/// </summary>
@@ -231,7 +238,10 @@ public override string CurrentTimestampSQLFunctionName
231238
{
232239
get { return "getdate()"; }
233240
}
234-
241+
242+
/// <inheritdoc />
243+
public override string CurrentUtcTimestampSQLFunctionName => "getutcdate()";
244+
235245
public override bool SupportsExpectedLobUsagePattern
236246
{
237247
get { return false; }

src/NHibernate/Dialect/SybaseSQLAnywhere12Dialect.cs

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,16 @@ public SybaseSQLAnywhere12Dialect()
7474
RegisterColumnType(DbType.DateTimeOffset, "DATETIMEOFFSET");
7575
}
7676

77+
/// <inheritdoc />
78+
public override string CurrentUtcTimestampSQLFunctionName => "cast(current UTC timestamp as timestamp)";
79+
80+
/// <inheritdoc />
81+
public override string CurrentUtcTimestampSelectString =>
82+
"SELECT " + CurrentUtcTimestampSQLFunctionName;
83+
84+
/// <inheritdoc />
85+
public override bool SupportsCurrentUtcTimestampSelection => true;
86+
7787
/// <summary>
7888
/// SQL Anywhere supports <tt>SEQUENCES</tt> using a primarily SQL Standard
7989
/// syntax. Sequence values can be queried using the <tt>.CURRVAL</tt> identifier, and the next
@@ -127,4 +137,4 @@ public override IDataBaseSchema GetDataBaseSchema(DbConnection connection)
127137
return new SybaseAnywhereDataBaseMetaData(connection);
128138
}
129139
}
130-
}
140+
}

src/NHibernate/Hql/Ast/ANTLR/HqlSqlWalker.cs

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -336,7 +336,9 @@ void PostProcessInsert(IASTNode insert)
336336
}
337337
else if (IsDatabaseGeneratedTimestamp(versionType))
338338
{
339-
string functionName = SessionFactoryHelper.Factory.Dialect.CurrentTimestampSQLFunctionName;
339+
var functionName = IsUtcDatabaseGeneratedTimestamp(versionType)
340+
? SessionFactoryHelper.Factory.Dialect.CurrentUtcTimestampSQLFunctionName
341+
: SessionFactoryHelper.Factory.Dialect.CurrentTimestampSQLFunctionName;
340342
versionValueNode = ASTFactory.CreateNode(SQL_TOKEN, functionName);
341343
}
342344
else
@@ -365,6 +367,11 @@ private static bool IsDatabaseGeneratedTimestamp(IType type)
365367
return type is DbTimestampType;
366368
}
367369

370+
private static bool IsUtcDatabaseGeneratedTimestamp(IType type)
371+
{
372+
return type is UtcDbTimestampType;
373+
}
374+
368375
private static bool IsIntegral(IType type)
369376
{
370377
return

src/NHibernate/NHibernateUtil.cs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -244,6 +244,11 @@ public static IType GuessType(System.Type type)
244244
/// </summary>
245245
public static readonly DbTimestampType DbTimestamp = new DbTimestampType();
246246

247+
/// <summary>
248+
/// NHibernate Timestamp type, seeded db side, in UTC.
249+
/// </summary>
250+
public static readonly UtcDbTimestampType UtcDbTimestamp = new UtcDbTimestampType();
251+
247252
/// <summary>
248253
/// NHibernate TrueFalse type
249254
/// </summary>

0 commit comments

Comments
 (0)