You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
228 lines
8.5 KiB
228 lines
8.5 KiB
"""Generate the Swedish -> English migration and its rollback from rename_map.py. |
|
|
|
Writing these by hand invites a typo in one direction that the other does not |
|
mirror. Generating both from one map means the rollback is correct by construction. |
|
|
|
python scripts/generate_rename_migration.py |
|
""" |
|
from __future__ import annotations |
|
|
|
import sys |
|
from pathlib import Path |
|
|
|
ROOT = Path(__file__).resolve().parents[1] |
|
sys.path.insert(0, str(ROOT)) |
|
|
|
from _postgres.rename_map import COLUMNS, INDEXES, TABLES # noqa: E402 |
|
|
|
OUT = ROOT / "_postgres" / "migrations" |
|
STAMP = "20260803_01" |
|
|
|
HEADER = """\ |
|
-- {title} |
|
-- |
|
-- Generated by scripts/generate_rename_migration.py from _postgres/rename_map.py. |
|
-- Do not edit by hand; edit the map and regenerate. |
|
-- |
|
-- ALTER TABLE ... RENAME is catalog-only in PostgreSQL: no table rewrite, no data |
|
-- movement, no index rebuild. Wall time is milliseconds regardless of row count, |
|
-- and the HNSW vector indexes are not touched. |
|
-- |
|
-- Guarded by an existence check, so this is a no-op on a database created from the |
|
-- current schema.sql and does the work on one created before the rename. One file |
|
-- serves both a fresh install and an existing deployment. |
|
|
|
BEGIN; |
|
|
|
SET LOCAL lock_timeout = '5s'; |
|
SET LOCAL statement_timeout = 0; |
|
|
|
DO $rename$ |
|
BEGIN |
|
IF {guard} THEN |
|
""" |
|
|
|
FOOTER = """\ |
|
END IF; |
|
END |
|
$rename$; |
|
|
|
{triggers} |
|
COMMIT; |
|
""" |
|
|
|
# Trigger function bodies are stored as opaque text and do NOT follow renames. |
|
# They would compile fine and then fail at the next INSERT with |
|
# 'record "new" has no field "anforandetext"'. Recreated unconditionally. |
|
TRIGGERS_FORWARD = """\ |
|
-- Recreate the search-vector triggers against the new column names. |
|
-- |
|
-- The text-search configuration is read from a database setting rather than |
|
-- hardcoded, so a deployment in another language does not have to patch DDL: |
|
-- ALTER DATABASE <db> SET app.fts_config = 'swedish'; |
|
-- It only takes effect for new sessions, so restart the API afterwards. |
|
|
|
DROP TRIGGER IF EXISTS talks_search_vector_trigger ON speeches; |
|
DROP TRIGGER IF EXISTS speeches_search_vector_trigger ON speeches; |
|
DROP FUNCTION IF EXISTS talks_search_vector_update(); |
|
|
|
CREATE OR REPLACE FUNCTION speeches_search_vector_update() RETURNS TRIGGER AS $fn$ |
|
BEGIN |
|
NEW.search_vector := to_tsvector( |
|
COALESCE(current_setting('app.fts_config', true), 'simple')::regconfig, |
|
coalesce(NEW.text, '')); |
|
RETURN NEW; |
|
END; |
|
$fn$ LANGUAGE plpgsql; |
|
|
|
CREATE TRIGGER speeches_search_vector_trigger |
|
BEFORE INSERT OR UPDATE OF text ON speeches |
|
FOR EACH ROW EXECUTE FUNCTION speeches_search_vector_update(); |
|
|
|
DROP TRIGGER IF EXISTS motions_search_vector_trigger ON documents; |
|
DROP TRIGGER IF EXISTS documents_search_vector_trigger ON documents; |
|
DROP FUNCTION IF EXISTS motions_search_vector_update(); |
|
|
|
CREATE OR REPLACE FUNCTION documents_search_vector_update() RETURNS TRIGGER AS $fn$ |
|
DECLARE cfg regconfig := COALESCE(current_setting('app.fts_config', true), 'simple')::regconfig; |
|
BEGIN |
|
NEW.search_vector := |
|
setweight(to_tsvector(cfg, coalesce(NEW.title, '')), 'A') || |
|
setweight(to_tsvector(cfg, coalesce(NEW.proposals_text, '')), 'B') || |
|
setweight(to_tsvector(cfg, coalesce(NEW.subtitle, '')), 'C') || |
|
setweight(to_tsvector(cfg, coalesce(NEW.text, '')), 'D'); |
|
RETURN NEW; |
|
END; |
|
$fn$ LANGUAGE plpgsql; |
|
|
|
CREATE TRIGGER documents_search_vector_trigger |
|
BEFORE INSERT OR UPDATE OF title, subtitle, text, proposals_text ON documents |
|
FOR EACH ROW EXECUTE FUNCTION documents_search_vector_update(); |
|
""" |
|
|
|
TRIGGERS_ROLLBACK = """\ |
|
-- Restore the original trigger functions. |
|
DROP TRIGGER IF EXISTS speeches_search_vector_trigger ON talks; |
|
DROP FUNCTION IF EXISTS speeches_search_vector_update(); |
|
|
|
CREATE OR REPLACE FUNCTION talks_search_vector_update() RETURNS TRIGGER AS $fn$ |
|
BEGIN |
|
NEW.search_vector := to_tsvector('swedish', coalesce(NEW.anforandetext, '')); |
|
RETURN NEW; |
|
END; |
|
$fn$ LANGUAGE plpgsql; |
|
|
|
DROP TRIGGER IF EXISTS talks_search_vector_trigger ON talks; |
|
CREATE TRIGGER talks_search_vector_trigger |
|
BEFORE INSERT OR UPDATE OF anforandetext ON talks |
|
FOR EACH ROW EXECUTE FUNCTION talks_search_vector_update(); |
|
|
|
DROP TRIGGER IF EXISTS documents_search_vector_trigger ON motions; |
|
DROP FUNCTION IF EXISTS documents_search_vector_update(); |
|
|
|
CREATE OR REPLACE FUNCTION motions_search_vector_update() RETURNS TRIGGER AS $fn$ |
|
BEGIN |
|
NEW.search_vector := |
|
setweight(to_tsvector('swedish', coalesce(NEW.titel, '')), 'A') || |
|
setweight(to_tsvector('swedish', coalesce(NEW.forslag_text, '')), 'B') || |
|
setweight(to_tsvector('swedish', coalesce(NEW.undertitel, '')), 'C') || |
|
setweight(to_tsvector('swedish', coalesce(NEW.text, '')), 'D'); |
|
RETURN NEW; |
|
END; |
|
$fn$ LANGUAGE plpgsql; |
|
|
|
DROP TRIGGER IF EXISTS motions_search_vector_trigger ON motions; |
|
CREATE TRIGGER motions_search_vector_trigger |
|
BEFORE INSERT OR UPDATE OF titel, undertitel, text, forslag_text ON motions |
|
FOR EACH ROW EXECUTE FUNCTION motions_search_vector_update(); |
|
""" |
|
|
|
|
|
def build(forward: bool) -> str: |
|
"""Emit the migration in one direction. |
|
|
|
Column renames run before table renames going forward (the columns are still on |
|
the old table), and after them going back. |
|
""" |
|
lines: list[str] = [] |
|
ind = " " |
|
|
|
def col_statements() -> list[str]: |
|
# Both directions address the table by its ORIGINAL name. Going forward, |
|
# columns are renamed before the table is; going back, the table has already |
|
# been renamed to its original name by the time these run. Getting this |
|
# wrong produces a rollback that fails halfway, mid-transaction. |
|
out = [] |
|
for table, cols in COLUMNS.items(): |
|
for old, new in cols.items(): |
|
a, b = (old, new) if forward else (new, old) |
|
out.append(f"{ind}ALTER TABLE {table} RENAME COLUMN {a} TO {b};") |
|
return out |
|
|
|
def table_statements() -> list[str]: |
|
out = [] |
|
for old, new in TABLES.items(): |
|
a, b = (old, new) if forward else (new, old) |
|
out.append(f"{ind}ALTER TABLE {a} RENAME TO {b};") |
|
return out |
|
|
|
if forward: |
|
lines += [" -- columns (still on their original tables)"] |
|
lines += col_statements() |
|
lines += ["", " -- tables"] |
|
lines += table_statements() |
|
lines += [ |
|
"", |
|
" -- New: lets bills, written questions and committee reports share this", |
|
" -- table instead of needing one table per document type. ADD COLUMN with a", |
|
" -- constant default is metadata-only in PostgreSQL 11+.", |
|
f"{ind}ALTER TABLE documents ADD COLUMN IF NOT EXISTS doc_type TEXT NOT NULL DEFAULT 'motion';", |
|
] |
|
lines += ["", " -- indexes (cosmetic, but keeps pg_dump diffs readable)"] |
|
for old, new in INDEXES.items(): |
|
lines.append(f"{ind}ALTER INDEX IF EXISTS {old} RENAME TO {new};") |
|
else: |
|
lines += [" -- indexes"] |
|
for old, new in INDEXES.items(): |
|
lines.append(f"{ind}ALTER INDEX IF EXISTS {new} RENAME TO {old};") |
|
lines += ["", " -- tables", f"{ind}ALTER TABLE documents DROP COLUMN IF EXISTS doc_type;"] |
|
lines += table_statements() |
|
lines += ["", " -- columns"] |
|
lines += col_statements() |
|
|
|
guard = ( |
|
"EXISTS (SELECT 1 FROM information_schema.columns\n" |
|
" WHERE table_schema = 'public' AND table_name = 'talks'\n" |
|
" AND column_name = 'anforandetext')" |
|
if forward |
|
else "EXISTS (SELECT 1 FROM information_schema.columns\n" |
|
" WHERE table_schema = 'public' AND table_name = 'speeches'\n" |
|
" AND column_name = 'text')" |
|
) |
|
title = ( |
|
"Rename the Swedish schema to neutral English" |
|
if forward |
|
else "ROLLBACK: restore the Swedish schema" |
|
) |
|
return ( |
|
HEADER.format(title=title, guard=guard) |
|
+ "\n".join(lines) |
|
+ "\n" |
|
+ FOOTER.format(triggers=TRIGGERS_FORWARD if forward else TRIGGERS_ROLLBACK) |
|
) |
|
|
|
|
|
def main() -> int: |
|
OUT.mkdir(parents=True, exist_ok=True) |
|
for forward, name in ((True, f"{STAMP}_rename_to_english.sql"), |
|
(False, f"{STAMP}_rename_to_english_ROLLBACK.sql")): |
|
path = OUT / name |
|
path.write_text(build(forward), encoding="utf-8") |
|
print(f" wrote {path.relative_to(ROOT)} ({len(path.read_text().splitlines())} lines)") |
|
total = sum(len(c) for c in COLUMNS.values()) |
|
print(f"\n {total} column renames, {len(TABLES)} table renames, {len(INDEXES)} index renames") |
|
return 0 |
|
|
|
|
|
if __name__ == "__main__": |
|
sys.exit(main())
|
|
|