Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions api/src/org/labkey/api/data/dialect/BasePostgreSqlDialect.java
Original file line number Diff line number Diff line change
Expand Up @@ -937,10 +937,27 @@ public boolean supportsIsNumeric()
@Override
public SQLFragment isNumericExpr(SQLFragment expression)
{
// 1/0, matching what SQL Server's ISNUMERIC() passthrough returns.
return new SQLFragment("(CASE WHEN CAST((").append(expression)
.append(") AS TEXT) ~ '^[+-]?([0-9]+([.][0-9]*)?|[.][0-9]+)$' THEN 1 ELSE 0 END)");
}

@Override
public SQLFragment weekIsoExpr(SQLFragment expression)
{
return new SQLFragment("CAST(EXTRACT(week FROM (").append(expression).append(")) AS INTEGER)");
}

@Override
public SQLFragment weekUsExpr(SQLFragment expression)
{
// Week 1 is whatever week contains Jan 1, and weeks start Sunday: (day of year + Sunday-based weekday of Jan 1 - 1) / 7, rounded down, plus one.
// The VALUES subquery names the argument so it is evaluated once; spelled out twice, a volatile argument could read either side of midnight.
return new SQLFragment("(SELECT CAST(FLOOR((EXTRACT(doy FROM v.d) + EXTRACT(dow FROM date_trunc('year', v.d)) - 1) / 7) + 1 AS INTEGER) FROM (VALUES (CAST(")
.append(expression)
.append(" AS TIMESTAMP))) AS v(d))");
}

private class PostgreSqlColumnMetaDataReader extends ColumnMetaDataReader
{
private final TableInfo _table;
Expand Down
18 changes: 18 additions & 0 deletions api/src/org/labkey/api/data/dialect/SqlDialect.java
Original file line number Diff line number Diff line change
Expand Up @@ -847,6 +847,24 @@ public SQLFragment isNumericExpr(SQLFragment expression)
throw new UnsupportedOperationException(getClass().getSimpleName() + " does not implement");
}

/**
* ISO 8601 week number, 1 to 53: weeks start Monday and week 1 is the week holding the year's first Thursday.
* A week belongs to whichever year owns its Thursday, so Jan 1-3 can number as week 52 or 53 of the prior year (2027-01-01 is week 53) and Dec 29-31 as week 1 of the next.
*/
public SQLFragment weekIsoExpr(SQLFragment expression)
{
throw new UnsupportedOperationException(getClass().getSimpleName() + " does not implement");
}

/**
* US week number, 1 to 54: weeks start Sunday and week 1 is whatever week holds Jan 1, so the first and last weeks of a year are both partial.
* Every date numbers within its own calendar year, so Jan 1 is always week 1. Matches SQL Server's DATEPART(week, x) under the default DATEFIRST 7.
*/
public SQLFragment weekUsExpr(SQLFragment expression)
{
throw new UnsupportedOperationException(getClass().getSimpleName() + " does not implement");
}

public void handleCreateDatabaseException(SQLException e) throws ServletException
{
throw(new ServletException("Can't create database", e));
Expand Down
65 changes: 58 additions & 7 deletions query/src/org/labkey/query/QueryServiceImpl.java
Original file line number Diff line number Diff line change
Expand Up @@ -3786,17 +3786,17 @@ public void testWhereClauseWithUnion()
@Test
public void testRightAndIsnumeric() throws SQLException
{
// Portable LabKey-SQL functions: right() dispatches via the JDBC {fn right} escape;
// isnumeric() emits ISNUMERIC(x) on SQL Server and a regex-based CASE on PostgreSQL.
// This test exercises both against whichever dialect the test container is using.
// Portable LabKey-SQL functions: right() dispatches via the JDBC {fn right} escape; isnumeric() yields 1/0
// -- ISNUMERIC(x) on SQL Server, a regex-based CASE on PostgreSQL -- so it is compared with = 1 rather than
// selected bare, which QuerySelect wraps in CASE WHEN on SQL Server and the wrap then has no predicate.
String sql =
"SELECT " +
" right('hello', 2) AS r1, " +
" right('xy', 5) AS r2, " +
" isnumeric('5') AS n1, " +
" isnumeric('-3.14') AS n2, " +
" isnumeric('abc') AS n3, " +
" isnumeric(NULL) AS n4 " +
" CASE WHEN isnumeric('5') = 1 THEN 1 ELSE 0 END AS n1, " +
" CASE WHEN isnumeric('-3.14') = 1 THEN 1 ELSE 0 END AS n2, " +
" CASE WHEN isnumeric('abc') = 1 THEN 1 ELSE 0 END AS n3, " +
" CASE WHEN isnumeric(NULL) = 1 THEN 1 ELSE 0 END AS n4 " +
"FROM core.Containers";

QueryDef qd = new QueryDef();
Expand All @@ -3821,5 +3821,56 @@ public void testRightAndIsnumeric() throws SQLException
assertEquals("isnumeric(NULL) on " + dialect, 0, results.getInt("n4"));
}
}

