Skip to content

Commit a15a1a5

Browse files
fredericDelaportehazzik
authored andcommitted
NH-4084 - DbTimeStamp cause stale update exception
1 parent 7ac0206 commit a15a1a5

File tree

8 files changed

+46
-104
lines changed

8 files changed

+46
-104
lines changed

src/NHibernate.Test/Async/NHSpecificTest/NH1756/Fixture.cs

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -28,19 +28,19 @@ protected override bool AppliesTo(Dialect.Dialect dialect)
2828
protected override bool AppliesTo(ISessionFactoryImplementor factory)
2929
{
3030
// ODBC driver DateTime handling with SQL Server 2008+ Client is broken and forbids using it as a time stamp
31-
// generated on db-side.
31+
// custom generated on db-side.
3232
// Due to NH-3895, we have to force the scale on date-time parameters to 3 (3 significant fractional seconds digits)
3333
// when using ODBC + SQL Server 2008+, otherwise DateTime values having milliseconds will be rejected. But the SQL
3434
// Server DateTime does not have actually a one millisecond resolution (it has 3.333), causing ODBC to convert the
35-
// parameter to DateTime2. A DateTime value ending by 3ms (indeed 3.333) or 7ms (indeed 6.666) is
35+
// parameter to DateTime2 (yes it supports it, only its .Net classes do not).
36+
// A DateTime value ending by 3ms (indeed 3.333) or 7ms (indeed 6.666) is
3637
// to be transmitted as having 3ms or 7ms and will match if transmitted as a DateTime. But when transmitted as
3738
// DateTime2, it will no more be considered equal, causing the test to be flaky and failing two thirds of tries.
3839
// Example failing update captured with profiler:
3940
// exec sp_executesql N'UPDATE book SET name_column = @P1 WHERE id = @P2 AND version_column = @P3',
4041
// N'@P1 nvarchar(18),@P2 int,@P3 datetime2',N'modified test book',1,'2017-08-02 16:37:16.0630000'
4142
// Setting the scale to 2 still causes failure for two thirds of tries, due to 3ms/7ms being truncated in such case
42-
// with ODBC and SQL Server 2008+ Client, which is rejected bu ODBC.
43-
// (Affects DbVersionFixture too.)
43+
// with ODBC and SQL Server 2008+ Client, which is rejected by ODBC.
4444
return !(factory.ConnectionProvider.Driver is OdbcDriver);
4545
}
4646

@@ -115,4 +115,4 @@ public async Task SaveTransient_Then_Update_BugAsync()
115115
}
116116
}
117117
}
118-
}
118+
}

src/NHibernate.Test/Async/NHSpecificTest/NH392/Fixture.cs

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -46,15 +46,15 @@ public async Task UnsavedMinusOneNoNullReferenceExceptionAsync()
4646
}
4747
}
4848

49-
protected override void OnTearDown()
49+
protected override void OnTearDown()
5050
{
5151
using (ISession s = Sfi.OpenSession())
5252
{
5353
// s.Delete("from UnsavedValueMinusOne") loads then delete entities one by one, checking the version.
54-
// This fails with ODBC & Sql Server 2008+, see NH-1756 test case or DbVersionFixture for more details.
54+
// This fails with ODBC & Sql Server 2008+, see NH-1756 test case for more details.
5555
// Use an in-db query instead.
5656
s.CreateQuery("delete from UnsavedValueMinusOne").ExecuteUpdate();
5757
}
58-
}
58+
}
5959
}
60-
}
60+
}

src/NHibernate.Test/Async/VersionTest/Db/DbVersionFixture.cs

Lines changed: 1 addition & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,6 @@
1111
using System;
1212
using System.Collections;
1313
using System.Threading;
14-
using NHibernate.Driver;
15-
using NHibernate.Engine;
1614
using NUnit.Framework;
1715

1816
namespace NHibernate.Test.VersionTest.Db
@@ -30,25 +28,6 @@ protected override string MappingsAssembly
3028
get { return "NHibernate.Test"; }
3129
}
3230

