Skip to content
46 changes: 46 additions & 0 deletions api/src/org/labkey/api/data/dialect/BasePostgreSqlDialect.java
Original file line number Diff line number Diff line change
Expand Up @@ -1117,6 +1117,8 @@ public SQLFragment formatJdbcFunction(String fn, SQLFragment... arguments)
return formatFunction(call, nativeFn, arguments);
else if (fn.equalsIgnoreCase("timestampdiff"))
return timestampdiff(arguments);
else if (fn.equalsIgnoreCase("timestampdiff2"))
return timestampdiff2(arguments);
else
return super.formatJdbcFunction(fn, arguments);
}
Expand Down Expand Up @@ -1157,6 +1159,50 @@ private SQLFragment timestampdiff(SQLFragment... arguments)
return super.formatJdbcFunction("timestampdiff", arguments);
}

/* Native PostgreSQL implementation for all 9 SQL_TSI intervals.
* This returns INTEGER for all intervals and never falls back to the JDBC escape.
*/
private SQLFragment timestampdiff2(SQLFragment... arguments)
{
String interval = arguments[0].getSQL();
SQLFragment start = arguments[1];
SQLFragment end = arguments[2];
// Compute whole elapsed months first, then derive quarter/year from that value so all larger
// intervals use the same truncation-toward-zero semantics as the epoch-based branches below.
SQLFragment wholeMonths = getWholeElapsedMonths(start, end);

return switch (interval)
{
case "SQL_TSI_YEAR" ->
new SQLFragment("TRUNC((").append(wholeMonths).append(")::NUMERIC / 12)::INT");
case "SQL_TSI_QUARTER" ->
new SQLFragment("TRUNC((").append(wholeMonths).append(")::NUMERIC / 3)::INT");
case "SQL_TSI_MONTH" ->
wholeMonths;
case "SQL_TSI_WEEK" ->
new SQLFragment("TRUNC(EXTRACT(EPOCH FROM (").append(end).append(") - (").append(start).append(")) / 604800)::INT");
case "SQL_TSI_DAY" ->
new SQLFragment("TRUNC(EXTRACT(EPOCH FROM (").append(end).append(") - (").append(start).append(")) / 86400)::INT");
case "SQL_TSI_HOUR" ->
new SQLFragment("TRUNC(EXTRACT(EPOCH FROM (").append(end).append(") - (").append(start).append(")) / 3600)::INT");
case "SQL_TSI_MINUTE" ->
new SQLFragment("TRUNC(EXTRACT(EPOCH FROM (").append(end).append(") - (").append(start).append(")) / 60)::INT");
case "SQL_TSI_SECOND" ->
new SQLFragment("TRUNC(EXTRACT(EPOCH FROM (").append(end).append(") - (").append(start).append(")))::INT");
case "SQL_TSI_FRAC_SECOND" ->
new SQLFragment("TRUNC(EXTRACT(EPOCH FROM (").append(end).append(") - (").append(start).append(")) * 1000)::BIGINT");
default -> throw new IllegalArgumentException("Unsupported interval for timestampdiff2: " + interval);
};
}

private SQLFragment getWholeElapsedMonths(SQLFragment start, SQLFragment end)
{
// AGE() normalizes the symbolic year/month/day components for both positive and negative spans.
SQLFragment age = new SQLFragment("AGE((").append(end).append("), (").append(start).append("))");
return new SQLFragment("((EXTRACT(YEAR FROM ").append(age).append(") * 12) + EXTRACT(MONTH FROM ").append(age)
.append("))::INT");
}