// 2026 (Jan 1 = Thursday) makes the two rules agree except on Sundays; 2027 (Jan 1 = Friday) puts them one
// apart every day and starts in the prior ISO year. A mid-year sample in a Mon-Thu year passes either way.
private static final String[] WEEK_DATES = {
"2026-01-01", // Thursday
"2026-01-03", // Saturday
"2026-01-04", // Sunday
"2027-01-01", // Friday
"2027-07-15" // Thursday
};

@Test
public void testWeekUs() throws SQLException
{
// Weeks start Sunday and week 1 holds Jan 1, matching SQL Server's DATEPART(week, x) under DATEFIRST 7.
assertWeeks("weekus", 1, 1, 2, 1, 29);
}

@Test
public void testWeekIso() throws SQLException
{
// ISO 8601: weeks start Monday and week 1 holds the year's first Thursday, so 2027-01-01 lands in 2026's week 53.
assertWeeks("weekiso", 1, 1, 1, 53, 28);
}

private void assertWeeks(String method, int... expected) throws SQLException
{
StringBuilder sql = new StringBuilder("SELECT ");
for (int i = 0; i < WEEK_DATES.length; i++)
sql.append(i > 0 ? ", " : "").append(method)
.append("(CAST('").append(WEEK_DATES[i]).append(" 00:00:00' AS TIMESTAMP)) AS w").append(i + 1);
sql.append(" FROM core.Containers");

QueryDef qd = new QueryDef();
qd.setSchema("core");
qd.setName("junit" + GUID.makeHash());
qd.setContainer(JunitUtil.getTestContainer().getId());
qd.setSql(sql.toString());
QueryDefinition qdef = new CustomQueryDefinitionImpl(TestContext.get().getUser(), JunitUtil.getTestContainer(), qd);
List<QueryException> errors = new ArrayList<>();
TableInfo t = qdef.getTable(errors, false);
String dialect = t == null ? "?" : t.getSqlDialect().getProductName();
assertTrue("Query parse errors on " + dialect + ": " + errors, errors.isEmpty());

try (Results results = new TableSelector(t).getResults())
{
assertTrue("Expected at least one row from core.Containers", results.next());
for (int i = 0; i < expected.length; i++)
assertEquals(method + "(" + WEEK_DATES[i] + ") on " + dialect, expected[i], results.getInt("w" + (i + 1)));
}
}
}
}
55 changes: 51 additions & 4 deletions query/src/org/labkey/query/sql/Method.java
Original file line number Diff line number Diff line change
Expand Up @@ -482,6 +482,25 @@ public MethodInfo getMethodInfo()
// Put new methods below this line and move above after they're documented, i.e.,
// added to https://www.labkey.org/Documentation/wiki-page.view?name=labkeySql

// week() is a passthrough, so its numbering is whatever the database does. These two are defined by LabKey and return the same number on either dialect.
// weekiso(x) -- ISO 8601, 1 to 53. Weeks start Monday and week 1 holds the year's first Thursday, so early January can number as the prior year's week 52 or 53.
// weekus(x) -- US, 1 to 54. Weeks start Sunday and week 1 holds Jan 1, so every date numbers within its own year.
labkeyMethod.put("weekiso", new Method("weekiso", JdbcType.INTEGER, 1, 1)
{
@Override
public MethodInfo getMethodInfo()
{
return new WeekIsoInfo();
}
});
labkeyMethod.put("weekus", new Method("weekus", JdbcType.INTEGER, 1, 1)
{
@Override
public MethodInfo getMethodInfo()
{
return new WeekUsInfo();
}
});

