Skip to content

Commit c71d298

Browse files
authored
Junit test for timestamps on French SQL Server (#5997)
1 parent 60901c5 commit c71d298

3 files changed

Lines changed: 205 additions & 15 deletions

File tree

api/src/org/labkey/api/data/SqlExecutingSelector.java

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -56,12 +56,7 @@ public abstract class SqlExecutingSelector<FACTORY extends SqlFactory, SELECTOR
5656
// optimizations won't mutate the ExecutingSelector's externally set state.
5757
abstract protected FACTORY getSqlFactory(boolean isResultSet);
5858

59-
SqlExecutingSelector(DbScope scope)
60-
{
61-
this(scope, null);
62-
}
63-
64-
private SqlExecutingSelector(DbScope scope, Connection conn)
59+
protected SqlExecutingSelector(DbScope scope, Connection conn)
6560
{
6661
this(scope, conn, new QueryLogging());
6762
}

api/src/org/labkey/api/data/TableSelector.java

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@
3131

3232
import jakarta.servlet.http.HttpServletResponse;
3333
import java.io.IOException;
34+
import java.sql.Connection;
3435
import java.sql.ResultSet;
3536
import java.sql.SQLException;
3637
import java.util.ArrayList;
@@ -62,9 +63,9 @@ public class TableSelector extends SqlExecutingSelector<TableSelector.TableSqlFa
6263
private boolean _forceSortForDisplay = false;
6364

6465
// Primary constructor
65-
private TableSelector(@NotNull TableInfo table, Collection<ColumnInfo> columns, @Nullable Filter filter, @Nullable Sort sort, boolean stableColumnOrdering)
66+
protected TableSelector(@NotNull TableInfo table, @Nullable Connection conn, Collection<ColumnInfo> columns, @Nullable Filter filter, @Nullable Sort sort, boolean stableColumnOrdering)
6667
{
67-
super(table.getSchema().getScope());
68+
super(table.getSchema().getScope(), conn);
6869
_table = Objects.requireNonNull(table);
6970
_columns = columns;
7071
_filter = filter;
@@ -81,7 +82,7 @@ rely on column order (we return the values from the first one).
8182
*/
8283
public TableSelector(@NotNull TableInfo table, Collection<ColumnInfo> columns, @Nullable Filter filter, @Nullable Sort sort)
8384
{
84-
this(table, columns, filter, sort, isStableOrdered(columns));
85+
this(table, null, columns, filter, sort, isStableOrdered(columns));
8586
}
8687

8788
// Select all columns from a table, with no filter or sort
@@ -111,13 +112,13 @@ public TableSelector(@NotNull TableInfo table, @Nullable Filter filter, @Nullabl
111112
*/
112113
public TableSelector(@NotNull TableInfo table, Set<String> columnNames, @Nullable Filter filter, @Nullable Sort sort)
113114
{
114-
this(table, columnInfosList(table, columnNames), filter, sort, isStableOrdered(columnNames));
115+
this(table, null, columnInfosList(table, columnNames), filter, sort, isStableOrdered(columnNames));
115116
}
116117

117118
// Select a single column
118119
public TableSelector(@NotNull ColumnInfo column, @Nullable Filter filter, @Nullable Sort sort)
119120
{
120-
this(column.getParentTable(), Collections.singleton(column), filter, sort, true); // Single column is stable ordered
121+
this(column.getParentTable(), null, Collections.singleton(column), filter, sort, true); // Single column is stable ordered
121122
}
122123

123124
// Select a single column from all rows

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

Lines changed: 198 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -15,16 +15,31 @@
1515
*/
1616
package org.labkey.bigiron.mssql;
1717

18+
import jakarta.servlet.ServletException;
1819
import org.apache.commons.lang3.time.FastDateFormat;
1920
import org.apache.logging.log4j.Logger;
21+
import org.jetbrains.annotations.NotNull;
22+
import org.jetbrains.annotations.Nullable;
2023
import org.junit.Assert;
2124
import org.junit.Test;
25+
import org.labkey.api.data.ColumnInfo;
26+
import org.labkey.api.data.CompareType;
2227
import org.labkey.api.data.ConnectionWrapper;
28+
import org.labkey.api.data.CoreSchema;
2329
import 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;
2436
import org.labkey.api.data.SqlSelector;
37+
import org.labkey.api.data.TableInfo;
38+
import org.labkey.api.data.TableSelector;
2539
import org.labkey.api.data.dialect.SqlDialect;
2640
import org.labkey.api.data.dialect.StatementWrapper;
2741
import org.labkey.api.module.ModuleLoader;
42+
import org.labkey.api.query.FieldKey;
2843
import org.labkey.api.util.logging.LogHelper;
2944

3045
import java.sql.CallableStatement;
@@ -35,8 +50,10 @@
3550
import java.sql.Timestamp;
3651
import java.sql.Types;
3752
import java.util.Calendar;
53+
import java.util.Collections;
3854
import java.util.Date;
39-
import java.util.Map;
55+
import java.util.GregorianCalendar;
56+
import java.util.Set;
4057

4158
public 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

Comments
 (0)