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
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import org.jetbrains.annotations.Nullable;
import org.labkey.api.data.CompareType;
import org.labkey.api.data.DbSchemaType;
import org.labkey.api.data.DbSchema;
import org.labkey.api.data.SimpleFilter.FilterClause;
import org.labkey.api.data.TableInfo;
import org.labkey.api.query.FieldKey;
Expand All @@ -17,6 +18,7 @@
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.function.Consumer;

public interface DatabaseMigrationService
{
Expand Down Expand Up @@ -51,6 +53,10 @@ default void registerSchemaHandler(MigrationSchemaHandler schemaHandler) {}
default void registerTableHandler(MigrationTableHandler tableHandler) {}
default void registerMigrationFilter(MigrationFilter filter) {}

// Register a contributor that runs during migration before a schema's tables are processed.
// Useful for modules that need to register table handlers for a schema owned by another module.
default void registerSchemaContributor(String schemaName, Consumer<DbSchema> contributor) {}

default @Nullable MigrationFilter getMigrationFilter(String propertyName)
{
return null;
Expand Down
15 changes: 15 additions & 0 deletions query/src/org/labkey/query/QueryTestCase.jsp
Original file line number Diff line number Diff line change
Expand Up @@ -695,6 +695,21 @@ d,seven,twelve,day,month,date,duration,guid
new MethodSqlTest("SELECT CAST(AGE(CAST('02 Jan 2003' AS TIMESTAMP), CAST('03 Jan 2004' AS TIMESTAMP), SQL_TSI_YEAR) AS INTEGER)", JdbcType.INTEGER, 1),
new MethodSqlTest("SELECT CAST(AGE(CAST('02 Jan 2003' AS TIMESTAMP), CAST('01 Feb 2004' AS TIMESTAMP), SQL_TSI_MONTH) AS INTEGER)", JdbcType.INTEGER, 12),
new MethodSqlTest("SELECT CAST(AGE(CAST('02 Jan 2003' AS TIMESTAMP), CAST('02 Feb 2004' AS TIMESTAMP), SQL_TSI_MONTH) AS INTEGER)", JdbcType.INTEGER, 13),
// age_in_days() and age(..., SQL_TSI_DAY) - counts calendar-day boundaries (SQL Server semantics)
new MethodSqlTest("SELECT CAST(AGE_IN_DAYS(CAST('01 Jan 2003' AS TIMESTAMP), CAST('31 Jan 2004' AS TIMESTAMP)) AS INTEGER)", JdbcType.INTEGER, 395),
new MethodSqlTest("SELECT CAST(AGE_IN_DAYS(CAST('31 Jan 2004' AS TIMESTAMP), CAST('01 Jan 2003' AS TIMESTAMP)) AS INTEGER)", JdbcType.INTEGER, -395),
new MethodSqlTest("SELECT CAST(AGE_IN_DAYS(CAST('01 Jan 2004' AS TIMESTAMP), CAST('02 Jan 2004' AS TIMESTAMP)) AS INTEGER)", JdbcType.INTEGER, 1),
new MethodSqlTest("SELECT CAST(AGE(CAST('01 Jan 2003' AS TIMESTAMP), CAST('31 Jan 2004' AS TIMESTAMP), SQL_TSI_DAY) AS INTEGER)", JdbcType.INTEGER, 395),
new MethodSqlTest("SELECT CAST(AGE(CAST('31 Jan 2004' AS TIMESTAMP), CAST('01 Jan 2003' AS TIMESTAMP), SQL_TSI_DAY) AS INTEGER)", JdbcType.INTEGER, -395),
// age_in_days() with datetime inputs - verifies calendar-boundary counting (not 24-hour elapsed time)
new MethodSqlTest("SELECT CAST(AGE_IN_DAYS(CAST('01 Jan 2003 00:00:01' AS TIMESTAMP), CAST('01 Jan 2003 23:59:59' AS TIMESTAMP)) AS INTEGER)", JdbcType.INTEGER, 0),
new MethodSqlTest("SELECT CAST(AGE_IN_DAYS(CAST('01 Jan 2003 23:59:59' AS TIMESTAMP), CAST('02 Jan 2003 00:00:01' AS TIMESTAMP)) AS INTEGER)", JdbcType.INTEGER, 1),
new MethodSqlTest("SELECT CAST(AGE_IN_DAYS(CAST('01 Jan 2003 12:00:00' AS TIMESTAMP), CAST('02 Jan 2003 11:00:00' AS TIMESTAMP)) AS INTEGER)", JdbcType.INTEGER, 1),
new MethodSqlTest("SELECT CAST(AGE_IN_DAYS(CAST('02 Jan 2003 00:00:01' AS TIMESTAMP), CAST('01 Jan 2003 23:59:59' AS TIMESTAMP)) AS INTEGER)", JdbcType.INTEGER, -1),
new MethodSqlTest("SELECT CAST(AGE_IN_DAYS(CAST('01 Jan 2003 06:00:00' AS TIMESTAMP), CAST('03 Jan 2003 18:00:00' AS TIMESTAMP)) AS INTEGER)", JdbcType.INTEGER, 2),
// age(..., SQL_TSI_DAY) with datetime inputs - same calendar-boundary semantics
new MethodSqlTest("SELECT CAST(AGE(CAST('01 Jan 2003 00:00:01' AS TIMESTAMP), CAST('01 Jan 2003 23:59:59' AS TIMESTAMP), SQL_TSI_DAY) AS INTEGER)", JdbcType.INTEGER, 0),
new MethodSqlTest("SELECT CAST(AGE(CAST('01 Jan 2003 23:59:59' AS TIMESTAMP), CAST('02 Jan 2003 00:00:01' AS TIMESTAMP), SQL_TSI_DAY) AS INTEGER)", JdbcType.INTEGER, 1),
new MethodSqlTest("SELECT CAST('1' AS SQL_INTEGER) ", JdbcType.INTEGER, 1),
new MethodSqlTest("SELECT CAST('1' AS INTEGER) ", JdbcType.INTEGER, 1),
new MethodSqlTest("SELECT CAST('1.5' AS DOUBLE) ", JdbcType.DOUBLE, 1.5),
Expand Down
1 change: 1 addition & 0 deletions query/src/org/labkey/query/controllers/LabKeySql.md
Original file line number Diff line number Diff line change
Expand Up @@ -198,6 +198,7 @@ Here is a summary of the available functions and methods in LabKey SQL.
#### **Date and Time Functions**
* `age(date1, date2, [interval])`: Supplies the difference in age.
* `age_in_days(date1, date2)`: Returns age in days.
* `age_in_months(date1, date2)`: Returns age in months.
* `age_in_years(date1, date2)`: Returns age in years.
* `curdate()`, `curtime()`: Returns the current date/time.
Expand Down
40 changes: 37 additions & 3 deletions query/src/org/labkey/query/sql/Method.java
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@
import org.labkey.query.QueryServiceImpl;
import org.labkey.query.sql.antlr.SqlBaseLexer;

import java.util.Calendar;
import java.lang.reflect.Field;
import java.lang.reflect.Modifier;
import java.text.DecimalFormat;
Expand Down Expand Up @@ -94,9 +95,9 @@ public void validate(CommonTree fn, List<QNode> args, List<Exception> parseError
if (text.length() >= 2 && text.startsWith("'") && text.endsWith("'"))
text = text.substring(1, text.length() - 1);
TimestampDiffInterval i = TimestampDiffInterval.parse(text);
if (!(i == TimestampDiffInterval.SQL_TSI_MONTH || i == TimestampDiffInterval.SQL_TSI_YEAR))
if (!(i == TimestampDiffInterval.SQL_TSI_DAY || i == TimestampDiffInterval.SQL_TSI_MONTH || i == TimestampDiffInterval.SQL_TSI_YEAR))
{
parseErrors.add(new QueryParseException("AGE function supports SQL_TSI_YEAR or SQL_TSI_MONTH", null,
parseErrors.add(new QueryParseException("AGE function supports SQL_TSI_DAY, SQL_TSI_MONTH, or SQL_TSI_YEAR", null,
nodeInterval.getLine(), nodeInterval.getColumn()));
}
}
Expand All @@ -118,6 +119,14 @@ public MethodInfo getMethodInfo()
return new AgeInYearsMethodInfo();
}
});
labkeyMethod.put("age_in_days", new Method(JdbcType.INTEGER, 2, 2)
{
@Override
public MethodInfo getMethodInfo()
{
return new AgeInDaysMethodInfo();
}
});
labkeyMethod.put("asin", new JdbcMethod("asin", JdbcType.DOUBLE, 1, 1));
labkeyMethod.put("atan", new JdbcMethod("atan", JdbcType.DOUBLE, 1, 1));
labkeyMethod.put("atan2", new JdbcMethod("atan2", JdbcType.DOUBLE, 2, 2));
Expand Down Expand Up @@ -889,10 +898,12 @@ public SQLFragment getSQL(SqlDialect dialect, SQLFragment[] arguments)
return new AgeInYearsMethodInfo().getSQL(dialect, arguments);
if (i == TimestampDiffInterval.SQL_TSI_MONTH)
return new AgeInMonthsMethodInfo().getSQL(dialect, arguments);
if (i == TimestampDiffInterval.SQL_TSI_DAY)
return new AgeInDaysMethodInfo().getSQL(dialect, arguments);
if (null == i)
throw new IllegalArgumentException("AGE(" + arguments[2].getSQL() + ")");
else
throw new IllegalArgumentException("AGE only supports YEAR and MONTH");
throw new IllegalArgumentException("AGE only supports DAY, MONTH, and YEAR");
}
}

Expand Down Expand Up @@ -972,6 +983,29 @@ public SQLFragment getSQL(SqlDialect dialect, SQLFragment[] arguments)
}


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

@Override
public SQLFragment getSQL(SqlDialect dialect, SQLFragment[] arguments)
{
MethodInfo convert = labkeyMethod.get("convert").getMethodInfo();
SQLFragment dateType = new SQLFragment("DATE");
SQLFragment startDate = convert.getSQL(dialect, new SQLFragment[]{arguments[0], dateType});
SQLFragment endDate = convert.getSQL(dialect, new SQLFragment[]{arguments[1], dateType});

if (dialect.isPostgreSQL())
return new SQLFragment("(").append(endDate).append(" - ").append(startDate).append(")");

return dialect.getDateDiff(Calendar.DATE, endDate, startDate);
}
}


static class StartsWithInfo extends AbstractMethodInfo
{
StartsWithInfo()
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
Loading