// ========== Don't document these ==========
labkeyMethod.put("__cte_two__", new Method(JdbcType.INTEGER, 0, 0)
Expand Down Expand Up @@ -1076,9 +1095,9 @@ public SQLFragment getSQL(SqlDialect dialect, SQLFragment[] arguments)
}
}

// Portable isnumeric() emits ISNUMERIC(x) on SQL Server and a regex-based CASE on PostgreSQL.
// Returns 1 for digit strings with an optional sign/decimal point, 0 otherwise.
// This is stricter than SQL Server's ISNUMERIC(), which also accepts formats like scientific notation.
// A regex-based CASE on PostgreSQL; SQL Server resolves isnumeric to the mssqlMethods passthrough and never
// reaches here. Yields 1/0 to match that passthrough, not a boolean as JdbcType.BOOLEAN suggests, so
// isnumeric(x) = 1 works on either database -- don't "fix" the dialects to emit predicates instead.
static class IsNumericInfo extends AbstractMethodInfo
{
IsNumericInfo()
Expand All @@ -1097,6 +1116,34 @@ public SQLFragment getSQL(SqlDialect dialect, SQLFragment[] arguments)
}
}

static class WeekIsoInfo extends AbstractMethodInfo
{
WeekIsoInfo()
{
super(JdbcType.INTEGER);
}

@Override
public SQLFragment getSQL(SqlDialect dialect, SQLFragment[] arguments)
{
return dialect.weekIsoExpr(arguments[0]);
}
}

static class WeekUsInfo extends AbstractMethodInfo
{
WeekUsInfo()
{
super(JdbcType.INTEGER);
}

@Override
public SQLFragment getSQL(SqlDialect dialect, SQLFragment[] arguments)
{
return dialect.weekUsExpr(arguments[0]);
}
}

static class VersionMethodInfo extends AbstractMethodInfo
{
VersionMethodInfo()
Expand Down Expand Up @@ -1904,7 +1951,7 @@ private static void addJsonPassthroughMethod(String name, JdbcType type, int min
mssqlMethods.put("charindex", new PassthroughMethod("charindex", JdbcType.INTEGER, 2, 3));
mssqlMethods.put("concat_ws", new PassthroughMethod("concat_ws", JdbcType.VARCHAR, 1, Integer.MAX_VALUE));
mssqlMethods.put("difference", new PassthroughMethod("difference", JdbcType.INTEGER, 2, 2));
// isnumeric is registered in labkeyMethod (portable across PostgreSQL and SQL Server)
mssqlMethods.put("isnumeric", new PassthroughMethod("isnumeric", JdbcType.BOOLEAN, 1, 1));
mssqlMethods.put("len", new PassthroughMethod("len", JdbcType.INTEGER, 1, 1));
mssqlMethods.put("patindex", new PassthroughMethod("patindex", JdbcType.INTEGER, 2, 2));
mssqlMethods.put("quotename", new PassthroughMethod("quotename", JdbcType.VARCHAR, 1, 2));
Expand Down
11 changes: 9 additions & 2 deletions query/src/org/labkey/query/sql/QueryPivot.java
Original file line number Diff line number Diff line change
Expand Up @@ -549,7 +549,9 @@ public Map<String, RelationColumn> getAllColumns()

String pivotName = makePivotAggName(name, pivotValue);
RelationColumn pvt = _makePivotedAggColumn(s, new FieldKey(null, pivotName), pivotValue);
_columns.put(pivotName, pvt);
// _makePivotedAggColumn() returns null when parse errors are present
if (null != pvt)
_columns.put(pivotName, pvt);
}
}
}
Expand Down Expand Up @@ -824,7 +826,12 @@ public SQLFragment getSql()
if (value instanceof QNull)
sql.append(" IS NULL");
else
sql.append("=").append(value.getSourceText());
{
// Let the constant write itself into sql, quoting through the dialect. Never splice in its source text:
// text carrying ';' or an unbalanced quote trips SQLFragment's guardrail.
sql.append("=");
((QExpr) value).appendSql(sql, _query);
}
sql.append(") THEN (").append(col.getValueSql()).append(") ELSE NULL END) AS ").appendIdentifier(alias);
comma = ",\n";
}
Expand Down
Loading