Skip to content

Commit 44b3e00

Browse files
dippindotsclaude
andcommitted
Auto-regenerate derived tables via migrate_db.py --regenerate-derived-tables
migrate_db.py gains --regenerate-derived-tables (off by default) and --derived-tables-sql. When passed, it compares generate_derived_tables.sql's own header version against the database's current info.derived_table_schema_version and regenerates derived tables whenever they differ — covering both a base-table migration that requires a derived rebuild and a derived-table-only version bump with no corresponding migrate_schema.sql section. generate_derived_tables.sql now records its own version into info.derived_table_schema_version at the end of the script, so this works regardless of what invokes it (migrate_db.py, the docker-compose init scripts, or metaImport.py's rebuild_derived_tables.py). Off by default so institutional deployments that run derived-table regeneration as a separate manual step (e.g. against ClickHouse Cloud) are unaffected; cbioportal-docker-compose opts in explicitly. Also: - migrate_schema.sql: soften the idempotency rule from a hard requirement to a recommendation (not always achievable), and document the convention that a base-table migration must also bump generate_derived_tables.sql's version. - test_db_version.sh: also check generate_derived_tables.sql's trailing info.derived_table_schema_version UPDATE against its own header version. - README: document when derived tables get rebuilt across docker-compose and manual deployment scenarios, and describe the new flag. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
1 parent 5a96757 commit 44b3e00

5 files changed

Lines changed: 136 additions & 17 deletions

File tree

docs/deployment/clickhouse/README.md

Lines changed: 26 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -189,6 +189,14 @@ Without derived tables, every Study View page load would need to join across gen
189189

190190
### When Derived Tables Are Built
191191

192+
| Scenario | Derived tables rebuilt? | Why |
193+
|---|---|---|
194+
| First-ever `docker compose up` (empty ClickHouse volume) | Yes | The fresh-install init scripts run `generate_derived_tables.sql` as part of first-time database setup. |
195+
| `docker compose up` on an existing, already-initialized database, no pending migration | No | Docker's init scripts only run once against an empty data volume; nothing else rebuilds derived tables on a plain restart. |
196+
| After importing a study (`metaImport.py`) | Yes, automatically | `metaImport.py` rebuilds derived tables after every successful import, unless you pass `--no-derive-tables` (see below). |
197+
| After `docker compose up` applies a pending schema migration | Yes, automatically | `migrate_db.py` is invoked with `--regenerate-derived-tables` in `cbioportal-docker-compose`, so it regenerates derived tables whenever `generate_derived_tables.sql`'s own version differs from the database's current version — whether or not the migration that triggered it touched base tables. See [§11 Version Migration](#11-version-migration). |
198+
| Manual/institutional deployments running `migrate_db.py` directly (no docker-compose) | No, unless you opt in | `migrate_db.py` does **not** regenerate derived tables by default — pass `--regenerate-derived-tables`, or rebuild them yourself as a separate step. See [§11 Version Migration](#11-version-migration). |
199+
192200
By default, `metaImport.py` **automatically rebuilds derived tables** after every import. This ensures query performance stays fast after loading new studies.
193201

194202
### Skipping Derived Table Rebuild (`--no-derive-tables` and `derive-tables`)
@@ -268,17 +276,29 @@ Starting with `DB_SCHEMA_VERSION` `3.0.0`, in-place schema upgrades are handled
268276
sections) applied by `db-scripts/clickhouse/migrate/migrate_db.py`. The runner reads the current
269277
`db_schema_version` from the `info` table, skips sections already applied, and applies the rest in
270278
order. Derived table schema updates (tracked by `DERIVED_TABLE_SCHEMA_VERSION`) version
271-
independently and are applied by rebuilding your derived tables with `generate_derived_tables.sql`.
279+
independently, and are applied by rebuilding derived tables with `generate_derived_tables.sql`.
280+
281+
**Version-bump convention:** any migration that changes base tables must also bump
282+
`generate_derived_tables.sql`'s version (even with no semantic change to that script), since a
283+
base-table change may affect derived tables in ways that aren't obvious to every contributor.
284+
`derived_table_schema_version` can also bump on its own, with no corresponding base-table
285+
migration.
272286

273287
**Docker Compose deployments:** `git pull` the latest `cbioportal-docker-compose` master, then
274288
`docker compose up`. The migration step runs automatically before the `cbioportal` service starts;
275-
on a fresh install it's a safe no-op since `schema.sql` already seeds `info` at the current version.
289+
on a fresh install it's a safe no-op since `schema.sql` already seeds `info` at the current
290+
version. It also regenerates derived tables automatically whenever `generate_derived_tables.sql`'s
291+
version differs from the database's current `derived_table_schema_version` — you don't need a
292+
separate manual step.
276293

277294
**Manual deployments (e.g. ClickHouse Cloud, Kubernetes, or any setup that doesn't go through
278-
`cbioportal-docker-compose`):** run `migrate_db.py` directly against your database, then rebuild
279-
derived tables, before deploying the new cBioPortal backend image. The backend refuses to start
280-
against a `db_schema_version` that doesn't match its build's `db.version` unless
281-
`db.suppress_schema_version_mismatch_errors=true` is set.
295+
`cbioportal-docker-compose`):** run `migrate_db.py` directly against your database before deploying
296+
the new cBioPortal backend image. By default `migrate_db.py` only touches base tables — pass
297+
`--regenerate-derived-tables` if you want it to also regenerate derived tables (using the same
298+
version-comparison logic described above) in the same run; otherwise, rebuild derived tables
299+
yourself as a separate step (e.g. if you run derivation through your own tooling against
300+
ClickHouse Cloud). The backend refuses to start against a `db_schema_version` that doesn't match
301+
its build's `db.version` unless `db.suppress_schema_version_mismatch_errors=true` is set.
282302

283303
Upgrades from **before** `3.0.0` (i.e. the original v6→v7 migration, or any pre-migration-tooling
284304
ClickHouse deployment) still require the manual re-import process, since no migration path exists

src/main/resources/db-scripts/clickhouse/generate_derived_tables.sql

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -711,3 +711,8 @@ OPTIMIZE TABLE genetic_alteration_derived;
711711
OPTIMIZE TABLE generic_assay_data_derived;
712712
OPTIMIZE TABLE generic_assay_profile_entity_derived;
713713
OPTIMIZE TABLE generic_assay_meta_derived;
714+
715+
-- Record the version of this script in info.derived_table_schema_version, so callers (e.g.
716+
-- migrate_db.py --regenerate-derived-tables) can detect whether derived tables are stale by
717+
-- comparing against this script's own header version, regardless of who/what last ran it.
718+
ALTER TABLE info UPDATE derived_table_schema_version = '2.0.0' WHERE 1;

src/main/resources/db-scripts/clickhouse/migrate/migrate_db.py

Lines changed: 88 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,12 @@
55
sections and applies any section newer than the target database's current db_schema_version,
66
strictly in ascending order. See the header of migrate_schema.sql for the section format.
77
8+
With --regenerate-derived-tables, also regenerates derived tables (generate_derived_tables.sql)
9+
whenever its version differs from the database's current info.derived_table_schema_version. This
10+
covers both a derived-table rebuild required by a base-table migration and a derived-table-only
11+
version bump shipped with no corresponding migrate_schema.sql section. Off by default, since
12+
institutional deployments may run derived-table regeneration as a separate manual step.
13+
814
ClickHouse connection is configured via environment variables, matching the convention used by
915
cbioportal-core's rebuild_derived_tables.py:
1016
CLICKHOUSE_HOST, CLICKHOUSE_NATIVE_PORT, CLICKHOUSE_USER, CLICKHOUSE_PASSWORD, CLICKHOUSE_DB
@@ -29,9 +35,13 @@
2935
SECTION_HEADER_RE = re.compile(r'^##\s*db_schema_version:\s*(\S+)\s*$')
3036
DESCRIPTION_RE = re.compile(r'^##\s*description:\s*(.*)$')
3137
CUSTOM_RE = re.compile(r'^##\s*custom:\s*true\s*$', re.IGNORECASE)
38+
DERIVED_TABLE_VERSION_HEADER_RE = re.compile(
39+
r'^-- version (\d+\.\d+\.\d+) of derived table schema and data definition\s*$')
3240

3341
SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__))
3442
DEFAULT_MIGRATE_SCHEMA_SQL = os.path.join(SCRIPT_DIR, 'migrate_schema.sql')
43+
DEFAULT_GENERATE_DERIVED_TABLES_SQL = os.path.normpath(
44+
os.path.join(SCRIPT_DIR, os.pardir, 'generate_derived_tables.sql'))
3545

