1515 */
1616package org .labkey .bigiron .mssql ;
1717
18+ import jakarta .servlet .ServletException ;
1819import org .apache .commons .lang3 .time .FastDateFormat ;
1920import org .apache .logging .log4j .Logger ;
21+ import org .jetbrains .annotations .NotNull ;
22+ import org .jetbrains .annotations .Nullable ;
2023import org .junit .Assert ;
2124import org .junit .Test ;
25+ import org .labkey .api .data .ColumnInfo ;
26+ import org .labkey .api .data .CompareType ;
2227import org .labkey .api .data .ConnectionWrapper ;
28+ import org .labkey .api .data .CoreSchema ;
2329import org .labkey .api .data .DbScope ;
30+ import org .labkey .api .data .Filter ;
31+ import org .labkey .api .data .RuntimeSQLException ;
32+ import org .labkey .api .data .SQLFragment ;
33+ import org .labkey .api .data .SimpleFilter ;
34+ import org .labkey .api .data .Sort ;
35+ import org .labkey .api .data .SqlExecutor ;
2436import org .labkey .api .data .SqlSelector ;
37+ import org .labkey .api .data .TableInfo ;
38+ import org .labkey .api .data .TableSelector ;
2539import org .labkey .api .data .dialect .SqlDialect ;
2640import org .labkey .api .data .dialect .StatementWrapper ;
2741import org .labkey .api .module .ModuleLoader ;
42+ import org .labkey .api .query .FieldKey ;
2843import org .labkey .api .util .logging .LogHelper ;
2944
3045import java .sql .CallableStatement ;
3550import java .sql .Timestamp ;
3651import java .sql .Types ;
3752import java .util .Calendar ;
53+ import java .util .Collections ;
3854import java .util .Date ;
39- import java .util .Map ;
55+ import java .util .GregorianCalendar ;
56+ import java .util .Set ;
4057
4158public class MicrosoftSqlServer2016Dialect extends MicrosoftSqlServer2014Dialect
4259{
@@ -51,9 +68,16 @@ public void prepare(DbScope scope)
5168 {
5269 super .prepare (scope );
5370
54- Map <String , Object > map = new SqlSelector (scope , "SELECT language, date_format FROM sys.dm_exec_sessions WHERE session_id = @@spid" ).getMap ();
55- _language = (String ) map .get ("language" );
56- _dateFormat = (String ) map .get ("date_format" );
71+ try (Connection conn = scope .getConnection ())
72+ {
73+ LanguageSettings settings = getLanguageSettings (scope , conn );
74+ _language = settings .getLanguage ();
75+ _dateFormat = settings .getDate_format ();
76+ }
77+ catch (SQLException e )
78+ {
79+ throw new RuntimeSQLException (e );
80+ }
5781
5882 // This seems to be the only string format acceptable for sending Timestamps, but unfortunately it's ambiguous;
5983 // SQL Server interprets the "MM-dd" portion based on the database's regional settings. So we must query the
@@ -70,6 +94,48 @@ public void prepare(DbScope scope)
7094 LOG .info ("\n Language: {}\n DateFormat: {}" , _language , _dateFormat );
7195 }
7296
97+ // TODO: Turn this into a record on 24.11 (24.7 SqlSelector doesn't support records)
98+ public static class LanguageSettings
99+ {
100+ String _language ;
101+ String _date_format ;
102+
103+ public String getLanguage ()
104+ {
105+ return _language ;
106+ }
107+
108+ public void setLanguage (String language )
109+ {
110+ _language = language ;
111+ }
112+
113+ public String getDate_format ()
114+ {
115+ return _date_format ;
116+ }
117+
118+ public void setDate_format (String date_format )
119+ {
120+ _date_format = date_format ;
121+ }
122+
123+ @ Override
124+ public String toString ()
125+ {
126+ return "LanguageSettings{" +
127+ "_language='" + _language + '\'' +
128+ ", _date_format='" + _date_format + '\'' +
129+ '}' ;
130+ }
131+ }
132+
133+ private static LanguageSettings getLanguageSettings (DbScope scope , Connection conn )
134+ {
135+ return new SqlSelector (scope , conn , "SELECT language, date_format FROM sys.dm_exec_sessions WHERE session_id = @@spid" )
136+ .getObject (LanguageSettings .class );
137+ }
138+
73139 @ Override
74140 public StatementWrapper getStatementWrapper (ConnectionWrapper conn , Statement stmt )
75141 {
@@ -274,5 +340,133 @@ private void test(TimestampStatementWrapper wrapper, String expected, String tes
274340 Timestamp ts = Timestamp .valueOf (test );
275341 Assert .assertEquals (expected , wrapper .convert (ts ));
276342 }
343+
344+ @ Test
345+ public void testCompareClauses () throws SQLException , ServletException
346+ {
347+ // Issue 51472 pointed out issues with Timestamp conversions on French SQL Server. Primary fixes were in
348+ // the DateCompareClause subclasses, so put them through their paces here.
349+
350+ // Use a test scope that passes out an un-pooled connection so changing the language settings don't affect
351+ // connections in the pool. This also gives us a SqlDialect we can prepare every time we set the language.
352+ try (TestScope scope = new TestScope (DbScope .getLabKeyScope ()))
353+ {
354+ TableInfo containers = CoreSchema .getInstance ().getTableInfoContainers ();
355+ ColumnInfo created = containers .getColumn ("Created" );
356+
357+ try (Connection conn = scope .getConnection ())
358+ {
359+ setLanguage (scope , conn , "English" );
360+ testMultipleFilters (conn , containers , created .getFieldKey ());
361+
362+ if (scope .getSqlDialect ().isSqlServer ())
363+ {
364+ setLanguage (scope , conn , "French" );
365+ testMultipleFilters (conn , containers , created .getFieldKey ());
366+ }
367+ }
368+ }
369+ }
370+
371+ private static class TestScope extends DbScope implements AutoCloseable
372+ {
373+ private TestConnectionWrapper _connection = getWrapped ();
374+
375+ public TestScope (DbScope scope ) throws ServletException , SQLException
376+ {
377+ super (scope .getDataSourceName (), scope .getLabKeyDataSource ());
378+ }
379+
380+ @ Override
381+ public Connection getConnection ()
382+ {
383+ return _connection ;
384+ }
385+
386+ private TestConnectionWrapper getWrapped () throws SQLException
387+ {
388+ // Hand out an un-pooled connection since we might set language and don't want that to persist outside this test
389+ return new TestConnectionWrapper (getUnpooledConnection (), this );
390+ }
391+
392+ @ Override
393+ public void close () throws SQLException
394+ {
395+ _connection .closeConnection ();
396+ _connection = null ;
397+ }
398+
399+ private static class TestConnectionWrapper extends ConnectionWrapper
400+ {
401+ public TestConnectionWrapper (Connection conn , DbScope scope )
402+ {
403+ super (conn , scope , null , DbScope .ConnectionType .Transaction , null );
404+ }
405+
406+ @ Override
407+ public void close ()
408+ {
409+ // No-op
410+ }
411+
412+ private void closeConnection () throws SQLException
413+ {
414+ super .close ();
415+ }
416+ }
417+ }
418+
419+ private void setLanguage (DbScope scope , Connection conn , String language )
420+ {
421+ SqlDialect dialect = scope .getSqlDialect ();
422+ if (dialect .isSqlServer ())
423+ {
424+ new SqlExecutor (scope , conn ).execute ("SET LANGUAGE " + language );
425+ dialect .prepare (scope );
426+ LOG .info (getLanguageSettings (scope , conn ));
427+ }
428+ }
429+
430+ private void testMultipleFilters (Connection conn , TableInfo table , FieldKey date )
431+ {
432+ Calendar cal = new GregorianCalendar ();
433+ cal .add (Calendar .DATE , -30 );
434+ Date startDate = cal .getTime ();
435+
436+ testFilter (conn , table , date , startDate , CompareType .DATE_EQUAL );
437+ testFilter (conn , table , date , startDate , CompareType .DATE_NOT_EQUAL );
438+ testFilter (conn , table , date , startDate , CompareType .DATE_GTE );
439+ testFilter (conn , table , date , startDate , CompareType .DATE_GT );
440+ testFilter (conn , table , date , startDate , CompareType .DATE_LT );
441+ testFilter (conn , table , date , startDate , CompareType .DATE_LTE );
442+ }
443+
444+ // We don't care about the row counts, just that each query executes without any exceptions
445+ private void testFilter (Connection conn , TableInfo table , FieldKey fk , Object value , CompareType type )
446+ {
447+ SimpleFilter filter = new SimpleFilter (fk , value , type );
448+
449+ new TestTableSelector (table , conn , Collections .singleton (table .getColumn (fk )), filter , null ).getRowCount ();
450+
451+ // This mimics the query that UserManager.getActiveDaysCount() generates
452+ SQLFragment sql = new SQLFragment ("SELECT * FROM (SELECT CAST(" )
453+ .append (fk .getName ())
454+ .append (" AS DATE) AS " )
455+ .append (fk .getName ())
456+ .append (" FROM " )
457+ .append (table .getSelectName ())
458+ .append (") x " )
459+ .append (filter .getSQLFragment (table .getSqlDialect ()));
460+
461+ new SqlSelector (table .getSchema ().getScope (), conn , sql ).getRowCount ();
462+ }
463+
464+ private static class TestTableSelector extends TableSelector
465+ {
466+ public TestTableSelector (@ NotNull TableInfo table , @ NotNull Connection conn , Set <ColumnInfo > columns , @ Nullable Filter filter , @ Nullable Sort sort )
467+ {
468+ super (table , conn , columns , filter , sort , true );
469+ }
470+ }
277471 }
278472}
0 commit comments