Skip to content

Commit 69ab456

Browse files
authored
Fix SQL Server timestamp handling with non-US date formats (#5805)
1 parent d429c40 commit 69ab456

2 files changed

Lines changed: 141 additions & 48 deletions

File tree

bigiron/src/org/labkey/bigiron/BigIronModule.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
import org.labkey.api.module.ModuleContext;
2525
import org.labkey.api.view.WebPartFactory;
2626
import org.labkey.bigiron.mssql.GroupConcatInstallationManager;
27+
import org.labkey.bigiron.mssql.MicrosoftSqlServer2016Dialect;
2728
import org.labkey.bigiron.mssql.MicrosoftSqlServerDialectFactory;
2829
import org.labkey.bigiron.mssql.MicrosoftSqlServerVersion;
2930
import org.labkey.bigiron.mssql.synonym.SynonymTestCase;
@@ -87,6 +88,7 @@ public Set<Class> getIntegrationTests()
8788
{
8889
return Set.of(
8990
GroupConcatInstallationManager.TestCase.class,
91+
MicrosoftSqlServer2016Dialect.TestCase.class,
9092
MicrosoftSqlServerVersion.TestCase.class,
9193
OracleVersion.TestCase.class,
9294
SynonymTestCase.class

bigiron/src/org/labkey/bigiron/mssql/MicrosoftSqlServer2016Dialect.java

Lines changed: 139 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -15,21 +15,62 @@
1515
*/
1616
package org.labkey.bigiron.mssql;
1717

18+
import org.apache.logging.log4j.Logger;
19+
import org.junit.Assert;
20+
import org.junit.Test;
1821
import org.labkey.api.data.ConnectionWrapper;
22+
import org.labkey.api.data.DbScope;
23+
import org.labkey.api.data.SqlSelector;
24+
import org.labkey.api.data.dialect.SqlDialect;
1925
import org.labkey.api.data.dialect.StatementWrapper;
26+
import org.labkey.api.module.ModuleLoader;
27+
import org.labkey.api.util.logging.LogHelper;
2028

29+
import java.sql.CallableStatement;
30+
import java.sql.Connection;
31+
import java.sql.PreparedStatement;
2132
import java.sql.SQLException;
2233
import java.sql.Statement;
2334
import java.sql.Timestamp;
35+
import java.sql.Types;
36+
import java.time.ZoneId;
37+
import java.time.format.DateTimeFormatter;
2438
import java.util.Calendar;
39+
import java.util.Date;
40+
import java.util.Map;
2541

26-
/**
27-
* User: adam
28-
* Date: 8/11/2015
29-
* Time: 1:19 PM
30-
*/
3142
public class MicrosoftSqlServer2016Dialect extends MicrosoftSqlServer2014Dialect
3243
{
44+
private static final Logger LOG = LogHelper.getLogger(MicrosoftSqlServer2016Dialect.class, "SQL Server settings");
45+
46+
private volatile String _language = null;
47+
private volatile String _dateFormat = null;
48+
private volatile DateTimeFormatter _timestampFormatter = null;
49+
50+
@Override
51+
public void prepare(DbScope scope)
52+
{
53+
super.prepare(scope);
54+
55+
Map<String, Object> map = new SqlSelector(scope, "SELECT language, date_format FROM sys.dm_exec_sessions WHERE session_id = @@spid").getMap();
56+
_language = (String) map.get("language");
57+
_dateFormat = (String) map.get("date_format");
58+
59+
// This seems to be the only string format acceptable for sending Timestamps, but unfortunately it's ambiguous;
60+
// SQL Server interprets the "MM-dd" portion based on the database's regional settings. So we must query the
61+
// current date format and switch the formatter pattern based on what we find. See Issue 51129.
62+
String mdFormat = switch (_dateFormat)
63+
{
64+
case "mdy" -> "MM-dd";
65+
case "dmy" -> "dd-MM";
66+
default -> throw new IllegalStateException("Unsupported date format: " + _dateFormat);
67+
};
68+
69+
_timestampFormatter = DateTimeFormatter.ofPattern("yyyy-" + mdFormat + " HH:mm:ss.SSS");
70+
71+
LOG.info("\n Language: {}\n DateFormat: {}", _language, _dateFormat);
72+
}
73+
3374
@Override
3475
public StatementWrapper getStatementWrapper(ConnectionWrapper conn, Statement stmt)
3576
{
@@ -44,17 +85,15 @@ public StatementWrapper getStatementWrapper(ConnectionWrapper conn, Statement st
4485

4586
/**
4687
* Per the SQL Server JDBC driver docs at <a href="https://docs.microsoft.com/en-us/sql/connect/jdbc/using-basic-data-types?view=sql-server-ver16">...</a>
47-
*
48-
* Note that java.sql.Timestamp values can no longer be used to compare values from a datetime column starting
88+
* "Note that java.sql.Timestamp values can no longer be used to compare values from a datetime column starting
4989
* from SQL Server 2016. This limitation is due to a server-side change that converts datetime to datetime2
5090
* differently, resulting in non-equitable values. The workaround to this issue is to either change datetime
5191
* columns to datetime2(3), use String instead of java.sql.Timestamp, or change database compatibility level
52-
* to 120 or below.
53-
*
54-
*
55-
* java.sql.Timestamp.toString() includes the nanos in a ISO 8061-like format
92+
* to 120 or below." We can't change column types in external schemas, and we don't want a low compatibility level,
93+
* so we send Timestamps as Strings. SQL Server is very picky about this format; for example, Timestamp.toString(),
94+
* which is basically ISO, is actually ambiguous and fails if language is French (e.g.). See Issue 51129.
5695
*/
57-
private static class TimestampStatementWrapper extends StatementWrapper
96+
private class TimestampStatementWrapper extends StatementWrapper
5897
{
5998
public TimestampStatementWrapper(ConnectionWrapper conn, Statement stmt)
6099
{
@@ -67,12 +106,11 @@ public TimestampStatementWrapper(ConnectionWrapper conn, Statement stmt, String
67106
}
68107

69108
@Override
70-
public void setTimestamp(String parameterName, Timestamp x)
71-
throws SQLException
109+
public void setTimestamp(String parameterName, Timestamp x) throws SQLException
72110
{
73111
if (x != null)
74112
{
75-
setObject(parameterName, x.toString());
113+
setObject(parameterName, convert(x));
76114
}
77115
else
78116
{
@@ -81,12 +119,11 @@ public void setTimestamp(String parameterName, Timestamp x)
81119
}
82120

83121
@Override
84-
public void setTimestamp(String parameterName, Timestamp x, Calendar cal)
85-
throws SQLException
122+
public void setTimestamp(String parameterName, Timestamp x, Calendar cal) throws SQLException
86123
{
87124
if (x != null)
88125
{
89-
setObject(parameterName, x.toString());
126+
setObject(parameterName, convert(x));
90127
}
91128
else
92129
{
@@ -95,12 +132,11 @@ public void setTimestamp(String parameterName, Timestamp x, Calendar cal)
95132
}
96133

97134
@Override
98-
public void setTimestamp(int parameterIndex, Timestamp x)
99-
throws SQLException
135+
public void setTimestamp(int parameterIndex, Timestamp x) throws SQLException
100136
{
101137
if (x != null)
102138
{
103-
setObject(parameterIndex, x.toString());
139+
setObject(parameterIndex, convert(x));
104140
}
105141
else
106142
{
@@ -109,12 +145,11 @@ public void setTimestamp(int parameterIndex, Timestamp x)
109145
}
110146

111147
@Override
112-
public void setTimestamp(int parameterIndex, Timestamp x, Calendar cal)
113-
throws SQLException
148+
public void setTimestamp(int parameterIndex, Timestamp x, Calendar cal) throws SQLException
114149
{
115150
if (x != null)
116151
{
117-
setObject(parameterIndex, x.toString());
152+
setObject(parameterIndex, convert(x));
118153
}
119154
else
120155
{
@@ -123,51 +158,107 @@ public void setTimestamp(int parameterIndex, Timestamp x, Calendar cal)
123158
}
124159

125160
@Override
126-
public void setObject(int parameterIndex, Object x, int targetSqlType, int scale)
127-
throws SQLException
161+
public void setObject(int parameterIndex, Object x, int targetSqlType, int scale) throws SQLException
128162
{
129-
x = x instanceof Timestamp ? x.toString() : x;
130-
super.setObject(parameterIndex, x, targetSqlType, scale);
163+
if (targetSqlType == Types.TIMESTAMP)
164+
setObject(parameterIndex, x);
165+
else
166+
super.setObject(parameterIndex, x, targetSqlType, scale);
131167
}
132168

133169
@Override
134-
public void setObject(int parameterIndex, Object x, int targetSqlType)
135-
throws SQLException
170+
public void setObject(int parameterIndex, Object x, int targetSqlType) throws SQLException
136171
{
137-
x = x instanceof Timestamp ? x.toString() : x;
138-
super.setObject(parameterIndex, x, targetSqlType);
172+
if (targetSqlType == Types.TIMESTAMP)
173+
setObject(parameterIndex, x);
174+
else
175+
super.setObject(parameterIndex, x, targetSqlType);
139176
}
140177

141178
@Override
142-
public void setObject(int parameterIndex, Object x)
143-
throws SQLException
179+
public void setObject(int parameterIndex, Object x) throws SQLException
144180
{
145-
x = x instanceof Timestamp ? x.toString() : x;
146-
super.setObject(parameterIndex, x);
181+
super.setObject(parameterIndex, convert(x));
147182
}
148183

149184
@Override
150-
public void setObject(String parameterName, Object x, int targetSqlType, int scale)
151-
throws SQLException
185+
public void setObject(String parameterName, Object x, int targetSqlType, int scale) throws SQLException
152186
{
153-
x = x instanceof Timestamp ? x.toString() : x;
154-
super.setObject(parameterName, x, targetSqlType, scale);
187+
if (targetSqlType == Types.TIMESTAMP)
188+
setObject(parameterName, x);
189+
else
190+
super.setObject(parameterName, x, targetSqlType, scale);
155191
}
156192

157193
@Override
158-
public void setObject(String parameterName, Object x, int targetSqlType)
159-
throws SQLException
194+
public void setObject(String parameterName, Object x, int targetSqlType) throws SQLException
160195
{
161-
x = x instanceof Timestamp ? x.toString() : x;
162-
super.setObject(parameterName, x, targetSqlType);
196+
if (targetSqlType == Types.TIMESTAMP)
197+
setObject(parameterName, x);
198+
else
199+
super.setObject(parameterName, x, targetSqlType);
163200
}
164201

165202
@Override
166-
public void setObject(String parameterName, Object x)
167-
throws SQLException
203+
public void setObject(String parameterName, Object x) throws SQLException
168204
{
169-
x = x instanceof Timestamp ? x.toString() : x;
170-
super.setObject(parameterName, x);
205+
super.setObject(parameterName, convert(x));
206+
}
207+
208+
private Object convert(Object x)
209+
{
210+
return x instanceof Timestamp ts ? convert(ts) : x;
211+
}
212+
213+
private String convert(Timestamp ts)
214+
{
215+
return _timestampFormatter.format(ts.toInstant().atZone(ZoneId.systemDefault()));
216+
}
217+
}
218+
219+
public static class TestCase
220+
{
221+
@Test
222+
public void testTimestamps()
223+
{
224+
DbScope scope = DbScope.getLabKeyScope();
225+
SqlDialect dialect = scope.getSqlDialect();
226+
227+
if (dialect.isSqlServer() && dialect instanceof MicrosoftSqlServer2016Dialect)
228+
{
229+
try (Connection conn = DbScope.getLabKeyScope().getConnection())
230+
{
231+
Timestamp ts = new Timestamp(new Date().getTime());
232+
Calendar cal = Calendar.getInstance();
233+
234+
try (PreparedStatement statement = conn.prepareStatement("SELECT ?"))
235+
{
236+
Assert.assertTrue(statement instanceof TimestampStatementWrapper);
237+
statement.setTimestamp(1, ts);
238+
statement.setTimestamp(1, ts, cal);
239+
statement.setObject(1, ts, Types.TIMESTAMP, 0);
240+
statement.setObject(1, ts, Types.TIMESTAMP);
241+
statement.setObject(1, ts);
242+
}
243+
244+
if (ModuleLoader.getInstance().hasModule("DataIntegration"))
245+
{
246+
try (CallableStatement statement = conn.prepareCall("{call etltest.etlTest(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)}"))
247+
{
248+
Assert.assertTrue(statement instanceof TimestampStatementWrapper);
249+
statement.setTimestamp("filterStartTimeStamp", ts);
250+
statement.setTimestamp("filterStartTimeStamp", ts, cal);
251+
statement.setObject("filterStartTimeStamp", ts, Types.TIMESTAMP, 0);
252+
statement.setObject("filterStartTimeStamp", ts, Types.TIMESTAMP);
253+
statement.setObject("filterStartTimeStamp", ts);
254+
}
255+
}
256+
}
257+
catch (SQLException e)
258+
{
259+
throw new RuntimeException(e);
260+
}
261+
}
171262
}
172263
}
173264
}

0 commit comments

Comments
 (0)