Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
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
27 changes: 27 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,33 @@ 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('31 Jan 2003 23:59' AS TIMESTAMP), CAST('01 Feb 2003 00:00' AS TIMESTAMP))", JdbcType.INTEGER, 0),
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