@Override
public boolean supportsBatchGeneratedKeys()
{
Expand Down
2 changes: 2 additions & 0 deletions api/src/org/labkey/api/data/dialect/SqlDialect.java
Original file line number Diff line number Diff line change
Expand Up @@ -2107,6 +2107,8 @@ public SQLFragment formatFunction(SQLFragment target, String fn, SQLFragment...

public SQLFragment formatJdbcFunction(String fn, SQLFragment... arguments)
{
if (fn.equalsIgnoreCase("timestampdiff2"))
fn = "timestampdiff";
SQLFragment ret = new SQLFragment();
ret.append("{fn ");
formatFunction(ret, fn, arguments);
Expand Down
26 changes: 26 additions & 0 deletions query/src/org/labkey/query/QueryTestCase.jsp
Original file line number Diff line number Diff line change
Expand Up @@ -777,6 +777,32 @@ d,seven,twelve,day,month,date,duration,guid
new MethodSqlTest("SELECT CAST(TIMESTAMPDIFF(SQL_TSI_DAY, CAST('31 Jan 2004' AS TIMESTAMP), CAST('01 Jan 2003' AS TIMESTAMP)) AS INTEGER)", JdbcType.INTEGER, -395),
// NOTE: SQL_TSI_WEEK, SQL_TSI_MONTH, SQL_TSI_QUARTER, and SQL_TSI_YEAR are NYI in PostsgreSQL TIMESTAMPDIFF

// timestampdiff2 - native PostgreSQL implementation for all intervals, returns INTEGER
new MethodSqlTest("SELECT TIMESTAMPDIFF2(SQL_TSI_SECOND, CAST('01 Jan 2004 5:00' AS TIMESTAMP), CAST('01 Jan 2004 6:00' AS TIMESTAMP))", JdbcType.INTEGER, 3600),
new MethodSqlTest("SELECT TIMESTAMPDIFF2(SQL_TSI_MINUTE, CAST('01 Jan 2003' AS TIMESTAMP), CAST('01 Jan 2004' AS TIMESTAMP))", JdbcType.INTEGER, 525600),
new MethodSqlTest("SELECT TIMESTAMPDIFF2(SQL_TSI_MINUTE, CAST('01 Jan 2004' AS TIMESTAMP), CAST('01 Jan 2005' AS TIMESTAMP))", JdbcType.INTEGER, 527040),
new MethodSqlTest("SELECT TIMESTAMPDIFF2(SQL_TSI_HOUR, CAST('01 Jan 2003' AS TIMESTAMP), CAST('01 Jan 2004' AS TIMESTAMP))", JdbcType.INTEGER, 8760),
new MethodSqlTest("SELECT TIMESTAMPDIFF2(SQL_TSI_HOUR, CAST('01 Jan 2004' AS TIMESTAMP), CAST('01 Jan 2005' AS TIMESTAMP))", JdbcType.INTEGER, 8784),
new MethodSqlTest("SELECT TIMESTAMPDIFF2(SQL_TSI_DAY, CAST('01 Jan 2003' AS TIMESTAMP), CAST('31 Jan 2004' AS TIMESTAMP))", JdbcType.INTEGER, 395),
new MethodSqlTest("SELECT TIMESTAMPDIFF2(SQL_TSI_DAY, CAST('31 Jan 2004' AS TIMESTAMP), CAST('01 Jan 2003' AS TIMESTAMP))", JdbcType.INTEGER, -395),
new MethodSqlTest("SELECT TIMESTAMPDIFF2(SQL_TSI_WEEK, CAST('01 Jan 2003' AS TIMESTAMP), CAST('22 Jan 2003' AS TIMESTAMP))", JdbcType.INTEGER, 3),
new MethodSqlTest("SELECT TIMESTAMPDIFF2(SQL_TSI_WEEK, CAST('22 Jan 2003' AS TIMESTAMP), CAST('01 Jan 2003' AS TIMESTAMP))", JdbcType.INTEGER, -3),
new MethodSqlTest("SELECT TIMESTAMPDIFF2(SQL_TSI_MONTH, CAST('01 Jan 2003' AS TIMESTAMP), CAST('01 Apr 2003' AS TIMESTAMP))", JdbcType.INTEGER, 3),
new MethodSqlTest("SELECT TIMESTAMPDIFF2(SQL_TSI_MONTH, CAST('01 Feb 2003 00:00' AS TIMESTAMP), CAST('31 Jan 2003 23:59' AS TIMESTAMP))", JdbcType.INTEGER, 0),
new MethodSqlTest("SELECT TIMESTAMPDIFF2(SQL_TSI_MONTH, CAST('15 Jan 2003' AS TIMESTAMP), CAST('14 Apr 2003' AS TIMESTAMP))", JdbcType.INTEGER, 2),
new MethodSqlTest("SELECT TIMESTAMPDIFF2(SQL_TSI_MONTH, CAST('15 Jan 2003' AS TIMESTAMP), CAST('15 Apr 2003' AS TIMESTAMP))", JdbcType.INTEGER, 3),
new MethodSqlTest("SELECT TIMESTAMPDIFF2(SQL_TSI_MONTH, CAST('15 Apr 2003' AS TIMESTAMP), CAST('14 Jan 2003' AS TIMESTAMP))", JdbcType.INTEGER, -3),
new MethodSqlTest("SELECT TIMESTAMPDIFF2(SQL_TSI_QUARTER, CAST('01 Jan 2003' AS TIMESTAMP), CAST('01 Oct 2003' AS TIMESTAMP))", JdbcType.INTEGER, 3),
new MethodSqlTest("SELECT TIMESTAMPDIFF2(SQL_TSI_QUARTER, CAST('15 Jan 2003' AS TIMESTAMP), CAST('14 Oct 2003' AS TIMESTAMP))", JdbcType.INTEGER, 2),
new MethodSqlTest("SELECT TIMESTAMPDIFF2(SQL_TSI_QUARTER, CAST('15 Jan 2003' AS TIMESTAMP), CAST('15 Oct 2003' AS TIMESTAMP))", JdbcType.INTEGER, 3),
new MethodSqlTest("SELECT TIMESTAMPDIFF2(SQL_TSI_QUARTER, CAST('15 Oct 2003' AS TIMESTAMP), CAST('14 Jan 2003' AS TIMESTAMP))", JdbcType.INTEGER, -3),
new MethodSqlTest("SELECT TIMESTAMPDIFF2(SQL_TSI_YEAR, CAST('01 Jan 2003' AS TIMESTAMP), CAST('01 Jan 2006' AS TIMESTAMP))", JdbcType.INTEGER, 3),
new MethodSqlTest("SELECT TIMESTAMPDIFF2(SQL_TSI_YEAR, CAST('15 Jan 2003' AS TIMESTAMP), CAST('14 Jan 2006' AS TIMESTAMP))", JdbcType.INTEGER, 2),
new MethodSqlTest("SELECT TIMESTAMPDIFF2(SQL_TSI_YEAR, CAST('15 Jan 2003' AS TIMESTAMP), CAST('15 Jan 2006' AS TIMESTAMP))", JdbcType.INTEGER, 3),
new MethodSqlTest("SELECT TIMESTAMPDIFF2(SQL_TSI_YEAR, CAST('14 Jan 2006' AS TIMESTAMP), CAST('15 Jan 2003' AS TIMESTAMP))", JdbcType.INTEGER, -2),
new MethodSqlTest("SELECT TIMESTAMPDIFF2(SQL_TSI_FRAC_SECOND, CAST('01 Jan 2004 5:00:00' AS TIMESTAMP), CAST('01 Jan 2004 5:00:01' AS TIMESTAMP))", JdbcType.INTEGER, 1000),
new MethodSqlTest("SELECT TIMESTAMPDIFF2(SQL_TSI_FRAC_SECOND, CAST('01 Jan 2004' AS TIMESTAMP), CAST('31 Jan 2004' AS TIMESTAMP))", JdbcType.INTEGER, 2592000000L),

new MethodSqlTest("SELECT UCASE('Fred')", JdbcType.VARCHAR, "FRED"),
new MethodSqlTest("SELECT UPPER('fred')", JdbcType.VARCHAR, "FRED"),
new MethodSqlTest("SELECT USERID()", JdbcType.INTEGER, () -> TestContext.get().getUser().getUserId()),
Expand Down
8 changes: 8 additions & 0 deletions query/src/org/labkey/query/sql/Method.java
Original file line number Diff line number Diff line change
Expand Up @@ -428,6 +428,14 @@ public MethodInfo getMethodInfo()
return new TimestampInfo(this);
}
});
labkeyMethod.put("timestampdiff2", new Method("timestampdiff2", JdbcType.INTEGER, 3, 3)
{
@Override
public MethodInfo getMethodInfo()
{
return new TimestampInfo(this);
}
});
labkeyMethod.put("truncate", new JdbcMethod("truncate", JdbcType.DOUBLE, 2, 2));
labkeyMethod.put("ucase", new JdbcMethod("ucase", JdbcType.VARCHAR, 1, 1));
labkeyMethod.put("upper", new JdbcMethod("ucase", JdbcType.VARCHAR, 1, 1));
Expand Down
4 changes: 3 additions & 1 deletion query/src/org/labkey/query/sql/QuerySelect.java
Original file line number Diff line number Diff line change
Expand Up @@ -2264,7 +2264,9 @@ SQLFragment getInternalSql()
QExpr expr = getResolvedField();

