Skip to content

Commit 3bf2621

Browse files
fix: reconcile SQLite migrations across plugin and npm releases (#183)
* fix: reconcile cross-surface SQLite migrations * fix: reconcile mixed legacy and released scan migrations
1 parent 5625adc commit 3bf2621

2 files changed

Lines changed: 549 additions & 1 deletion

File tree

sdk/typescript/_bundled_plugin/scripts/workbench_schema.py

Lines changed: 164 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -604,6 +604,14 @@
604604
),
605605
(
606606
25,
607+
"persist scan model settings",
608+
"""
609+
ALTER TABLE scans ADD COLUMN model TEXT;
610+
ALTER TABLE scans ADD COLUMN reasoning_effort TEXT;
611+
""",
612+
),
613+
(
614+
26,
607615
"persist scan completion warnings",
608616
"""
609617
ALTER TABLE scans
@@ -613,7 +621,31 @@
613621
)
614622

615623

616-
def normalize_pre_release_migrations(connection: sqlite3.Connection, timestamp: str) -> None:
624+
def normalize_pre_release_migrations(
625+
connection: sqlite3.Connection, timestamp: str
626+
) -> None:
627+
execution_migrations = {
628+
row["version"]: row["name"]
629+
for row in connection.execute(
630+
"SELECT version, name FROM schema_migrations WHERE version IN (11, 12)"
631+
)
632+
}
633+
supported_execution_migrations = {
634+
11: {"deep scan orchestration state", "scan execution profiles"},
635+
12: {
636+
"scan continuation threads",
637+
"dynamic scan execution profiles",
638+
"phase-specific scan progress",
639+
},
640+
}
641+
if any(
642+
name not in supported_execution_migrations[version]
643+
for version, name in execution_migrations.items()
644+
):
645+
raise SystemExit(
646+
"The Codex Security database has an unsupported execution-profile migration history."
647+
)
648+
617649
phase_progress_migration = connection.execute(
618650
"SELECT name FROM schema_migrations WHERE version = 12"
619651
).fetchone()
@@ -633,6 +665,8 @@ def normalize_pre_release_migrations(connection: sqlite3.Connection, timestamp:
633665
("phase-specific scan progress",),
634666
)
635667

668+
normalize_pre_release_execution_profile_migrations(connection, timestamp)
669+
636670
preflight_progress_migration = connection.execute(
637671
"SELECT name FROM schema_migrations WHERE version = 13"
638672
).fetchone()
@@ -693,6 +727,135 @@ def normalize_pre_release_migrations(connection: sqlite3.Connection, timestamp:
693727
)
694728

695729

730+
def normalize_pre_release_execution_profile_migrations(
731+
connection: sqlite3.Connection, timestamp: str
732+
) -> None:
733+
execution_migrations = {
734+
row["version"]: row["name"]
735+
for row in connection.execute(
736+
"SELECT version, name FROM schema_migrations WHERE version IN (11, 12, 25, 26)"
737+
)
738+
}
739+
legacy_names = {
740+
11: "scan execution profiles",
741+
12: "dynamic scan execution profiles",
742+
}
743+
released_names = {
744+
11: "deep scan orchestration state",
745+
12: "scan continuation threads",
746+
}
747+
model_migration_name = "persist scan model settings"
748+
warnings_migration_name = "persist scan completion warnings"
749+
has_legacy_profile_history = execution_migrations.get(11) == legacy_names[11]
750+
has_public_warnings_history = (
751+
execution_migrations.get(25) == warnings_migration_name
752+
)
753+
754+
scan_columns = {
755+
row["name"] for row in connection.execute("PRAGMA table_info(scans)")
756+
}
757+
workspace_columns = {
758+
row["name"] for row in connection.execute("PRAGMA table_info(workspaces)")
759+
}
760+
has_legacy_profile_columns = (
761+
"execution_model" in scan_columns or "execution_model" in workspace_columns
762+
)
763+
if not (
764+
has_legacy_profile_history
765+
or has_public_warnings_history
766+
or has_legacy_profile_columns
767+
):
768+
return
769+
770+
if (
771+
any(
772+
execution_migrations.get(version)
773+
not in (None, released_names[version], legacy_names[version])
774+
for version in (11, 12)
775+
)
776+
or (
777+
has_legacy_profile_history
778+
and execution_migrations.get(12)
779+
not in (None, legacy_names[12], released_names[12])
780+
)
781+
or (
782+
not has_legacy_profile_history
783+
and execution_migrations.get(12) == legacy_names[12]
784+
)
785+
):
786+
raise SystemExit(
787+
"The Codex Security database has an unsupported execution-profile migration history."
788+
)
789+
790+
expected_legacy_columns = {"execution_model", "reasoning_effort"}
791+
renamed_legacy_columns = {
792+
"legacy_execution_model",
793+
"legacy_reasoning_effort",
794+
}
795+
if has_legacy_profile_columns and (
796+
not expected_legacy_columns.issubset(scan_columns)
797+
or not expected_legacy_columns.issubset(workspace_columns)
798+
or renamed_legacy_columns.intersection(scan_columns)
799+
or renamed_legacy_columns.intersection(workspace_columns)
800+
):
801+
raise SystemExit(
802+
"The Codex Security database has an unsupported execution-profile migration history."
803+
)
804+
if has_legacy_profile_history and not has_legacy_profile_columns:
805+
raise SystemExit(
806+
"The Codex Security database has an unsupported execution-profile migration history."
807+
)
808+
809+
if has_public_warnings_history:
810+
if execution_migrations.get(26) is not None:
811+
raise SystemExit(
812+
"The Codex Security database has an unsupported pre-release migration history."
813+
)
814+
connection.execute(
815+
"UPDATE schema_migrations SET version = 26 WHERE version = 25 AND name = ?",
816+
(warnings_migration_name,),
817+
)
818+
execution_migrations.pop(25)
819+
820+
if execution_migrations.get(25) not in (None, model_migration_name):
821+
raise SystemExit(
822+
"The Codex Security database has an unsupported execution-profile migration history."
823+
)
824+
825+
# Keep the historical values and constraints for recovery while moving
826+
# them out of the namespace used by the current independent scan settings.
827+
for table in ("workspaces", "scans") if has_legacy_profile_columns else ():
828+
connection.execute(
829+
f"ALTER TABLE {table} RENAME COLUMN execution_model TO legacy_execution_model"
830+
)
831+
connection.execute(
832+
f"ALTER TABLE {table} RENAME COLUMN reasoning_effort TO legacy_reasoning_effort"
833+
)
834+
add_column_if_missing(connection, "scans", "model", "TEXT")
835+
add_column_if_missing(connection, "scans", "reasoning_effort", "TEXT")
836+
if has_legacy_profile_columns:
837+
connection.execute(
838+
"""
839+
UPDATE scans
840+
SET model = COALESCE(model, legacy_execution_model),
841+
reasoning_effort = legacy_reasoning_effort
842+
WHERE legacy_execution_model IS NOT NULL
843+
OR legacy_reasoning_effort IS NOT NULL
844+
"""
845+
)
846+
if has_legacy_profile_history:
847+
for version, name in legacy_names.items():
848+
connection.execute(
849+
"DELETE FROM schema_migrations WHERE version = ? AND name = ?",
850+
(version, name),
851+
)
852+
if execution_migrations.get(25) is None:
853+
connection.execute(
854+
"INSERT INTO schema_migrations (version, name, applied_at) VALUES (?, ?, ?)",
855+
(25, model_migration_name, timestamp),
856+
)
857+
858+
696859
def add_column_if_missing(
697860
connection: sqlite3.Connection, table: str, column: str, definition: str
698861
) -> None:

0 commit comments

Comments
 (0)