33-
protected override bool AppliesTo(ISessionFactoryImplementor factory)
34-
{
35-
// ODBC driver DateTime handling with SQL Server 2008+ Client is broken and forbids using it as a time stamp
36-
// generated on db-side.
37-
// Due to NH-3895, we have to force the scale on date-time parameters to 3 (3 significant fractional seconds digits)
38-
// when using ODBC + SQL Server 2008+, otherwise DateTime values having milliseconds will be rejected. But the SQL
39-
// Server DateTime does not have actually a one millisecond resolution (it has 3.333), causing ODBC to convert the
40-
// parameter to DateTime2. A DateTime value ending by 3ms (indeed 3.333) or 7ms (indeed 6.666) is
41-
// to be transmitted as having 3ms or 7ms and will match if transmitted as a DateTime. But when transmitted as
42-
// DateTime2, it will no more be considered equal, causing the test to be flaky and failing two thirds of tries.
43-
// Example failing update captured with profiler:
44-
// exec sp_executesql N'UPDATE book SET name_column = @P1 WHERE id = @P2 AND version_column = @P3',
45-
// N'@P1 nvarchar(18),@P2 int,@P3 datetime2',N'modified test book',1,'2017-08-02 16:37:16.0630000'
46-
// Setting the scale to 2 still causes failure for two thirds of tries, due to 3ms/7ms being truncated in such case
47-
// with ODBC and SQL Server 2008+ Client, which is rejected bu ODBC.
48-
// (Affects NH-1756 too.)
49-
return !(factory.ConnectionProvider.Driver is OdbcDriver);
50-
}
51-
5231
[Test]
5332
public async System.Threading.Tasks.Task CollectionVersionAsync()
5433
{
@@ -143,4 +122,4 @@ public async System.Threading.Tasks.Task CollectionNoVersionAsync()
143122
s.Close();
144123
}
145124
}
146-
}
125+
}

src/NHibernate.Test/NHSpecificTest/NH1756/Fixture.cs

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -17,19 +17,19 @@ protected override bool AppliesTo(Dialect.Dialect dialect)
1717
protected override bool AppliesTo(ISessionFactoryImplementor factory)
1818
{
1919
// ODBC driver DateTime handling with SQL Server 2008+ Client is broken and forbids using it as a time stamp
20-
// generated on db-side.
20+
// custom generated on db-side.
2121
// Due to NH-3895, we have to force the scale on date-time parameters to 3 (3 significant fractional seconds digits)
2222
// when using ODBC + SQL Server 2008+, otherwise DateTime values having milliseconds will be rejected. But the SQL
2323
// Server DateTime does not have actually a one millisecond resolution (it has 3.333), causing ODBC to convert the
24-
// parameter to DateTime2. A DateTime value ending by 3ms (indeed 3.333) or 7ms (indeed 6.666) is
24+
// parameter to DateTime2 (yes it supports it, only its .Net classes do not).
25+
// A DateTime value ending by 3ms (indeed 3.333) or 7ms (indeed 6.666) is
2526
// to be transmitted as having 3ms or 7ms and will match if transmitted as a DateTime. But when transmitted as
2627
// DateTime2, it will no more be considered equal, causing the test to be flaky and failing two thirds of tries.
2728
// Example failing update captured with profiler:
2829
// exec sp_executesql N'UPDATE book SET name_column = @P1 WHERE id = @P2 AND version_column = @P3',
2930
// N'@P1 nvarchar(18),@P2 int,@P3 datetime2',N'modified test book',1,'2017-08-02 16:37:16.0630000'
3031
// Setting the scale to 2 still causes failure for two thirds of tries, due to 3ms/7ms being truncated in such case
31-
// with ODBC and SQL Server 2008+ Client, which is rejected bu ODBC.
32-
// (Affects DbVersionFixture too.)
32+
// with ODBC and SQL Server 2008+ Client, which is rejected by ODBC.
3333
return !(factory.ConnectionProvider.Driver is OdbcDriver);
3434
}
3535

@@ -104,4 +104,4 @@ public void SaveTransient_Then_Update_Bug()
104104
}
105105
}
106106
}
107-
}
107+
}

src/NHibernate.Test/NHSpecificTest/NH392/Fixture.cs

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -35,15 +35,15 @@ public void UnsavedMinusOneNoNullReferenceException()
3535
}
3636
}
3737

