From 09e27159dd7f0fe8e2a9cf07f885e734c0459e25 Mon Sep 17 00:00:00 2001 From: Marty Pradere Date: Sun, 1 Mar 2026 09:16:50 -0800 Subject: [PATCH 1/6] Don't do the case statement when casting boolean value. --- query/src/org/labkey/query/sql/QuerySelect.java | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/query/src/org/labkey/query/sql/QuerySelect.java b/query/src/org/labkey/query/sql/QuerySelect.java index c863c3ad90d..98adee324e7 100644 --- a/query/src/org/labkey/query/sql/QuerySelect.java +++ b/query/src/org/labkey/query/sql/QuerySelect.java @@ -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); From 42a9e645212526ef7834ddc2405d34fbb1aeac6b Mon Sep 17 00:00:00 2001 From: Marty Pradere Date: Mon, 9 Mar 2026 07:05:45 -0700 Subject: [PATCH 2/6] timestampdiff2 --- .../data/dialect/BasePostgreSqlDialect.java | 35 +++++++++++++++++++ .../labkey/api/data/dialect/SqlDialect.java | 2 ++ query/src/org/labkey/query/QueryTestCase.jsp | 14 ++++++++ query/src/org/labkey/query/sql/Method.java | 8 +++++ query/src/org/labkey/query/sql/SqlParser.java | 3 +- 5 files changed, 61 insertions(+), 1 deletion(-) diff --git a/api/src/org/labkey/api/data/dialect/BasePostgreSqlDialect.java b/api/src/org/labkey/api/data/dialect/BasePostgreSqlDialect.java index 0ecd40466c3..23a0dd97e28 100644 --- a/api/src/org/labkey/api/data/dialect/BasePostgreSqlDialect.java +++ b/api/src/org/labkey/api/data/dialect/BasePostgreSqlDialect.java @@ -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); } @@ -1157,6 +1159,39 @@ 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]; + + return switch (interval) + { + case "SQL_TSI_YEAR" -> + new SQLFragment("(EXTRACT(YEAR FROM (").append(end).append(")) - EXTRACT(YEAR FROM (").append(start).append(")))::INT"); + case "SQL_TSI_QUARTER" -> + new SQLFragment("((EXTRACT(YEAR FROM (").append(end).append(")) - EXTRACT(YEAR FROM (").append(start).append("))) * 4 + EXTRACT(QUARTER FROM (").append(end).append(")) - EXTRACT(QUARTER FROM (").append(start).append(")))::INT"); + case "SQL_TSI_MONTH" -> + new SQLFragment("((EXTRACT(YEAR FROM (").append(end).append(")) - EXTRACT(YEAR FROM (").append(start).append("))) * 12 + EXTRACT(MONTH FROM (").append(end).append(")) - EXTRACT(MONTH FROM (").append(start).append(")))::INT"); + 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)::INT"); + default -> throw new IllegalArgumentException("Unsupported interval for timestampdiff2: " + interval); + }; + } + @Override public boolean supportsBatchGeneratedKeys() { diff --git a/api/src/org/labkey/api/data/dialect/SqlDialect.java b/api/src/org/labkey/api/data/dialect/SqlDialect.java index 868702be8e2..2c36f9ca156 100644 --- a/api/src/org/labkey/api/data/dialect/SqlDialect.java +++ b/api/src/org/labkey/api/data/dialect/SqlDialect.java @@ -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); diff --git a/query/src/org/labkey/query/QueryTestCase.jsp b/query/src/org/labkey/query/QueryTestCase.jsp index 1831ba89ada..e715011b5e1 100644 --- a/query/src/org/labkey/query/QueryTestCase.jsp +++ b/query/src/org/labkey/query/QueryTestCase.jsp @@ -777,6 +777,20 @@ 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_MONTH, CAST('01 Jan 2003' AS TIMESTAMP), CAST('01 Apr 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_YEAR, CAST('01 Jan 2003' AS TIMESTAMP), CAST('01 Jan 2006' AS TIMESTAMP))", JdbcType.INTEGER, 3), + 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 UCASE('Fred')", JdbcType.VARCHAR, "FRED"), new MethodSqlTest("SELECT UPPER('fred')", JdbcType.VARCHAR, "FRED"), new MethodSqlTest("SELECT USERID()", JdbcType.INTEGER, () -> TestContext.get().getUser().getUserId()), diff --git a/query/src/org/labkey/query/sql/Method.java b/query/src/org/labkey/query/sql/Method.java index b6377660675..ba044ace372 100644 --- a/query/src/org/labkey/query/sql/Method.java +++ b/query/src/org/labkey/query/sql/Method.java @@ -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)); diff --git a/query/src/org/labkey/query/sql/SqlParser.java b/query/src/org/labkey/query/sql/SqlParser.java index cca976db057..d6691ea7b95 100644 --- a/query/src/org/labkey/query/sql/SqlParser.java +++ b/query/src/org/labkey/query/sql/SqlParser.java @@ -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) { @@ -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)", From d054412e6ec902b94660b9daa821c3133f8314ac Mon Sep 17 00:00:00 2001 From: Marty Pradere Date: Mon, 9 Mar 2026 08:10:30 -0700 Subject: [PATCH 3/6] Better calculation of year, month, quarter --- .../data/dialect/BasePostgreSqlDialect.java | 37 +++++++++++++++++-- query/src/org/labkey/query/QueryTestCase.jsp | 9 +++++ 2 files changed, 43 insertions(+), 3 deletions(-) diff --git a/api/src/org/labkey/api/data/dialect/BasePostgreSqlDialect.java b/api/src/org/labkey/api/data/dialect/BasePostgreSqlDialect.java index 23a0dd97e28..5aacb8bdd57 100644 --- a/api/src/org/labkey/api/data/dialect/BasePostgreSqlDialect.java +++ b/api/src/org/labkey/api/data/dialect/BasePostgreSqlDialect.java @@ -1167,15 +1167,18 @@ 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("(EXTRACT(YEAR FROM (").append(end).append(")) - EXTRACT(YEAR FROM (").append(start).append(")))::INT"); + new SQLFragment("TRUNC((").append(wholeMonths).append(")::NUMERIC / 12)::INT"); case "SQL_TSI_QUARTER" -> - new SQLFragment("((EXTRACT(YEAR FROM (").append(end).append(")) - EXTRACT(YEAR FROM (").append(start).append("))) * 4 + EXTRACT(QUARTER FROM (").append(end).append(")) - EXTRACT(QUARTER FROM (").append(start).append(")))::INT"); + new SQLFragment("TRUNC((").append(wholeMonths).append(")::NUMERIC / 3)::INT"); case "SQL_TSI_MONTH" -> - new SQLFragment("((EXTRACT(YEAR FROM (").append(end).append(")) - EXTRACT(YEAR FROM (").append(start).append("))) * 12 + EXTRACT(MONTH FROM (").append(end).append(")) - EXTRACT(MONTH FROM (").append(start).append(")))::INT"); + wholeMonths; case "SQL_TSI_WEEK" -> new SQLFragment("TRUNC(EXTRACT(EPOCH FROM (").append(end).append(") - (").append(start).append(")) / 604800)::INT"); case "SQL_TSI_DAY" -> @@ -1192,6 +1195,34 @@ private SQLFragment timestampdiff2(SQLFragment... arguments) }; } + private SQLFragment getWholeElapsedMonths(SQLFragment start, SQLFragment end) + { + SQLFragment baseMonths = new SQLFragment("((EXTRACT(YEAR FROM (").append(end).append(")) - EXTRACT(YEAR FROM (") + .append(start).append("))) * 12 + EXTRACT(MONTH FROM (").append(end).append(")) - EXTRACT(MONTH FROM (") + .append(start).append(")))::INT"); + SQLFragment endBeforeStartInMonth = isSubMonthPartBefore(end, start); + SQLFragment endAfterStartInMonth = isSubMonthPartBefore(start, end); + + // baseMonths counts calendar month boundaries. Adjust away any trailing partial month so the result + // reflects only whole elapsed months, while still truncating toward zero for negative differences. + return new SQLFragment("(CASE WHEN (").append(baseMonths).append(") > 0 AND ").append(endBeforeStartInMonth) + .append(" THEN (").append(baseMonths).append(" - 1) WHEN (").append(baseMonths).append(") < 0 AND ") + .append(endAfterStartInMonth).append(" THEN (").append(baseMonths).append(" + 1) ELSE ") + .append(baseMonths).append(" END)"); + } + + private SQLFragment isSubMonthPartBefore(SQLFragment left, SQLFragment right) + { + SQLFragment leftTimeOfDay = new SQLFragment("EXTRACT(EPOCH FROM ((").append(left).append(") - DATE_TRUNC('day', (") + .append(left).append("))))"); + SQLFragment rightTimeOfDay = new SQLFragment("EXTRACT(EPOCH FROM ((").append(right).append(") - DATE_TRUNC('day', (") + .append(right).append("))))"); + + return new SQLFragment("((EXTRACT(DAY FROM (").append(left).append(")) < EXTRACT(DAY FROM (").append(right) + .append("))) OR (EXTRACT(DAY FROM (").append(left).append(")) = EXTRACT(DAY FROM (").append(right) + .append(")) AND (").append(leftTimeOfDay).append(") < (").append(rightTimeOfDay).append(")))"); + } + @Override public boolean supportsBatchGeneratedKeys() { diff --git a/query/src/org/labkey/query/QueryTestCase.jsp b/query/src/org/labkey/query/QueryTestCase.jsp index e715011b5e1..e70aa5e9c48 100644 --- a/query/src/org/labkey/query/QueryTestCase.jsp +++ b/query/src/org/labkey/query/QueryTestCase.jsp @@ -787,8 +787,17 @@ d,seven,twelve,day,month,date,duration,guid 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_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_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_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('15 Jan 2006' AS TIMESTAMP), CAST('14 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 UCASE('Fred')", JdbcType.VARCHAR, "FRED"), From 25aa55b27eee641b6798084392c99c8e0300dec4 Mon Sep 17 00:00:00 2001 From: Marty Pradere Date: Mon, 9 Mar 2026 08:30:11 -0700 Subject: [PATCH 4/6] Handle type overflow --- api/src/org/labkey/api/data/dialect/BasePostgreSqlDialect.java | 2 +- query/src/org/labkey/query/QueryTestCase.jsp | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/api/src/org/labkey/api/data/dialect/BasePostgreSqlDialect.java b/api/src/org/labkey/api/data/dialect/BasePostgreSqlDialect.java index 5aacb8bdd57..697d6a86f35 100644 --- a/api/src/org/labkey/api/data/dialect/BasePostgreSqlDialect.java +++ b/api/src/org/labkey/api/data/dialect/BasePostgreSqlDialect.java @@ -1190,7 +1190,7 @@ private SQLFragment timestampdiff2(SQLFragment... arguments) 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)::INT"); + new SQLFragment("TRUNC(EXTRACT(EPOCH FROM (").append(end).append(") - (").append(start).append(")) * 1000)::BIGINT"); default -> throw new IllegalArgumentException("Unsupported interval for timestampdiff2: " + interval); }; } diff --git a/query/src/org/labkey/query/QueryTestCase.jsp b/query/src/org/labkey/query/QueryTestCase.jsp index e70aa5e9c48..cb38f8f1c9f 100644 --- a/query/src/org/labkey/query/QueryTestCase.jsp +++ b/query/src/org/labkey/query/QueryTestCase.jsp @@ -799,6 +799,7 @@ d,seven,twelve,day,month,date,duration,guid 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('15 Jan 2006' AS TIMESTAMP), CAST('14 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"), From 3b2b28be47baac52e0753ec4d7b1f6fa6189e683 Mon Sep 17 00:00:00 2001 From: Marty Pradere Date: Mon, 9 Mar 2026 09:46:57 -0700 Subject: [PATCH 5/6] Simplify with AGE. Fix test case. --- .../data/dialect/BasePostgreSqlDialect.java | 28 +++---------------- query/src/org/labkey/query/QueryTestCase.jsp | 2 +- 2 files changed, 5 insertions(+), 25 deletions(-) diff --git a/api/src/org/labkey/api/data/dialect/BasePostgreSqlDialect.java b/api/src/org/labkey/api/data/dialect/BasePostgreSqlDialect.java index 697d6a86f35..ab886db944a 100644 --- a/api/src/org/labkey/api/data/dialect/BasePostgreSqlDialect.java +++ b/api/src/org/labkey/api/data/dialect/BasePostgreSqlDialect.java @@ -1197,30 +1197,10 @@ private SQLFragment timestampdiff2(SQLFragment... arguments) private SQLFragment getWholeElapsedMonths(SQLFragment start, SQLFragment end) { - SQLFragment baseMonths = new SQLFragment("((EXTRACT(YEAR FROM (").append(end).append(")) - EXTRACT(YEAR FROM (") - .append(start).append("))) * 12 + EXTRACT(MONTH FROM (").append(end).append(")) - EXTRACT(MONTH FROM (") - .append(start).append(")))::INT"); - SQLFragment endBeforeStartInMonth = isSubMonthPartBefore(end, start); - SQLFragment endAfterStartInMonth = isSubMonthPartBefore(start, end); - - // baseMonths counts calendar month boundaries. Adjust away any trailing partial month so the result - // reflects only whole elapsed months, while still truncating toward zero for negative differences. - return new SQLFragment("(CASE WHEN (").append(baseMonths).append(") > 0 AND ").append(endBeforeStartInMonth) - .append(" THEN (").append(baseMonths).append(" - 1) WHEN (").append(baseMonths).append(") < 0 AND ") - .append(endAfterStartInMonth).append(" THEN (").append(baseMonths).append(" + 1) ELSE ") - .append(baseMonths).append(" END)"); - } - - private SQLFragment isSubMonthPartBefore(SQLFragment left, SQLFragment right) - { - SQLFragment leftTimeOfDay = new SQLFragment("EXTRACT(EPOCH FROM ((").append(left).append(") - DATE_TRUNC('day', (") - .append(left).append("))))"); - SQLFragment rightTimeOfDay = new SQLFragment("EXTRACT(EPOCH FROM ((").append(right).append(") - DATE_TRUNC('day', (") - .append(right).append("))))"); - - return new SQLFragment("((EXTRACT(DAY FROM (").append(left).append(")) < EXTRACT(DAY FROM (").append(right) - .append("))) OR (EXTRACT(DAY FROM (").append(left).append(")) = EXTRACT(DAY FROM (").append(right) - .append(")) AND (").append(leftTimeOfDay).append(") < (").append(rightTimeOfDay).append(")))"); + // 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 diff --git a/query/src/org/labkey/query/QueryTestCase.jsp b/query/src/org/labkey/query/QueryTestCase.jsp index cb38f8f1c9f..9745b08c451 100644 --- a/query/src/org/labkey/query/QueryTestCase.jsp +++ b/query/src/org/labkey/query/QueryTestCase.jsp @@ -797,7 +797,7 @@ d,seven,twelve,day,month,date,duration,guid 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('15 Jan 2006' AS TIMESTAMP), CAST('14 Jan 2003' AS TIMESTAMP))", JdbcType.INTEGER, -2), + 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), From cb2aaee88232647feddadf55fdc269fe1b7da3ae Mon Sep 17 00:00:00 2001 From: Marty Pradere Date: Mon, 9 Mar 2026 09:53:12 -0700 Subject: [PATCH 6/6] More negative value tests --- query/src/org/labkey/query/QueryTestCase.jsp | 3 +++ 1 file changed, 3 insertions(+) diff --git a/query/src/org/labkey/query/QueryTestCase.jsp b/query/src/org/labkey/query/QueryTestCase.jsp index 9745b08c451..3e837c76e4b 100644 --- a/query/src/org/labkey/query/QueryTestCase.jsp +++ b/query/src/org/labkey/query/QueryTestCase.jsp @@ -786,14 +786,17 @@ d,seven,twelve,day,month,date,duration,guid 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),