feat: flyway style migration - #2822
Open
ppatel9703 wants to merge 8 commits into
Open
Conversation
Contributor
✅ Snyk checks have passed. No issues have been found so far.
💻 Catch issues earlier using the plugins for VS Code, JetBrains IDEs, Visual Studio, and Eclipse. |
ppatel9703
marked this pull request as ready for review
August 3, 2026 12:46
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Description
Adds Flyway-style versioned schema migrations for RDBMS-backed database engines, gated per-engine behind an
ENABLE_MIGRATIONSsmss flag. Migration files (V<version>__<description>.sql) live in the engine's ownassets/.migrationsfolder, are tracked via a history table inside the target engine's own database, and are run automatically on engineopen()as well as on-demand when a new migration is authored through the UI. This is the backend counterpart to the Migrations tab UI added in SEMOSS/semoss-ui#3500 — see How It Ties Together below for the full request lifecycle across both PRs.Changes Made
Gating & trigger —
Constants.ENABLE_MIGRATIONSis a plain smss property read per-engine. InRDBMSNativeEngine.open(), after the existing connection-establishment step (which only logs onSQLExceptionand never throws), a new block runsrunPendingMigrations()whenengineConnected && ENABLE_MIGRATIONS=true. Unlike the connection-failure path, a migration failure is not caught here — it propagates out ofopen()on purpose, sinceUtility.loadEngine()never registers an engine whoseopen()throws. A bad migration leaves the engine fully unusable rather than usable with a half-migrated schema.File discovery (
MigrationFile,MigrationFileUtils) — Files must match^V(\d+(?:\.\d+)*)__(.+)\.sql$(case-insensitive) and live underassets/.migrations(resolved viaEngineUtility.getSpecificEngineAssetsFolder). Non-matching files are logged as a warning and silently skipped, never treated as an error. Versions sort numerically segment-by-segment ("2" < "2.1" < "10", not lexicographic), and each file's checksum is a SHA-256 of its raw content. Statement splitting is intentionally naive (line-based, split on;-terminated lines) — does not handle semicolons inside string literals or stored procedures, called out in-code as out of scope for v1.History (
MigrationHistoryRecord,MigrationHistoryUtils) — Persisted in aSEMOSS_SCHEMA_HISTORYtable created inside the engine's own database (not a central SEMOSS table):VERSION, SCRIPTNAME, CHECKSUM, APPLIEDBY, APPLIEDON, EXECUTIONTIMEMS, SUCCESS, DESCRIPTION(the description column doubles as the failure-reason text on failed rows). Reads use raw JDBC rather thanSelectQueryStruct, because the history table only becomes an OWL-registered concept after the first migration's OWL sync — querying it through the query-struct layer would fail during that bootstrap window. Every attempt inserts a new row (never updates), so retries are fully auditable.Status (
MigrationStatus,MigrationStatusUtils) — Merges the on-disk file scan with the latest history record per version (by maxappliedOn, since retries produce multiple rows) into one of five states:PENDING(file, no history),SUCCESS,FAILED(last attempt failed),OUTDATED(succeeded previously but on-disk checksum no longer matches the recorded one),MISSING(history exists but the file is gone).Locking (
SchemaMigrationLock) — Dialect-dispatched, DB-level, not file- or JVM-local: Postgres uses a real session-level advisory lock (pg_try_advisory_lock/pg_advisory_unlock) keyed by a hash of the engine ID; MySQL/H2 use aSEMOSS_SCHEMA_LOCKtable with aPRIMARY KEYonENGINEIDso acquisition is race-free via constraint violation. A lock row older than 600s is treated as abandoned and stolen. Waits retry every 200ms up to a 30s timeout, then throwSchemaMigrationLockTimeoutException.Runner (
SchemaMigrationRunner) — Orchestration: acquire lock → ensure folder/history table exist → scan files → for each in version order: skip already-applied files after verifying checksum is unchanged (throws on mismatch), reject if version is lower than the highest already-applied version (out-of-order), reject re-running a version that previously failed with the same checksum (must fix the content first) → run the migration in its own transaction (own connection,setAutoCommit(false), statements viaPreparedStatement, history row inserted on the same transaction so SQL + history commit or roll back together) → sync OWL for that file immediately (not batched at the end, so a later failure doesn't leave earlier schema changes unsynced by OWL). On failure, the transaction rolls back and the failure is recorded on a fresh connection (so the failure record survives the rollback). On a lock-timeout, the runner re-checks history first — if another node already finished the migration,open()proceeds normally instead of failing.OWL sync (
RdbmsMigrationOwlSyncUtils) — Because migrations execute raw SQL directly over JDBC (bypassing SEMOSS's normal upload/edit reactors), the OWL metamodel thatSelectQueryStructrelies on would otherwise go stale. This reuses the same discovery/diff logic as the Metadata tab's "Sync" button (RDBMSEngineCreationHelperfor JDBC introspection,UploadUtilitiesfor the OWL side), called as plain Java since there's noInsight/user context atopen()time.SEMOSS_SCHEMA_LOCKis excluded from the diff (transient plumbing);SEMOSS_SCHEMA_HISTORYis deliberately included as an engine-managed table. Adds new concepts/props/FKs; known limitation: removed foreign keys are not currently pruned from OWL (add-only).Reactors —
GetEngineMigrationsEnabledReactor(input:engine; requires view permission; returns the raw smss boolean).ListEngineMigrationsReactor(input:engine; requires view permission; returnsMigrationStatusUtils.getStatus()as a vector of maps).SaveEngineMigrationReactor(inputs:engine, sql, description; requires edit permission): acquires the lock, computes the next version ashighestMajorSegment + 1(dotted sub-versions like2.1are collapsed — next after2.1is3, not2.2), sanitizes the description ([^a-zA-Z0-9-_]→_), writes the file, releases the lock, then immediately runs it through the sameSchemaMigrationRunnerpipeline used byopen(). On failure it distinguishes: if the newly-saved version itself failed, the just-written file is deleted so it doesn't permanently block the chain, and the recorded error is returned; if an earlier pending version failed instead, the new file is kept (queued) and a "queued behind an earlier failure" message is returned. DeclaresSMSS_MCP_EXECUTION=ASKso it can never be agent-auto-triggered, since it creates and immediately executes arbitrary DDL/DML.FileSystemUtil.java— Extended the hidden-asset checks (isHiddenName/isHiddenAsset/isWithinHiddenAsset) to also treat.migrationsas hidden, alongside.git/.admin, so engine file-explorer/search endpoints never expose or traverse into it.Tests —
SchemaMigrationRunnerUnitTestscoversrejectIfPreviouslyFailedUnchanged(throws on same version+checksum already failed; no-op when content changed, no history exists, only a success record exists, or the failure was for a different version).SaveEngineMigrationReactorUnitTestscovershandleRunFailure: deletes the file and surfaces the error when the just-saved version failed; keeps the file and returns a "queued" message when an earlier version failed instead; falls back to the caught exception's message when no status row exists at all.How It Ties Together
GetEngineMigrationsEnabled→ Migrations tab renders only if this engine's smss hasENABLE_MIGRATIONS=true.ListEngineMigrations→ this PR'sMigrationStatusUtils.getStatus()reconcilesassets/.migrationson disk againstSEMOSS_SCHEMA_HISTORYin the engine's own DB and returns one row per version with its state.SaveEngineMigration→ this PR writes theV<n>__desc.sqlfile, runs it immediately throughSchemaMigrationRunner, syncs OWL, and returns the outcome — UI then re-callsListEngineMigrationsto refresh the table.RDBMSNativeEngine.open()runs any migration files that were added out-of-band (e.g. deployed alongside a release) so environments stay in sync without requiring someone to click through the UI.MigrationStatetype (PENDING/SUCCESS/FAILED/MISSING/OUTDATED) is a direct mirror of this PR'sMigrationStatusstates — both sides comment the contract explicitly to keep them from drifting.How to Test
ENABLE_MIGRATIONS=trueon a DATABASE engine's smss properties.SaveEngineMigration(or the paired UI), create and run a new versioned SQL migration; confirm the file lands inassets/.migrationsasV<version>__<description>.sqland a history row is written inSEMOSS_SCHEMA_HISTORY.open().ListEngineMigrationsand confirm status reflects reality — including forcing anOUTDATEDstate by editing an already-applied file's content, and aMISSINGstate by deleting a file that has history.FAILED, the file is deleted, and a subsequent valid migration at the same version number can be resubmitted.SaveEngineMigration/open()calls against the same engine and confirmSchemaMigrationLockserializes them (no double-run, no duplicate version numbers).SchemaMigrationRunnerUnitTestsandSaveEngineMigrationReactorUnitTests.Notes
ENABLE_MIGRATIONSis explicitly set.SaveEngineMigration's next-version logic collapses dotted sub-versions to the next integer major version rather than incrementing the last segment.