38-
protected override void OnTearDown()
38+
protected override void OnTearDown()
3939
{
4040
using (ISession s = Sfi.OpenSession())
4141
{
4242
// s.Delete("from UnsavedValueMinusOne") loads then delete entities one by one, checking the version.
43-
// This fails with ODBC & Sql Server 2008+, see NH-1756 test case or DbVersionFixture for more details.
43+
// This fails with ODBC & Sql Server 2008+, see NH-1756 test case for more details.
4444
// Use an in-db query instead.
4545
s.CreateQuery("delete from UnsavedValueMinusOne").ExecuteUpdate();
4646
}
47-
}
47+
}
4848
}
49-
}
49+
}

src/NHibernate.Test/VersionTest/Db/DbVersionFixture.cs

Lines changed: 1 addition & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,6 @@
11
using System;
22
using System.Collections;
33
using System.Threading;
4-
using NHibernate.Driver;
5-
using NHibernate.Engine;
64
using NUnit.Framework;
75

86
namespace NHibernate.Test.VersionTest.Db
@@ -20,25 +18,6 @@ protected override string MappingsAssembly
2018
get { return "NHibernate.Test"; }
2119
}
2220

23-
protected override bool AppliesTo(ISessionFactoryImplementor factory)
24-
{
25-
// ODBC driver DateTime handling with SQL Server 2008+ Client is broken and forbids using it as a time stamp
26-
// generated on db-side.
27-
// Due to NH-3895, we have to force the scale on date-time parameters to 3 (3 significant fractional seconds digits)
28-
// when using ODBC + SQL Server 2008+, otherwise DateTime values having milliseconds will be rejected. But the SQL
29-
// Server DateTime does not have actually a one millisecond resolution (it has 3.333), causing ODBC to convert the
30-
// parameter to DateTime2. A DateTime value ending by 3ms (indeed 3.333) or 7ms (indeed 6.666) is
31-
// to be transmitted as having 3ms or 7ms and will match if transmitted as a DateTime. But when transmitted as
32-
// DateTime2, it will no more be considered equal, causing the test to be flaky and failing two thirds of tries.
33-
// Example failing update captured with profiler:
34-
// exec sp_executesql N'UPDATE book SET name_column = @P1 WHERE id = @P2 AND version_column = @P3',
35-
// N'@P1 nvarchar(18),@P2 int,@P3 datetime2',N'modified test book',1,'2017-08-02 16:37:16.0630000'
36-
// Setting the scale to 2 still causes failure for two thirds of tries, due to 3ms/7ms being truncated in such case
37-
// with ODBC and SQL Server 2008+ Client, which is rejected bu ODBC.
38-
// (Affects NH-1756 too.)
39-
return !(factory.ConnectionProvider.Driver is OdbcDriver);
40-
}
41-
4221
[Test]
4322
public void CollectionVersion()
4423
{
@@ -133,4 +112,4 @@ public void CollectionNoVersion()
133112
s.Close();
134113
}
135114
}
136-
}
115+
}

src/NHibernate/Async/Type/DbTimestampType.cs

