diff --git a/snprc_ehr/build.gradle b/snprc_ehr/build.gradle index bb7bfee92..12c4d3087 100644 --- a/snprc_ehr/build.gradle +++ b/snprc_ehr/build.gradle @@ -3,6 +3,7 @@ import org.labkey.gradle.util.ExternalDependency dependencies { + BuildUtils.addLabKeyDependency(project: project, config: "implementation", depProjectPath: ":server:modules:platform:study", depProjectConfig: "apiJarFile") BuildUtils.addLabKeyDependency(project: project, config: "implementation", depProjectPath: ":server:modules:ehrModules:ehr", depProjectConfig: "apiJarFile") BuildUtils.addLabKeyDependency(project: project, config: "implementation", depProjectPath: ":server:modules:snprcEHRModules:snd", depProjectConfig: "apiJarFile") BuildUtils.addLabKeyDependency(project: project, config: "implementation", depProjectPath: ":server:modules:LabDevKitModules:LDK", depProjectConfig: "apiJarFile") diff --git a/snprc_ehr/resources/schemas/dbscripts/sqlserver/snprc_ehr-26.000-26.001.sql b/snprc_ehr/resources/schemas/dbscripts/sqlserver/snprc_ehr-26.000-26.001.sql new file mode 100644 index 000000000..143a3a56e --- /dev/null +++ b/snprc_ehr/resources/schemas/dbscripts/sqlserver/snprc_ehr-26.000-26.001.sql @@ -0,0 +1,2 @@ +EXEC core.executeJavaUpgradeCode 'fixSpecimenStorageColumnNames'; + diff --git a/snprc_ehr/src/org/labkey/snprc_ehr/SNPRC_EHRModule.java b/snprc_ehr/src/org/labkey/snprc_ehr/SNPRC_EHRModule.java index 7f22b236a..d566d66c4 100644 --- a/snprc_ehr/src/org/labkey/snprc_ehr/SNPRC_EHRModule.java +++ b/snprc_ehr/src/org/labkey/snprc_ehr/SNPRC_EHRModule.java @@ -21,6 +21,7 @@ import org.labkey.api.data.Container; import org.labkey.api.data.DbSchema; import org.labkey.api.data.ObjectFactory; +import org.labkey.api.data.UpgradeCode; import org.labkey.api.ehr.EHRService; import org.labkey.api.ehr.dataentry.DefaultDataEntryFormFactory; import org.labkey.api.ehr.history.DefaultArrivalDataSource; @@ -119,7 +120,14 @@ public String getName() @Override public @Nullable Double getSchemaVersion() { - return 26.000; + return 26.001; + } + + @Nullable + @Override + public UpgradeCode getUpgradeCode() + { + return new SNPRC_EHRUpgradeCode(); } @Override diff --git a/snprc_ehr/src/org/labkey/snprc_ehr/SNPRC_EHRUpgradeCode.java b/snprc_ehr/src/org/labkey/snprc_ehr/SNPRC_EHRUpgradeCode.java new file mode 100644 index 000000000..dccefd73c --- /dev/null +++ b/snprc_ehr/src/org/labkey/snprc_ehr/SNPRC_EHRUpgradeCode.java @@ -0,0 +1,132 @@ +/* + * Copyright (c) 2026 LabKey Corporation + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.labkey.snprc_ehr; + +import org.apache.logging.log4j.Logger; +import org.labkey.api.data.ColumnInfo; +import org.labkey.api.data.DbSchema; +import org.labkey.api.data.DbSchemaType; +import org.labkey.api.data.DbScope; +import org.labkey.api.data.DbScope.Transaction; +import org.labkey.api.data.SQLFragment; +import org.labkey.api.data.SchemaTableInfo; +import org.labkey.api.data.SqlSelector; +import org.labkey.api.data.Table; +import org.labkey.api.data.TableInfo; +import org.labkey.api.data.UpgradeCode; +import org.labkey.api.exp.OntologyManager; +import org.labkey.api.module.ModuleContext; +import org.labkey.api.specimen.model.SpecimenTablesProvider; +import org.labkey.api.util.PageFlowUtil; +import org.labkey.api.util.logging.LogHelper; + +import java.util.List; + +public class SNPRC_EHRUpgradeCode implements UpgradeCode +{ + private static final Logger LOG = LogHelper.getLogger(SNPRC_EHRUpgradeCode.class, "SNPRC_EHR upgrade status"); + + /** + * Called from snprc_ehr-26.000-26.001.sql + * + * For every PropertyDescriptor in the specimen storage schema, verify that its StorageColumnName + * exactly matches (case-sensitively) the physical metadata identifier of a column on the provisioned + * table. If not, rewrite it to the physical identifier of the column resolved either by the existing + * (case-insensitive) StorageColumnName lookup or, failing that, by the descriptor's Name. This makes + * the value still match after migration to a case-sensitive database. + */ + @SuppressWarnings("unused") + public static void fixSpecimenStorageColumnNames(ModuleContext context) + { + if (context.isNewInstall()) + return; + + TableInfo tinfoDomainDescriptor = OntologyManager.getTinfoDomainDescriptor(); + TableInfo tinfoPropertyDomain = OntologyManager.getTinfoPropertyDomain(); + TableInfo tinfoPropertyDescriptor = OntologyManager.getTinfoPropertyDescriptor(); + + DbScope scope = tinfoPropertyDescriptor.getSchema().getScope(); + DbSchema specimenSchema = DbSchema.get(SpecimenTablesProvider.SCHEMA_NAME, DbSchemaType.Provisioned); + + SQLFragment sql = new SQLFragment("SELECT px.PropertyId, dd.StorageSchemaName, dd.StorageTableName, px.StorageColumnName, px.Name FROM ") + .append(tinfoDomainDescriptor, "dd") + .append(" INNER JOIN ") + .append(tinfoPropertyDomain, "pd") + .append(" ON dd.DomainId = pd.DomainId INNER JOIN ") + .append(tinfoPropertyDescriptor, "px") + .append(" ON pd.PropertyId = px.PropertyId ") + .append("WHERE dd.StorageSchemaName = ?").add(SpecimenTablesProvider.SCHEMA_NAME) + .append(" AND dd.StorageTableName IS NOT NULL AND px.StorageColumnName IS NOT NULL"); + + List properties = new SqlSelector(scope, sql).getArrayList(PropertyRow.class); + LOG.info("Checking {} PropertyDescriptors in specimen storage schema '{}'", properties.size(), SpecimenTablesProvider.SCHEMA_NAME); + + int updated = 0; + int skipped = 0; + int alreadyCorrect = 0; + try (Transaction tx = scope.ensureTransaction()) + { + for (PropertyRow row : properties) + { + SchemaTableInfo provisioned = specimenSchema.getTable(row.storageTableName()); + if (provisioned == null) + { + LOG.warn("Provisioned table '{}.{}' does not exist; skipping property '{}'", + row.storageSchemaName(), row.storageTableName(), row.name()); + skipped++; + continue; + } + + // getColumn() is case-insensitive, so a case-mismatched StorageColumnName still resolves + // on SQL Server but will break after migration to a case-sensitive DB. Compare to the + // physical metadata identifier to decide whether a rewrite is needed. + ColumnInfo byStorage = provisioned.getColumn(row.storageColumnName()); + if (byStorage != null && row.storageColumnName().equals(byStorage.getMetaDataIdentifier().getId())) + { + LOG.debug("Property '{}' in '{}.{}': StorageColumnName '{}' already matches physical column case; nothing to update", + row.name(), row.storageSchemaName(), row.storageTableName(), row.storageColumnName()); + alreadyCorrect++; + continue; + } + + ColumnInfo target = byStorage != null ? byStorage : provisioned.getColumn(row.name()); + if (target == null) + { + LOG.warn("Property '{}' in '{}.{}': neither StorageColumnName '{}' nor Name '{}' exists in provisioned table; skipping", + row.name(), row.storageSchemaName(), row.storageTableName(), row.storageColumnName(), row.name()); + skipped++; + continue; + } + + String newStorageColumnName = target.getMetaDataIdentifier().getId(); + LOG.info("Updating StorageColumnName from '{}' to '{}' for property '{}' in table '{}.{}'", + row.storageColumnName(), newStorageColumnName, row.name(), row.storageSchemaName(), row.storageTableName()); + Table.update(null, tinfoPropertyDescriptor, PageFlowUtil.map("StorageColumnName", newStorageColumnName), row.propertyId()); + updated++; + } + tx.commit(); + } + + // Drop any cached PropertyDescriptors that may have been populated earlier in startup with the + // pre-update StorageColumnName. + OntologyManager.clearCaches(); + + LOG.info("Specimen StorageColumnName fix complete: {} updated, {} skipped, {} already correct", + updated, skipped, alreadyCorrect); + } + + public record PropertyRow(int propertyId, String storageSchemaName, String storageTableName, String storageColumnName, String name) {} +}