// NOTE SqlServer does not like predicates (A=B) in select list, try to help out
if (expr instanceof QMethodCall && expr.getJdbcType() == JdbcType.BOOLEAN && b.getDialect().isSqlServer())
// Exclude CAST/CONVERT expressions — they produce BIT values, not boolean predicates
if (expr instanceof QMethodCall mc && mc.getJdbcType() == JdbcType.BOOLEAN && b.getDialect().isSqlServer()
&& !(mc.getMethod(b.getDialect()) instanceof Method.ConvertInfo))
{
b.append("CASE WHEN (");
expr.appendSql(b, _query);
Expand Down
3 changes: 2 additions & 1 deletion query/src/org/labkey/query/sql/SqlParser.java
Original file line number Diff line number Diff line change
Expand Up @@ -995,7 +995,7 @@ else if (divisorType==NUM_DOUBLE || divisorType==NUM_FLOAT || divisorType==NUM_I
}
exprList._replaceChildren(new LinkedList<>(List.of(valueExpression, type)));
}
else if (name.equals("timestampadd") || name.equals("timestampdiff"))
else if (name.equals("timestampadd") || name.equals("timestampdiff") || name.equals("timestampdiff2"))
{
if (!(exprList instanceof QExprList) || exprList.childList().size() != 3)
{
Expand Down Expand Up @@ -1945,6 +1945,7 @@ class delete elements fetch indices insert into limit new set update versioned b
"SELECT TIMESTAMPDIFF(SQL_TSI_SECOND,a,b), TIMESTAMPDIFF(SECOND,a,b), TIMESTAMPDIFF('SQL_TSI_DAY',a,b), TIMESTAMPDIFF('DAY',a,b) FROM R",
"SELECT TIMESTAMPDIFF('SQL_TSI_Second',a,b), TIMESTAMPDIFF('Second',a,b), TIMESTAMPDIFF('SQL_TSI_Day',a,b), TIMESTAMPDIFF('Day',a,b) FROM R",
"SELECT TIMESTAMPADD(SQL_TSI_SECOND,1,b), TIMESTAMPADD(SECOND,1,b), TIMESTAMPADD('SQL_TSI_DAY',1,b), TIMESTAMPADD('DAY',1,b) FROM R",
"SELECT TIMESTAMPDIFF2(SQL_TSI_SECOND,a,b), TIMESTAMPDIFF2('SQL_TSI_DAY',a,b), TIMESTAMPDIFF2('MONTH',a,b), TIMESTAMPDIFF2('YEAR',a,b) FROM R",

"SELECT (SELECT value FROM S WHERE S.x=R.x) AS V FROM R",
"SELECT R.value AS V FROM R WHERE R.y > (SELECT MAX(S.y) FROM S WHERE S.x=R.x)",
Expand Down