Lines changed: 18 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,6 @@
1111
using System;
1212
using System.Data;
1313
using System.Data.Common;
14-
1514
using NHibernate.Engine;
1615
using NHibernate.Exceptions;
1716
using NHibernate.Impl;
@@ -26,51 +25,34 @@ public partial class DbTimestampType : AbstractDateTimeType
2625
{
2726

2827
/// <inheritdoc />
29-
public override Task<object> SeedAsync(ISessionImplementor session, CancellationToken cancellationToken)
28+
public override async Task<object> SeedAsync(ISessionImplementor session, CancellationToken cancellationToken)
3029
{
31-
if (cancellationToken.IsCancellationRequested)
32-
{
33-
return Task.FromCanceled<object>(cancellationToken);
34-
}
35-
try
30+
cancellationToken.ThrowIfCancellationRequested();
31+
if (session == null)
3632
{
37-
if (session == null)
38-
{
39-
log.Debug("incoming session was null; using current application host time");
40-
return base.SeedAsync(null, cancellationToken);
41-
}
42-
if (!session.Factory.Dialect.SupportsCurrentTimestampSelection)
43-
{
44-
log.Info("falling back to application host based timestamp, as dialect does not support current timestamp selection");
45-
return base.SeedAsync(session, cancellationToken);
46-
}
47-
return GetCurrentTimestampAsync(session, cancellationToken);
33+
log.Debug("incoming session was null; using current application host time");
34+
return await (base.SeedAsync(null, cancellationToken)).ConfigureAwait(false);
4835
}
49-
catch (Exception ex)
36+
if (!session.Factory.Dialect.SupportsCurrentTimestampSelection)
5037
{
51-
return Task.FromException<object>(ex);
38+
log.Info("falling back to application host based timestamp, as dialect does not support current timestamp selection");
39+
return await (base.SeedAsync(session, cancellationToken)).ConfigureAwait(false);
5240
}
41+
return await (GetCurrentTimestampAsync(session, cancellationToken)).ConfigureAwait(false);
5342
}
5443

55-
private Task<object> GetCurrentTimestampAsync(ISessionImplementor session, CancellationToken cancellationToken)
44+
protected virtual async Task<DateTime> GetCurrentTimestampAsync(ISessionImplementor session, CancellationToken cancellationToken)
5645
{
57-
if (cancellationToken.IsCancellationRequested)
58-
{
59-
return Task.FromCanceled<object>(cancellationToken);
60-
}
61-
try
62-
{
63-
Dialect.Dialect dialect = session.Factory.Dialect;
64-
string timestampSelectString = dialect.CurrentTimestampSelectString;
65-
return UsePreparedStatementAsync(timestampSelectString, session, cancellationToken);
66-
}
67-
catch (Exception ex)
68-
{
69-
return Task.FromException<object>(ex);
70-
}
46+
cancellationToken.ThrowIfCancellationRequested();
47+
var dialect = session.Factory.Dialect;
48+
// Need to round notably for Sql Server DateTime with Odbc, which has a 3.33ms resolution,
49+
// causing stale data update failure 2/3 of times if not rounded to 10ms.
50+
return Round(
51+
await (UsePreparedStatementAsync(dialect.CurrentTimestampSelectString, session, cancellationToken)).ConfigureAwait(false),
52+
dialect.TimestampResolutionInTicks);
7153
}
7254

73-
protected virtual async Task<object> UsePreparedStatementAsync(string timestampSelectString, ISessionImplementor session, CancellationToken cancellationToken)
55+
protected virtual async Task<DateTime> UsePreparedStatementAsync(string timestampSelectString, ISessionImplementor session, CancellationToken cancellationToken)
7456
{
7557
cancellationToken.ThrowIfCancellationRequested();
7658
var tsSelect = new SqlString(timestampSelectString);

src/NHibernate/Type/DbTimestampType.cs

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
using System;
22
using System.Data;
33
using System.Data.Common;
4-
54
using NHibernate.Engine;
65
using NHibernate.Exceptions;
76
using NHibernate.Impl;
@@ -39,14 +38,17 @@ public override object Seed(ISessionImplementor session)
3938
return GetCurrentTimestamp(session);
4039
}
4140

42-
private object GetCurrentTimestamp(ISessionImplementor session)
41+
protected virtual DateTime GetCurrentTimestamp(ISessionImplementor session)
4342
{
44-
Dialect.Dialect dialect = session.Factory.Dialect;
45-
string timestampSelectString = dialect.CurrentTimestampSelectString;
46-
return UsePreparedStatement(timestampSelectString, session);
43+
var dialect = session.Factory.Dialect;
44+
// Need to round notably for Sql Server DateTime with Odbc, which has a 3.33ms resolution,
45+
// causing stale data update failure 2/3 of times if not rounded to 10ms.
46+
return Round(
47+
UsePreparedStatement(dialect.CurrentTimestampSelectString, session),
48+
dialect.TimestampResolutionInTicks);
4749
}
4850

49-
protected virtual object UsePreparedStatement(string timestampSelectString, ISessionImplementor session)
51+
protected virtual DateTime UsePreparedStatement(string timestampSelectString, ISessionImplementor session)
5052
{
5153
var tsSelect = new SqlString(timestampSelectString);
5254
DbCommand ps = null;

0 commit comments

Comments
 (0)