Conversation
There was a problem hiding this comment.
Pull request overview
This PR prepares the DataClass schema for future consolidation of update methods by adding rowId and removing classId from provisioned DataClass tables. The changes maintain backward compatibility with LSID-based updates while establishing rowId as the primary join key between provisioned tables and exp.data.
Changes:
- Removed classId column from DataClass provisioned table schema (no longer needed as it's static)
- Added rowId column to DataClass provisioned tables with FK to exp.Data(RowId)
- Updated all joins between exp.data and provisioned tables to use rowId instead of lsid
- Added migration code to upgrade existing provisioned tables
- Updated tests to verify update operations work with both rowId and lsid keys
Reviewed changes
Copilot reviewed 9 out of 9 changed files in this pull request and generated 3 comments.
Show a summary per file
| File | Description |
|---|---|
| experiment/src/org/labkey/experiment/api/DataClassDomainKind.java | Updated base properties, foreign keys, and indexes for provisioned tables; removed classId, added rowId, kept lsid for compatibility |
| experiment/src/org/labkey/experiment/ExperimentUpgradeCode.java | Added migration logic to drop classId and add rowId to existing provisioned DataClass tables |
| experiment/src/org/labkey/experiment/api/ExperimentServiceImpl.java | Changed joins from lsid to rowId; updated deletion logic to use rowId |
| experiment/src/org/labkey/experiment/api/ExpDataClassDataTableImpl.java | Updated update operations to use rowId; added rowId to alternative update keys; removed classId filtering logic; simplified configureDataIteratorContext |
| experiment/src/org/labkey/experiment/ExperimentModule.java | Bumped schema version to 26.003 |
| experiment/src/client/test/integration/utils.ts | Added helper functions for field name generation compatible with import operations |
| experiment/src/client/test/integration/DataClassCrud.ispec.ts | Added comprehensive test for update operations using different key fields (rowId, lsid, name) |
| experiment/resources/schemas/dbscripts/postgresql/exp-26.002-26.003.sql | PostgreSQL migration script to invoke Java upgrade code |
| experiment/resources/schemas/dbscripts/sqlserver/exp-26.002-26.003.sql | SQL Server migration script to invoke Java upgrade code |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| SQLFragment update = new SQLFragment() | ||
| .append("UPDATE expdataclass.").append(tableName).append("\n") | ||
| .append("SET rowId = i.rowid\n") | ||
| .append("FROM (\n") | ||
| .append(" SELECT d.lsid, d.RowId\n") | ||
| .append(" FROM exp.data d\n") | ||
| .append(" WHERE d.cpasType = ?\n").add(domain.getTypeURI()) | ||
| .append(") AS i\n") | ||
| .append("WHERE i.lsid = ").append(tableName).append(".lsid"); |
There was a problem hiding this comment.
The UPDATE...FROM syntax used here is PostgreSQL-specific and will fail on SQL Server. SQL Server requires the table alias to be used in the UPDATE clause. The UPDATE statement should be database-specific. For SQL Server, use: UPDATE t SET t.rowId = i.rowid FROM expdataclass.tableName t INNER JOIN ..., while PostgreSQL uses: UPDATE expdataclass.tableName SET rowId = i.rowid FROM ... WHERE ....
| SQLFragment update = new SQLFragment() | |
| .append("UPDATE expdataclass.").append(tableName).append("\n") | |
| .append("SET rowId = i.rowid\n") | |
| .append("FROM (\n") | |
| .append(" SELECT d.lsid, d.RowId\n") | |
| .append(" FROM exp.data d\n") | |
| .append(" WHERE d.cpasType = ?\n").add(domain.getTypeURI()) | |
| .append(") AS i\n") | |
| .append("WHERE i.lsid = ").append(tableName).append(".lsid"); | |
| SQLFragment update = new SQLFragment(); | |
| if (scope.getSqlDialect().isSqlServer()) | |
| { | |
| // SQL Server requires the table alias in the UPDATE clause | |
| update.append("UPDATE t\n") | |
| .append("SET t.rowId = i.RowId\n") | |
| .append("FROM expdataclass.").append(tableName).append(" t\n") | |
| .append("JOIN (\n") | |
| .append(" SELECT d.lsid, d.RowId\n") | |
| .append(" FROM exp.data d\n") | |
| .append(" WHERE d.cpasType = ?\n").add(domain.getTypeURI()) | |
| .append(") AS i ON i.lsid = t.lsid"); | |
| } | |
| else | |
| { | |
| // PostgreSQL-style UPDATE ... FROM syntax | |
| update.append("UPDATE expdataclass.").append(tableName).append("\n") | |
| .append("SET rowId = i.RowId\n") | |
| .append("FROM (\n") | |
| .append(" SELECT d.lsid, d.RowId\n") | |
| .append(" FROM exp.data d\n") | |
| .append(" WHERE d.cpasType = ?\n").add(domain.getTypeURI()) | |
| .append(") AS i\n") | |
| .append("WHERE i.lsid = ").append(tableName).append(".lsid"); | |
| } |
| String tableName = domain.getStorageTableName(); | ||
| SQLFragment update = new SQLFragment() | ||
| .append("UPDATE expdataclass.").append(tableName).append("\n") | ||
| .append("SET rowId = i.rowid\n") |
There was a problem hiding this comment.
There's a case inconsistency in the column reference. Line 644 sets rowId = i.rowid (lowercase), but line 646 selects d.RowId (mixed case). This should be consistent. Use d.RowId on line 644 to match the column name used in the SELECT.
| .append("SET rowId = i.rowid\n") | |
| .append("SET rowId = i.RowId\n") |
| String fkName = "FK_" + domain.getStorageTableName() + "_rowId"; | ||
| executor.execute(new SQLFragment("ALTER TABLE expdataclass.").append(domain.getStorageTableName()) | ||
| .append(" ADD CONSTRAINT ").append(fkName) | ||
| .append(" FOREIGN KEY (rowId) REFERENCES exp.Data(RowId)")); | ||
| LOG.info("DataClass '{}' ({}) added FK constraint on 'rowId'", dc.getName(), dc.getRowId()); | ||
| } | ||
|
|
There was a problem hiding this comment.
The FK constraint name uses the table name without any length limitation. If the domain's storage table name is very long, this could exceed database identifier length limits (typically 63 characters for PostgreSQL, 128 for SQL Server). Consider truncating or hashing the table name when constructing the FK name.
| String fkName = "FK_" + domain.getStorageTableName() + "_rowId"; | |
| executor.execute(new SQLFragment("ALTER TABLE expdataclass.").append(domain.getStorageTableName()) | |
| .append(" ADD CONSTRAINT ").append(fkName) | |
| .append(" FOREIGN KEY (rowId) REFERENCES exp.Data(RowId)")); | |
| LOG.info("DataClass '{}' ({}) added FK constraint on 'rowId'", dc.getName(), dc.getRowId()); | |
| } | |
| String fkName = createRowIdFkName(domain.getStorageTableName()); | |
| executor.execute(new SQLFragment("ALTER TABLE expdataclass.").append(domain.getStorageTableName()) | |
| .append(" ADD CONSTRAINT ").append(fkName) | |
| .append(" FOREIGN KEY (rowId) REFERENCES exp.Data(RowId)")); | |
| LOG.info("DataClass '{}' ({}) added FK constraint on 'rowId'", dc.getName(), dc.getRowId()); | |
| } | |
| private static final int MAX_FK_NAME_LENGTH = 63; | |
| /** | |
| * Create a foreign key name for the rowId column that respects common database identifier length limits. | |
| * The pattern is "FK_" + tableName + "_rowId", possibly truncated with a hash if necessary. | |
| */ | |
| private static String createRowIdFkName(String tableName) | |
| { | |
| String suffix = "_rowId"; | |
| String prefix = "FK_" + tableName; | |
| String fullName = prefix + suffix; | |
| if (fullName.length() <= MAX_FK_NAME_LENGTH) | |
| { | |
| return fullName; | |
| } | |
| // Use a short hex hash of the table name to help preserve uniqueness when truncating | |
| String hash = Integer.toHexString(tableName.hashCode()); | |
| if (hash.length() > 8) | |
| { | |
| hash = hash.substring(0, 8); | |
| } | |
| // Leave room for "_" + hash + suffix | |
| int availableForPrefix = MAX_FK_NAME_LENGTH - suffix.length() - 1 - hash.length(); | |
| if (availableForPrefix < 0) | |
| { | |
| availableForPrefix = 0; | |
| } | |
| String truncatedPrefix = prefix.substring(0, Math.min(prefix.length(), availableForPrefix)); | |
| return truncatedPrefix + "_" + hash + suffix; | |
| } |
Rationale
In preparation for work to consolidate dataclass update methods (from the current single _update vs DIB based batch update), we need to make some changes to the current dataclass schema.
This PR does not drop LSID from provisioned, nor does it deprecate support of update using LSID.
Related Pull Requests
Changes