From 3bbe7bd52ea5ea5e6e98ad437e0e4b47bb0c4435 Mon Sep 17 00:00:00 2001 From: Marty Pradere Date: Tue, 28 Jul 2026 19:16:27 -0600 Subject: [PATCH 1/3] Scope cage lookups to the EHR study container in housing tables ehr_lookups.cage is unique on (Container, Location), so the unscoped subqueries behind the housing room and daysInRoom columns returned one row per EHR folder and failed on any server hosting more than one. --- .../nbri_ehr/table/NBRI_EHRCustomizer.java | 21 ++++++++++++------- 1 file changed, 13 insertions(+), 8 deletions(-) diff --git a/nbri_ehr/src/org/labkey/nbri_ehr/table/NBRI_EHRCustomizer.java b/nbri_ehr/src/org/labkey/nbri_ehr/table/NBRI_EHRCustomizer.java index 19fe42d..fe2fbae 100644 --- a/nbri_ehr/src/org/labkey/nbri_ehr/table/NBRI_EHRCustomizer.java +++ b/nbri_ehr/src/org/labkey/nbri_ehr/table/NBRI_EHRCustomizer.java @@ -896,25 +896,30 @@ private void ensureSortColumn(AbstractTableInfo ti, ColumnInfo baseColumn) private void customizeHousingTable(AbstractTableInfo ti) { + // ehr_lookups.cage is unique on (Container, Location), so every lookup against it must be scoped to the EHR + // study container. Without that, a server hosting more than one EHR folder returns a row per folder and the + // scalar subqueries below fail with "more than one row returned by a subquery used as an expression". + UserSchema lookupSchema = getEHRUserSchema(ti, "ehr_lookups"); + Container lookupContainer = lookupSchema == null ? null : lookupSchema.getContainer(); + if (ti.getColumn("room") == null && ti.getColumn("cage") != null) { - UserSchema us = getUserSchema(ti, "ehr_lookups"); - if (us != null) + if (lookupContainer != null) { - SQLFragment roomSql = new SQLFragment("(SELECT room FROM ehr_lookups.cage WHERE location = " + ExprColumn.STR_TABLE_ALIAS + ".cage)"); + SQLFragment roomSql = new SQLFragment("(SELECT room FROM ehr_lookups.cage WHERE Container = ? AND location = " + ExprColumn.STR_TABLE_ALIAS + ".cage)", lookupContainer); ExprColumn roomCol = new ExprColumn(ti, "room", roomSql, JdbcType.VARCHAR, ti.getColumn("cage")); ti.addColumn(roomCol); - } - ensureSortColumn(ti, ti.getColumn("room")); + ensureSortColumn(ti, ti.getColumn("room")); + } } if (ti.getColumn("daysInRoom") == null) { TableInfo realTable = getRealTable(ti); - if (realTable != null && realTable.getColumn("participantid") != null && realTable.getColumn("date") != null && realTable.getColumn("enddate") != null) + if (lookupContainer != null && realTable != null && realTable.getColumn("participantid") != null && realTable.getColumn("date") != null && realTable.getColumn("enddate") != null) { - SQLFragment roomSql = new SQLFragment(realTable.getSqlDialect().getDateDiff(Calendar.DATE, "{fn curdate()}", "COALESCE((SELECT max(h2.enddate) as d FROM " + realTable.getSelectName() + " h2 LEFT JOIN ehr_lookups.cage cg ON h2.cage = cg.location " + - "WHERE h2.enddate IS NOT NULL AND h2.enddate <= " + ExprColumn.STR_TABLE_ALIAS + ".date AND h2.participantid = " + ExprColumn.STR_TABLE_ALIAS + ".participantid AND cg.room != (SELECT room FROM ehr_lookups.cage WHERE location = " + ExprColumn.STR_TABLE_ALIAS + ".cage)), " + ExprColumn.STR_TABLE_ALIAS + ".date)")); + SQLFragment roomSql = new SQLFragment(realTable.getSqlDialect().getDateDiff(Calendar.DATE, "{fn curdate()}", "COALESCE((SELECT max(h2.enddate) as d FROM " + realTable.getSelectName() + " h2 LEFT JOIN ehr_lookups.cage cg ON h2.cage = cg.location AND cg.Container = ? " + + "WHERE h2.enddate IS NOT NULL AND h2.enddate <= " + ExprColumn.STR_TABLE_ALIAS + ".date AND h2.participantid = " + ExprColumn.STR_TABLE_ALIAS + ".participantid AND cg.room != (SELECT room FROM ehr_lookups.cage WHERE Container = ? AND location = " + ExprColumn.STR_TABLE_ALIAS + ".cage)), " + ExprColumn.STR_TABLE_ALIAS + ".date)"), lookupContainer, lookupContainer); ExprColumn roomCol = new ExprColumn(ti, "daysInRoom", roomSql, JdbcType.INTEGER, realTable.getColumn("participantid"), realTable.getColumn("date"), realTable.getColumn("enddate")); roomCol.setLabel("Days In Room"); ti.addColumn(roomCol); From 7a8f90c08c5a72acc85d6bf3b2e712d4e2152af9 Mon Sep 17 00:00:00 2001 From: Marty Pradere Date: Tue, 28 Jul 2026 19:57:22 -0600 Subject: [PATCH 2/3] Fall back to the current container when the EHR study container is unset Skipping the room and daysInRoom columns broke study.demographicsCurLocation and ehr_lookups.roomUtilization, which both reference them; DefaultEHRCustomizer already defaults to the current container in this case. --- .../nbri_ehr/table/NBRI_EHRCustomizer.java | 25 +++++++++---------- 1 file changed, 12 insertions(+), 13 deletions(-) diff --git a/nbri_ehr/src/org/labkey/nbri_ehr/table/NBRI_EHRCustomizer.java b/nbri_ehr/src/org/labkey/nbri_ehr/table/NBRI_EHRCustomizer.java index fe2fbae..ea0f5b8 100644 --- a/nbri_ehr/src/org/labkey/nbri_ehr/table/NBRI_EHRCustomizer.java +++ b/nbri_ehr/src/org/labkey/nbri_ehr/table/NBRI_EHRCustomizer.java @@ -896,27 +896,26 @@ private void ensureSortColumn(AbstractTableInfo ti, ColumnInfo baseColumn) private void customizeHousingTable(AbstractTableInfo ti) { - // ehr_lookups.cage is unique on (Container, Location), so every lookup against it must be scoped to the EHR - // study container. Without that, a server hosting more than one EHR folder returns a row per folder and the - // scalar subqueries below fail with "more than one row returned by a subquery used as an expression". - UserSchema lookupSchema = getEHRUserSchema(ti, "ehr_lookups"); - Container lookupContainer = lookupSchema == null ? null : lookupSchema.getContainer(); + // ehr_lookups.cage is unique on (Container, Location), so every lookup against it must be scoped to a single + // container. Without that, a server hosting more than one EHR folder returns a row per folder and the scalar + // subqueries below fail with "more than one row returned by a subquery used as an expression". Prefer the EHR + // study container, defaulting to the current container as DefaultEHRCustomizer does. + Container lookupContainer = EHRService.get().getEHRStudyContainer(ti.getUserSchema().getContainer()); + if (lookupContainer == null) + lookupContainer = ti.getUserSchema().getContainer(); if (ti.getColumn("room") == null && ti.getColumn("cage") != null) { - if (lookupContainer != null) - { - SQLFragment roomSql = new SQLFragment("(SELECT room FROM ehr_lookups.cage WHERE Container = ? AND location = " + ExprColumn.STR_TABLE_ALIAS + ".cage)", lookupContainer); - ExprColumn roomCol = new ExprColumn(ti, "room", roomSql, JdbcType.VARCHAR, ti.getColumn("cage")); - ti.addColumn(roomCol); + SQLFragment roomSql = new SQLFragment("(SELECT room FROM ehr_lookups.cage WHERE Container = ? AND location = " + ExprColumn.STR_TABLE_ALIAS + ".cage)", lookupContainer); + ExprColumn roomCol = new ExprColumn(ti, "room", roomSql, JdbcType.VARCHAR, ti.getColumn("cage")); + ti.addColumn(roomCol); - ensureSortColumn(ti, ti.getColumn("room")); - } + ensureSortColumn(ti, ti.getColumn("room")); } if (ti.getColumn("daysInRoom") == null) { TableInfo realTable = getRealTable(ti); - if (lookupContainer != null && realTable != null && realTable.getColumn("participantid") != null && realTable.getColumn("date") != null && realTable.getColumn("enddate") != null) + if (realTable != null && realTable.getColumn("participantid") != null && realTable.getColumn("date") != null && realTable.getColumn("enddate") != null) { SQLFragment roomSql = new SQLFragment(realTable.getSqlDialect().getDateDiff(Calendar.DATE, "{fn curdate()}", "COALESCE((SELECT max(h2.enddate) as d FROM " + realTable.getSelectName() + " h2 LEFT JOIN ehr_lookups.cage cg ON h2.cage = cg.location AND cg.Container = ? " + "WHERE h2.enddate IS NOT NULL AND h2.enddate <= " + ExprColumn.STR_TABLE_ALIAS + ".date AND h2.participantid = " + ExprColumn.STR_TABLE_ALIAS + ".participantid AND cg.room != (SELECT room FROM ehr_lookups.cage WHERE Container = ? AND location = " + ExprColumn.STR_TABLE_ALIAS + ".cage)), " + ExprColumn.STR_TABLE_ALIAS + ".date)"), lookupContainer, lookupContainer); From 804488094f1467717c283e1e0e4e187e6d97cb9b Mon Sep 17 00:00:00 2001 From: Marty Pradere Date: Tue, 28 Jul 2026 20:00:41 -0600 Subject: [PATCH 3/3] Shorten the cage container-scoping comment --- .../src/org/labkey/nbri_ehr/table/NBRI_EHRCustomizer.java | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/nbri_ehr/src/org/labkey/nbri_ehr/table/NBRI_EHRCustomizer.java b/nbri_ehr/src/org/labkey/nbri_ehr/table/NBRI_EHRCustomizer.java index ea0f5b8..e8f771c 100644 --- a/nbri_ehr/src/org/labkey/nbri_ehr/table/NBRI_EHRCustomizer.java +++ b/nbri_ehr/src/org/labkey/nbri_ehr/table/NBRI_EHRCustomizer.java @@ -896,13 +896,11 @@ private void ensureSortColumn(AbstractTableInfo ti, ColumnInfo baseColumn) private void customizeHousingTable(AbstractTableInfo ti) { - // ehr_lookups.cage is unique on (Container, Location), so every lookup against it must be scoped to a single - // container. Without that, a server hosting more than one EHR folder returns a row per folder and the scalar - // subqueries below fail with "more than one row returned by a subquery used as an expression". Prefer the EHR - // study container, defaulting to the current container as DefaultEHRCustomizer does. + // ehr_lookups.cage is unique on (Container, Location), so the cage subqueries below must be container-scoped; + // a second EHR folder defining the same location would otherwise make them return multiple rows. Container lookupContainer = EHRService.get().getEHRStudyContainer(ti.getUserSchema().getContainer()); if (lookupContainer == null) - lookupContainer = ti.getUserSchema().getContainer(); + lookupContainer = ti.getUserSchema().getContainer(); // as DefaultEHRCustomizer does if (ti.getColumn("room") == null && ti.getColumn("cage") != null) {