Skip to content

Commit b68f55e

Browse files
Pre-migration fix for SNPRC specimen StorageColumnName mismatches (#935)
- Add `SNPRC_EHRUpgradeCode.fixSpecimenStorageColumnNames`: for each `PropertyDescriptor` in the specimen storage schema, if its `StorageColumnName` doesn't resolve on the provisioned table but `Name` does, update `StorageColumnName` to `Name`. Skips and logs when neither name resolves or both already match. - Bump `snprc_ehr` schema version 26.000 -> 26.001 and register the upgrade code. - Add SQL Server-only upgrade script `snprc_ehr-26.000-26.001.sql` invoking the Java upgrade code (no Postgres script -- runs only on the source side prior to migration). - Add `platform:study` apiJar dependency required by `SNPRC_EHRUpgradeCode` for `SpecimenTablesProvider`. Co-authored-by: Claude Opus 4.7 (1M context) <[email protected]>
1 parent 01c4d1c commit b68f55e

4 files changed

Lines changed: 144 additions & 1 deletion

File tree

snprc_ehr/build.gradle

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import org.labkey.gradle.util.ExternalDependency
33

44
dependencies
55
{
6+
BuildUtils.addLabKeyDependency(project: project, config: "implementation", depProjectPath: ":server:modules:platform:study", depProjectConfig: "apiJarFile")
67
BuildUtils.addLabKeyDependency(project: project, config: "implementation", depProjectPath: ":server:modules:ehrModules:ehr", depProjectConfig: "apiJarFile")
78
BuildUtils.addLabKeyDependency(project: project, config: "implementation", depProjectPath: ":server:modules:snprcEHRModules:snd", depProjectConfig: "apiJarFile")
89
BuildUtils.addLabKeyDependency(project: project, config: "implementation", depProjectPath: ":server:modules:LabDevKitModules:LDK", depProjectConfig: "apiJarFile")
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
EXEC core.executeJavaUpgradeCode 'fixSpecimenStorageColumnNames';
2+

snprc_ehr/src/org/labkey/snprc_ehr/SNPRC_EHRModule.java

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
import org.labkey.api.data.Container;
2222
import org.labkey.api.data.DbSchema;
2323
import org.labkey.api.data.ObjectFactory;
24+
import org.labkey.api.data.UpgradeCode;
2425
import org.labkey.api.ehr.EHRService;
2526
import org.labkey.api.ehr.dataentry.DefaultDataEntryFormFactory;
2627
import org.labkey.api.ehr.history.DefaultArrivalDataSource;
@@ -119,7 +120,14 @@ public String getName()
119120
@Override
120121
public @Nullable Double getSchemaVersion()
121122
{
122-
return 26.000;
123+
return 26.001;
124+
}
125+
126+
@Nullable
127+
@Override
128+
public UpgradeCode getUpgradeCode()
129+
{
130+
return new SNPRC_EHRUpgradeCode();
123131
}
124132

125133
@Override
Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,132 @@
1+
/*
2+
* Copyright (c) 2026 LabKey Corporation
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package org.labkey.snprc_ehr;
17+
18+
import org.apache.logging.log4j.Logger;
19+
import org.labkey.api.data.ColumnInfo;
20+
import org.labkey.api.data.DbSchema;
21+
import org.labkey.api.data.DbSchemaType;
22+
import org.labkey.api.data.DbScope;
23+
import org.labkey.api.data.DbScope.Transaction;
24+
import org.labkey.api.data.SQLFragment;
25+
import org.labkey.api.data.SchemaTableInfo;
26+
import org.labkey.api.data.SqlSelector;
27+
import org.labkey.api.data.Table;
28+
import org.labkey.api.data.TableInfo;
29+
import org.labkey.api.data.UpgradeCode;
30+
import org.labkey.api.exp.OntologyManager;
31+
import org.labkey.api.module.ModuleContext;
32+
import org.labkey.api.specimen.model.SpecimenTablesProvider;
33+
import org.labkey.api.util.PageFlowUtil;
34+
import org.labkey.api.util.logging.LogHelper;
35+
36+
import java.util.List;
37+
38+
public class SNPRC_EHRUpgradeCode implements UpgradeCode
39+
{
40+
private static final Logger LOG = LogHelper.getLogger(SNPRC_EHRUpgradeCode.class, "SNPRC_EHR upgrade status");
41+
42+
/**
43+
* Called from snprc_ehr-26.000-26.001.sql
44+
*
45+
* For every PropertyDescriptor in the specimen storage schema, verify that its StorageColumnName
46+
* exactly matches (case-sensitively) the physical metadata identifier of a column on the provisioned
47+
* table. If not, rewrite it to the physical identifier of the column resolved either by the existing
48+
* (case-insensitive) StorageColumnName lookup or, failing that, by the descriptor's Name. This makes
49+
* the value still match after migration to a case-sensitive database.
50+
*/
51+
@SuppressWarnings("unused")
52+
public static void fixSpecimenStorageColumnNames(ModuleContext context)
53+
{
54+
if (context.isNewInstall())
55+
return;
56+
57+
TableInfo tinfoDomainDescriptor = OntologyManager.getTinfoDomainDescriptor();
58+
TableInfo tinfoPropertyDomain = OntologyManager.getTinfoPropertyDomain();
59+
TableInfo tinfoPropertyDescriptor = OntologyManager.getTinfoPropertyDescriptor();
60+
61+
DbScope scope = tinfoPropertyDescriptor.getSchema().getScope();
62+
DbSchema specimenSchema = DbSchema.get(SpecimenTablesProvider.SCHEMA_NAME, DbSchemaType.Provisioned);
63+
64+
SQLFragment sql = new SQLFragment("SELECT px.PropertyId, dd.StorageSchemaName, dd.StorageTableName, px.StorageColumnName, px.Name FROM ")
65+
.append(tinfoDomainDescriptor, "dd")
66+
.append(" INNER JOIN ")
67+
.append(tinfoPropertyDomain, "pd")
68+
.append(" ON dd.DomainId = pd.DomainId INNER JOIN ")
69+
.append(tinfoPropertyDescriptor, "px")
70+
.append(" ON pd.PropertyId = px.PropertyId ")
71+
.append("WHERE dd.StorageSchemaName = ?").add(SpecimenTablesProvider.SCHEMA_NAME)
72+
.append(" AND dd.StorageTableName IS NOT NULL AND px.StorageColumnName IS NOT NULL");
73+
74+
List<PropertyRow> properties = new SqlSelector(scope, sql).getArrayList(PropertyRow.class);
75+
LOG.info("Checking {} PropertyDescriptors in specimen storage schema '{}'", properties.size(), SpecimenTablesProvider.SCHEMA_NAME);
76+
77+
int updated = 0;
78+
int skipped = 0;
79+
int alreadyCorrect = 0;
80+
try (Transaction tx = scope.ensureTransaction())
81+
{
82+
for (PropertyRow row : properties)
83+
{
84+
SchemaTableInfo provisioned = specimenSchema.getTable(row.storageTableName());
85+
if (provisioned == null)
86+
{
87+
LOG.warn("Provisioned table '{}.{}' does not exist; skipping property '{}'",
88+
row.storageSchemaName(), row.storageTableName(), row.name());
89+
skipped++;
90+
continue;
91+
}
92+
93+
// getColumn() is case-insensitive, so a case-mismatched StorageColumnName still resolves
94+
// on SQL Server but will break after migration to a case-sensitive DB. Compare to the
95+
// physical metadata identifier to decide whether a rewrite is needed.
96+
ColumnInfo byStorage = provisioned.getColumn(row.storageColumnName());
97+
if (byStorage != null && row.storageColumnName().equals(byStorage.getMetaDataIdentifier().getId()))
98+
{
99+
LOG.debug("Property '{}' in '{}.{}': StorageColumnName '{}' already matches physical column case; nothing to update",
100+
row.name(), row.storageSchemaName(), row.storageTableName(), row.storageColumnName());
101+
alreadyCorrect++;
102+
continue;
103+
}
104+
105+
ColumnInfo target = byStorage != null ? byStorage : provisioned.getColumn(row.name());
106+
if (target == null)
107+
{
108+
LOG.warn("Property '{}' in '{}.{}': neither StorageColumnName '{}' nor Name '{}' exists in provisioned table; skipping",
109+
row.name(), row.storageSchemaName(), row.storageTableName(), row.storageColumnName(), row.name());
110+
skipped++;
111+
continue;
112+
}
113+
114+
String newStorageColumnName = target.getMetaDataIdentifier().getId();
115+
LOG.info("Updating StorageColumnName from '{}' to '{}' for property '{}' in table '{}.{}'",
116+
row.storageColumnName(), newStorageColumnName, row.name(), row.storageSchemaName(), row.storageTableName());
117+
Table.update(null, tinfoPropertyDescriptor, PageFlowUtil.map("StorageColumnName", newStorageColumnName), row.propertyId());
118+
updated++;
119+
}
120+
tx.commit();
121+
}
122+
123+
// Drop any cached PropertyDescriptors that may have been populated earlier in startup with the
124+
// pre-update StorageColumnName.
125+
OntologyManager.clearCaches();
126+
127+
LOG.info("Specimen StorageColumnName fix complete: {} updated, {} skipped, {} already correct",
128+
updated, skipped, alreadyCorrect);
129+
}
130+
131+
public record PropertyRow(int propertyId, String storageSchemaName, String storageTableName, String storageColumnName, String name) {}
132+
}

0 commit comments

Comments
 (0)