3646
# Hardcoded custom migration steps, keyed by db_schema_version. Each function receives the
3747
# ClickHouse connection properties dict and is responsible for whatever work that version's
@@ -94,6 +104,16 @@ def flush():
94104
return sections
95105

96106

107+
def get_derived_tables_sql_version(filepath):
108+
with open(filepath) as f:
109+
first_line = f.readline()
110+
match = DERIVED_TABLE_VERSION_HEADER_RE.match(first_line.strip())
111+
if not match:
112+
raise RuntimeError(
113+
f"Could not parse derived table schema version from the first line of {filepath}")
114+
return match.group(1)
115+
116+
97117
def get_clickhouse_props():
98118
required_props = {
99119
'host': 'CLICKHOUSE_HOST',
@@ -175,6 +195,15 @@ def get_current_db_schema_version(ch_props):
175195
return version
176196

177197

198+
def get_current_derived_table_schema_version(ch_props):
199+
version = run_query(ch_props, "SELECT derived_table_schema_version FROM info LIMIT 1")
200+
if not version:
201+
raise RuntimeError(
202+
"Could not read derived_table_schema_version from info table. "
203+
"Is the database initialized?")
204+
return version
205+
206+
178207
def wait_for_mutations(ch_props, timeout_secs=300, poll_interval_secs=2):
179208
"""Block until all ClickHouse mutations in this database have completed.
180209
@@ -209,8 +238,42 @@ def apply_section(ch_props, section):
209238
print(GREEN + f"Applied db_schema_version {section.version}" + END)
210239

211240

212-
def run_migrations(migrate_schema_sql_filepath=None):
213-
"""Apply any pending migrations. Returns True on success, False on failure."""
241+
def maybe_regenerate_derived_tables(ch_props, derived_table_sql_filepath=None):
242+
"""Regenerate derived tables if generate_derived_tables.sql's own version differs from the
243+
database's current info.derived_table_schema_version. Runs unconditionally as a blanket
244+
policy whenever that's the case — covers both a derived-table rebuild required by a
245+
base-table migration and a derived-table-only version bump with no corresponding
246+
migrate_schema.sql section."""
247+
filepath = derived_table_sql_filepath or DEFAULT_GENERATE_DERIVED_TABLES_SQL
248+
if not os.path.exists(filepath):
249+
raise RuntimeError(f"Could not find generate_derived_tables.sql at {filepath}")
250+
251+
expected_version = get_derived_tables_sql_version(filepath)
252+
current_version = get_current_derived_table_schema_version(ch_props)
253+
if expected_version == current_version:
254+
print(f"Derived tables are already at derived_table_schema_version "
255+
f"{current_version}. Nothing to do.")
256+
return
257+
258+
print(f"Regenerating derived tables: derived_table_schema_version {current_version} -> "
259+
f"{expected_version}")
260+
optimize_backoff_secs = os.environ.get('CLICKHOUSE_OPTIMIZE_BACKOFF_SECS', '0')
261+
_run_client(ch_props, [
262+
'--multiquery',
263+
'--queries-file', filepath,
264+
'--param_optimize_backoff_secs', optimize_backoff_secs,
265+
])
266+
# generate_derived_tables.sql ends with an ALTER TABLE info UPDATE for
267+
# derived_table_schema_version, which is an async mutation.
268+
wait_for_mutations(ch_props)
269+
print(GREEN + f"Regenerated derived tables at derived_table_schema_version "
270+
f"{expected_version}" + END)
271+
272+
273+
def run_migrations(migrate_schema_sql_filepath=None, regenerate_derived_tables=False,
274+
derived_table_sql_filepath=None):
275+
"""Apply any pending migrations (and optionally regenerate derived tables). Returns True on
276+
success, False on failure."""
214277
config_path = None
215278
try:
216279
filepath = migrate_schema_sql_filepath or DEFAULT_MIGRATE_SCHEMA_SQL
@@ -229,12 +292,14 @@ def run_migrations(migrate_schema_sql_filepath=None):
229292

230293
if not pending:
231294
print(f"Database is already at db_schema_version {current_version}. Nothing to do.")
232-
return True
295+
else:
296+
print(f"Current db_schema_version: {current_version}. "
297+
f"{len(pending)} migration(s) to apply.")
298+
for section in pending:
299+
apply_section(ch_props, section)
233300

234-
print(f"Current db_schema_version: {current_version}. "
235-
f"{len(pending)} migration(s) to apply.")
236-
for section in pending:
237-
apply_section(ch_props, section)
301+
if regenerate_derived_tables:
302+
maybe_regenerate_derived_tables(ch_props, derived_table_sql_filepath)
238303

239304
return True
240305
except Exception as e:
@@ -251,9 +316,24 @@ def main():
251316
'--migrate-schema-sql',
252317
default=None,
253318
help="Path to migrate_schema.sql (defaults to the file next to this script)")
319+
parser.add_argument(
320+
'--regenerate-derived-tables',
321+
action='store_true',
322+
help="After applying migrations, also regenerate derived tables if "
323+
"generate_derived_tables.sql's version differs from the database's current "
324+
"derived_table_schema_version. Off by default: institutional deployments that run "
325+
"derived-table regeneration as a separate manual step should leave this unset.")
326+
parser.add_argument(
327+
'--derived-tables-sql',
328+
default=None,
329+
help="Path to generate_derived_tables.sql (defaults to the file next to this script's "
330+
"parent directory); only used with --regenerate-derived-tables")
254331
args = parser.parse_args()
255332

256-
success = run_migrations(args.migrate_schema_sql)
333+
success = run_migrations(
334+
args.migrate_schema_sql,
335+
regenerate_derived_tables=args.regenerate_derived_tables,
336+
derived_table_sql_filepath=args.derived_tables_sql)
257337
sys.exit(0 if success else 1)
258338

259339

src/main/resources/db-scripts/clickhouse/migrate/migrate_schema.sql

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,9 @@
99
-- <SQL statements for this section>
1010
--
1111
-- Rules for writing a section:
12-
-- 1. Sections must be idempotent (IF EXISTS / IF NOT EXISTS everywhere) — ClickHouse has no
13-
-- transactions, so a crash mid-section must be safe to re-run from the top.
12+
-- 1. Sections should be idempotent wherever possible (IF EXISTS / IF NOT EXISTS) — ClickHouse
13+
-- has no transactions, so a crash mid-section ideally leaves it safe to re-run from the top.
14+
-- Not always achievable; use judgment, and note in the description when a section isn't.
1415
-- 2. Never DROP a column in the same section that reads from it.
1516
-- 3. For ORDER BY / primary-key changes: create a new table, INSERT ... SELECT, RENAME —
1617
-- ClickHouse cannot ALTER these in place.
@@ -22,6 +23,10 @@
2223
-- that must be computed outside ClickHouse). migrate_db.py runs this section's SQL (if any)
2324
-- first, then calls the matching hardcoded Python function registered in
2425
-- migrate_db.py's CUSTOM_MIGRATIONS dict, keyed by this section's version.
26+
-- 6. Any section that changes base tables must also bump ../generate_derived_tables.sql's
27+
-- version (even with no semantic change to that file), since a base-table change may affect
28+
-- derived tables in ways that aren't obvious to every author. derived_table_schema_version
29+
-- may also bump on its own with no corresponding section here.
2530
--
2631
-- Sections for versions already recorded in the target database's info.db_schema_version are
2732
-- skipped automatically — do not remove or renumber old sections once released.

test/test_db_version.sh

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -99,12 +99,20 @@ migrate_schema_sql_db_version=$(grep -oE '^##[[:space:]]*db_schema_version:[[:sp
9999
generate_derived_tables_sql_version=$(head -n 1 ${GENERATE_DERIVED_TABLES_SQL} | \
100100
sed -E 's/^-- version ([0-9]+\.[0-9]+\.[0-9]+) of derived table schema and data definition/\1/')
101101

102+
# --- generate_derived_tables.sql: its own trailing "ALTER TABLE info UPDATE
103+
# derived_table_schema_version = 'X'" statement, which is what actually gets written to the
104+
# database when this script runs — must agree with the header above ---
105+
generate_derived_tables_sql_info_update_line=$(grep 'ALTER TABLE info UPDATE derived_table_schema_version' ${GENERATE_DERIVED_TABLES_SQL} | tail -n 1)
106+
find_delimited_substrings "$generate_derived_tables_sql_info_update_line" "'" "'"
107+
generate_derived_tables_sql_info_update_version=${found_delimited_substrings[0]}
108+
102109
echo "pom.xml db.version is $pom_db_version"
103110
echo "schema.sql db_schema_version is $schema_sql_db_version"
104111
echo "migrate_schema.sql highest db_schema_version section is $migrate_schema_sql_db_version"
105112
echo "pom.xml derived_table.version is $pom_derived_table_version"
106113
echo "schema.sql derived_table_schema_version is $schema_sql_derived_table_version"
107114
echo "generate_derived_tables.sql header version is $generate_derived_tables_sql_version"
115+
echo "generate_derived_tables.sql info UPDATE version is $generate_derived_tables_sql_info_update_version"
108116

109117
if [ "$pom_db_version" == "$schema_sql_db_version" ] &&
110118
[ "$schema_sql_db_version" == "$migrate_schema_sql_db_version" ] ; then
@@ -113,7 +121,8 @@ else
113121
db_versions_all_match="no"
114122
fi
115123
if [ "$pom_derived_table_version" == "$schema_sql_derived_table_version" ] &&
116-
[ "$schema_sql_derived_table_version" == "$generate_derived_tables_sql_version" ] ; then
124+
[ "$schema_sql_derived_table_version" == "$generate_derived_tables_sql_version" ] &&
125+
[ "$generate_derived_tables_sql_version" == "$generate_derived_tables_sql_info_update_version" ] ; then
117126
derived_table_versions_all_match="yes"
118127
else
119128
derived_table_versions_all_match="no"

0 commit comments

Comments
 (0)