From b2845cfde3022154464eb8b49220573851ef5a85 Mon Sep 17 00:00:00 2001 From: Marty Pradere Date: Wed, 15 Apr 2026 05:20:53 -0700 Subject: [PATCH 1/3] Use age_in_days instead of timestampdiff --- ehr/src/org/labkey/ehr/table/DefaultEHRCustomizer.java | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/ehr/src/org/labkey/ehr/table/DefaultEHRCustomizer.java b/ehr/src/org/labkey/ehr/table/DefaultEHRCustomizer.java index b46909ce3..e734cb849 100644 --- a/ehr/src/org/labkey/ehr/table/DefaultEHRCustomizer.java +++ b/ehr/src/org/labkey/ehr/table/DefaultEHRCustomizer.java @@ -1819,9 +1819,9 @@ public TableInfo getLookupTableInfo() "WHEN d.birth is null or c." + dateColName + " is null\n" + " THEN null\n" + "WHEN (d.lastDayAtCenter IS NOT NULL AND d.lastDayAtCenter < c." + dateColName + ") THEN\n" + - " ROUND(CONVERT(timestampdiff('SQL_TSI_DAY', d.birth, d.lastDayAtCenter), DOUBLE) / 365.25, 2)\n" + + " ROUND(CONVERT(age_in_days(d.birth, d.lastDayAtCenter), DOUBLE) / 365.25, 2)\n" + "ELSE\n" + - " ROUND(CONVERT(timestampdiff('SQL_TSI_DAY', d.birth, CAST(c." + dateColName + " as DATE)), DOUBLE) / 365.25, 2)\n" + + " ROUND(CONVERT(age_in_days(d.birth, c." + dateColName + "), DOUBLE) / 365.25, 2)\n" + "END AS float) as AgeAtTimeYears,\n" + "\n" + "CAST(\n" + @@ -1840,9 +1840,9 @@ public TableInfo getLookupTableInfo() "WHEN d.birth is null or c." + dateColName + " is null\n" + " THEN null\n" + "WHEN (d.lastDayAtCenter IS NOT NULL AND d.lastDayAtCenter < c." + dateColName + ") THEN\n" + - " CONVERT(TIMESTAMPDIFF('SQL_TSI_DAY',d.birth, d.lastDayAtCenter), INTEGER)\n" + + " age_in_days(d.birth, d.lastDayAtCenter)\n" + "ELSE\n" + - " CONVERT(TIMESTAMPDIFF('SQL_TSI_DAY',d.birth, CAST(c." + dateColName + " AS DATE)), INTEGER)\n" + + " age_in_days(d.birth, c." + dateColName + ")\n" + "END AS float) as AgeAtTimeDays,\n" + "\n" + // From e516b6c8c6ff4944b0f093c82586a8f5f6c4c659 Mon Sep 17 00:00:00 2001 From: Marty Pradere Date: Tue, 21 Apr 2026 20:47:35 -0700 Subject: [PATCH 2/3] age at time and demographicsAge --- .../queries/study/demographicsAge.sql | 6 +- .../ehr/table/DefaultEHRCustomizer.java | 117 +++++++----------- 2 files changed, 50 insertions(+), 73 deletions(-) diff --git a/ehr/resources/queries/study/demographicsAge.sql b/ehr/resources/queries/study/demographicsAge.sql index 10d5417c2..150e825d1 100644 --- a/ehr/resources/queries/study/demographicsAge.sql +++ b/ehr/resources/queries/study/demographicsAge.sql @@ -17,15 +17,15 @@ ROUND(CONVERT(age_in_months(d.birth, COALESCE(d.lastDayAtCenter, now())), DOUBLE ROUND(CONVERT(age_in_months(d.birth, COALESCE(d.lastDayAtCenter, now())), DOUBLE) / 12, 1) AS ageInYears, -TIMESTAMPDIFF('SQL_TSI_DAY', d.birth, COALESCE(d.lastDayAtCenter, now())) as ageInDays, +age_in_days(d.birth, COALESCE(d.lastDayAtCenter, now())) as ageInDays, case when (age_in_months(d.birth, COALESCE(d.lastDayAtCenter, now()))) < 1 - then (CONVERT(CONVERT(TIMESTAMPDIFF('SQL_TSI_DAY', d.birth, COALESCE(d.lastDayAtCenter, now())), float), VARCHAR) || ' days') + then (CONVERT(CONVERT(age_in_days(d.birth, COALESCE(d.lastDayAtCenter, now())), float), VARCHAR) || ' days') when (age_in_months(d.birth, COALESCE(d.lastDayAtCenter, now()))) < 12 then (CONVERT(CONVERT(ROUND(age_in_months(d.birth, COALESCE(d.lastDayAtCenter, now())), 1), float), VARCHAR) || ' months') else - (CONVERT(CONVERT(FLOOR(age_in_months(d.birth, COALESCE(d.lastDayAtCenter, now())) / 12), SQL_INTEGER), VARCHAR) || '.' || CONVERT(MOD(CONVERT(ROUND(age_in_months(d.birth, COALESCE(d.death, now())) / 12.0 * 10.0, 0), SQL_INTEGER), 10), VARCHAR) || ' years') + (CONVERT(CONVERT(FLOOR(age_in_months(d.birth, COALESCE(d.lastDayAtCenter, now())) / 12), SQL_INTEGER), VARCHAR) || '.' || CONVERT(MOD(CONVERT(ROUND(age_in_months(d.birth, COALESCE(d.lastDayAtCenter, now())) / 12.0 * 10.0, 0), SQL_INTEGER), 10), VARCHAR) || ' years') end as ageFriendly FROM study.Demographics d diff --git a/ehr/src/org/labkey/ehr/table/DefaultEHRCustomizer.java b/ehr/src/org/labkey/ehr/table/DefaultEHRCustomizer.java index e734cb849..6215e43aa 100644 --- a/ehr/src/org/labkey/ehr/table/DefaultEHRCustomizer.java +++ b/ehr/src/org/labkey/ehr/table/DefaultEHRCustomizer.java @@ -1796,75 +1796,47 @@ private void appendAgeAtTimeCol(UserSchema ehrSchema, AbstractTableInfo ds, fina @Override public TableInfo getLookupTableInfo() { - String name = queryName + "_ageAtTime"; + String lookupQueryName = queryName + "_ageAtTime"; + String dateSql = "c." + FieldKey.fromString(dateColName).toSQLString(); + String pkSql = "c." + pkCol.getFieldKey().toSQLString(); UserSchema targetSchema = ds.getUserSchema().getDefaultSchema().getUserSchema(targetSchemaName); - QueryDefinition qd = QueryService.get().createQueryDef(u, targetSchemaContainer, targetSchema, name); + QueryDefinition qd = QueryService.get().createQueryDef(u, targetSchemaContainer, targetSchema, lookupQueryName); + // Compute the "as-of" date once: clamp to lastDayAtCenter when the record is after the animal left the center. + // All downstream age expressions reference x.effDate, so the clamp lives in one place. //NOTE: do not need to account for QCstate b/c study.demographics only allows 1 row per subject - qd.setSql("SELECT\n" + - "c." + pkCol.getFieldKey().toSQLString() + ",\n" + - "\n" + - "CAST(\n" + - "CASE\n" + - "WHEN d.birth is null or c." + dateColName + " is null\n" + - " THEN null\n" + - "WHEN (d.lastDayAtCenter IS NOT NULL AND d.lastDayAtCenter < c." + dateColName + ") THEN\n" + - " ROUND(CONVERT(age_in_months(d.birth, d.lastDayAtCenter), DOUBLE) / 12, 1)\n" + - "ELSE\n" + - " ROUND(CONVERT(age_in_months(d.birth, CAST(c." + dateColName + " as DATE)), DOUBLE) / 12, 1)\n" + - "END AS float) as AgeAtTime,\n" + - "\n" + - - "CAST(\n" + - "CASE\n" + - "WHEN d.birth is null or c." + dateColName + " is null\n" + - " THEN null\n" + - "WHEN (d.lastDayAtCenter IS NOT NULL AND d.lastDayAtCenter < c." + dateColName + ") THEN\n" + - " ROUND(CONVERT(age_in_days(d.birth, d.lastDayAtCenter), DOUBLE) / 365.25, 2)\n" + - "ELSE\n" + - " ROUND(CONVERT(age_in_days(d.birth, c." + dateColName + "), DOUBLE) / 365.25, 2)\n" + - "END AS float) as AgeAtTimeYears,\n" + - "\n" + - "CAST(\n" + - "CASE\n" + - "WHEN d.birth is null or c." + dateColName + " is null\n" + - " THEN null\n" + - "WHEN (d.lastDayAtCenter IS NOT NULL AND d.lastDayAtCenter < c." + dateColName + ") THEN\n" + - " floor(age(d.birth, d.lastDayAtCenter))\n" + - "ELSE\n" + - " floor(age(d.birth, CAST(c." + dateColName + " as DATE)))\n" + - "END AS float) as AgeAtTimeYearsRounded,\n" + - "\n" + - //Added 'Age at time Days' by kollil on 02/15/2019 - "CAST(\n" + - "CASE\n" + - "WHEN d.birth is null or c." + dateColName + " is null\n" + - " THEN null\n" + - "WHEN (d.lastDayAtCenter IS NOT NULL AND d.lastDayAtCenter < c." + dateColName + ") THEN\n" + - " age_in_days(d.birth, d.lastDayAtCenter)\n" + - "ELSE\n" + - " age_in_days(d.birth, c." + dateColName + ")\n" + - "END AS float) as AgeAtTimeDays,\n" + - "\n" + - // - "CAST(\n" + - "CASE\n" + - "WHEN d.birth is null or c." + dateColName + " is null\n" + - " THEN null\n" + - "WHEN (d.lastDayAtCenter IS NOT NULL AND d.lastDayAtCenter < c." + dateColName + ") THEN\n" + - " CONVERT(age_in_months(d.birth, d.lastDayAtCenter), INTEGER)\n" + - "ELSE\n" + - " CONVERT(age_in_months(d.birth, CAST(c." + dateColName + " AS DATE)), INTEGER)\n" + - "END AS float) as AgeAtTimeMonths,\n" + - //NOTE: written as subselect so we ensure a single row returned in case data in ehr_lookups.ageclass has rows that allow dupes - "(SELECT ac.ageclass FROM ehr_lookups.ageclass ac\n" + - " WHERE " + - " (CONVERT(age_in_months(d.birth, COALESCE(d.lastDayAtCenter, now())), DOUBLE) / 12) >= ac.\"min\" AND\n" + - " ((CONVERT(age_in_months(d.birth, COALESCE(d.lastDayAtCenter, now())), DOUBLE) / 12) < ac.\"max\" OR ac.\"max\" is null) AND\n" + - " d.species = ac.species AND\n" + - " (d.gender = ac.gender OR ac.gender IS NULL)\n" + - ") AS AgeClassAtTime \n" + - "FROM \"" + schemaName + "\".\"" + queryName + "\" c " + - "LEFT JOIN \"" + ehrPath + "\".study.demographics d ON (d.Id = c." + idCol.getFieldKey().toSQLString() + ")" + String pkAlias = pkCol.getFieldKey().toSQLString(); + qd.setSql( + "SELECT\n" + + " x." + pkAlias + ",\n" + + " CAST(CASE WHEN x.birth IS NULL OR x.effDate IS NULL THEN NULL\n" + + " ELSE ROUND(CONVERT(age_in_months(x.birth, x.effDate), DOUBLE) / 12, 1) END AS float) AS AgeAtTime,\n" + + " CAST(CASE WHEN x.birth IS NULL OR x.effDate IS NULL THEN NULL\n" + + " ELSE ROUND(CONVERT(age_in_days(x.birth, x.effDate), DOUBLE) / 365.25, 2) END AS float) AS AgeAtTimeYears,\n" + + " CAST(CASE WHEN x.birth IS NULL OR x.effDate IS NULL THEN NULL\n" + + " ELSE floor(age(x.birth, x.effDate)) END AS INTEGER) AS AgeAtTimeYearsRounded,\n" + + " CAST(CASE WHEN x.birth IS NULL OR x.effDate IS NULL THEN NULL\n" + + " ELSE age_in_days(x.birth, x.effDate) END AS INTEGER) AS AgeAtTimeDays,\n" + + " CAST(CASE WHEN x.birth IS NULL OR x.effDate IS NULL THEN NULL\n" + + " ELSE CONVERT(age_in_months(x.birth, x.effDate), INTEGER) END AS INTEGER) AS AgeAtTimeMonths,\n" + + //NOTE: written as subselect so we ensure a single row returned in case data in ehr_lookups.ageclass has rows that allow dupes + " (SELECT ac.ageclass FROM ehr_lookups.ageclass ac\n" + + " WHERE (CONVERT(age_in_months(x.birth, x.effDate), DOUBLE) / 12) >= ac.\"min\"\n" + + " AND ((CONVERT(age_in_months(x.birth, x.effDate), DOUBLE) / 12) < ac.\"max\" OR ac.\"max\" IS NULL)\n" + + " AND x.species = ac.species\n" + + " AND (x.gender = ac.gender OR ac.gender IS NULL)\n" + + " ) AS AgeClassAtTime\n" + + "FROM (\n" + + " SELECT\n" + + " " + pkSql + " AS " + pkAlias + ",\n" + + " d.birth AS birth,\n" + + " d.species AS species,\n" + + " d.gender AS gender,\n" + + " CASE WHEN (d.lastDayAtCenter IS NOT NULL AND d.lastDayAtCenter < " + dateSql + ")\n" + + " THEN d.lastDayAtCenter\n" + + " ELSE CAST(" + dateSql + " AS DATE) END AS effDate\n" + + " FROM \"" + schemaName + "\".\"" + queryName + "\" c\n" + + " LEFT JOIN \"" + ehrPath + "\".study.demographics d ON (d.Id = c." + idCol.getFieldKey().toSQLString() + ")\n" + + ") x" ); qd.setIsTemporary(true); @@ -1877,12 +1849,17 @@ public TableInfo getLookupTableInfo() { _log.warn(e.getMessage(), e); } + return null; } if (ti != null) { - ((BaseColumnInfo)ti.getColumn(pkCol.getName())).setHidden(true); - ((BaseColumnInfo)ti.getColumn(pkCol.getName())).setKeyField(true); + ColumnInfo pk = ti.getColumn(pkCol.getName()); + if (pk instanceof BaseColumnInfo basePk) + { + basePk.setHidden(true); + basePk.setKeyField(true); + } } else { @@ -1891,7 +1868,7 @@ public TableInfo getLookupTableInfo() if (demographics != null) { _log.warn("Demographics table columns: "); - _log.warn(targetSchema.getTable("demographics").getColumnNameSet()); + _log.warn(demographics.getColumnNameSet()); } } From fecf056e6042d49f9fc3f91c0db41b4e3410e219 Mon Sep 17 00:00:00 2001 From: Marty Pradere Date: Mon, 27 Apr 2026 09:02:18 -0700 Subject: [PATCH 3/3] cast back to float for backwards compatibility --- ehr/src/org/labkey/ehr/table/DefaultEHRCustomizer.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/ehr/src/org/labkey/ehr/table/DefaultEHRCustomizer.java b/ehr/src/org/labkey/ehr/table/DefaultEHRCustomizer.java index 6215e43aa..c3bcfe483 100644 --- a/ehr/src/org/labkey/ehr/table/DefaultEHRCustomizer.java +++ b/ehr/src/org/labkey/ehr/table/DefaultEHRCustomizer.java @@ -1813,11 +1813,11 @@ public TableInfo getLookupTableInfo() " CAST(CASE WHEN x.birth IS NULL OR x.effDate IS NULL THEN NULL\n" + " ELSE ROUND(CONVERT(age_in_days(x.birth, x.effDate), DOUBLE) / 365.25, 2) END AS float) AS AgeAtTimeYears,\n" + " CAST(CASE WHEN x.birth IS NULL OR x.effDate IS NULL THEN NULL\n" + - " ELSE floor(age(x.birth, x.effDate)) END AS INTEGER) AS AgeAtTimeYearsRounded,\n" + + " ELSE floor(age(x.birth, x.effDate)) END AS float) AS AgeAtTimeYearsRounded,\n" + " CAST(CASE WHEN x.birth IS NULL OR x.effDate IS NULL THEN NULL\n" + - " ELSE age_in_days(x.birth, x.effDate) END AS INTEGER) AS AgeAtTimeDays,\n" + + " ELSE age_in_days(x.birth, x.effDate) END AS float) AS AgeAtTimeDays,\n" + " CAST(CASE WHEN x.birth IS NULL OR x.effDate IS NULL THEN NULL\n" + - " ELSE CONVERT(age_in_months(x.birth, x.effDate), INTEGER) END AS INTEGER) AS AgeAtTimeMonths,\n" + + " ELSE CONVERT(age_in_months(x.birth, x.effDate), INTEGER) END AS float) AS AgeAtTimeMonths,\n" + //NOTE: written as subselect so we ensure a single row returned in case data in ehr_lookups.ageclass has rows that allow dupes " (SELECT ac.ageclass FROM ehr_lookups.ageclass ac\n" + " WHERE (CONVERT(age_in_months(x.birth, x.effDate), DOUBLE) / 12) >= ac.\"min\"\n" +