1515 */
1616package org .labkey .bigiron .mssql ;
1717
18+ import org .apache .logging .log4j .Logger ;
19+ import org .junit .Assert ;
20+ import org .junit .Test ;
1821import 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 ;
1925import 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 ;
2132import java .sql .SQLException ;
2233import java .sql .Statement ;
2334import java .sql .Timestamp ;
35+ import java .sql .Types ;
36+ import java .time .ZoneId ;
37+ import java .time .format .DateTimeFormatter ;
2438import 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- */
3142public 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