Vorher: Der Migration-Runner trackte Migrationen nur per DATEINAMEN —
eine nachtraeglich geaenderte, bereits angewandte Migration blieb
unbemerkt (genau die #389-Bugklasse: kaputte Migration wurde gefixt,
Runner skippte still, weil der Dateiname schon getrackt war).
Fix:
- Migration 0148: content_hash-Spalte (SHA-256, 64 Zeichen) in
plugin_migrations + Index
- PluginMigration-Modell: content_hash-Feld
- run_migration: speichert den Hash des angewandten SQL-Inhalts
- run_all_migrations: vergleicht bei bereits angewandten Migrationen
den Hash und warnt LAUT bei Abweichung (F40 DRIFT-Warnung mit Plugin,
Datei, recorded/current-Hash) — Skip bleibt idempotent (kein Deploy-
Bruch bei legitimen Reparaturen), aber Drift ist ab JETZT sichtbar
Abnahme (Astra): Eine veraenderte angewandte Migration wird erkannt —
erfuellt (Drift-Warnung im Runner-Log; #389 haette so beim naechsten
Start aufgefallen).
Verifikation: Syntax OK, ruff clean, alembic heads = 0148.
Plugin migrations run after core migration 0085 which sets up RLS for
all known core tables. Plugin-created tables were left without RLS,
creating a critical multi-tenant isolation gap (84 tables affected).
The migration runner now automatically enables RLS on all newly created
tenant tables after validation:
- ENABLE + FORCE ROW LEVEL SECURITY
- Idempotent DROP IF EXISTS + CREATE fail-closed tenant isolation policy
- GRANT CRUD to crm_api and crm_worker
- ALTER TABLE OWNER TO crm_migration
Global tables (-- GLOBAL TABLE comment) are skipped.
Fixes 3 issues found by API integration tests:
1. migration_runner.py: Add GLOBAL TABLE exemption for tables without tenant_id
- New _extract_global_table_names() method parses -- GLOBAL TABLE: comments
- marketplace_listings is intentionally global (no tenant_id)
2. unified_search/migrations/0002_embeddings.sql: Remove companies table (does not exist),
add DO $$ BEGIN END $$ blocks to check table existence before ALTER
3. marketplace/migrations/0001_initial.sql: Add -- GLOBAL TABLE: marketplace_listings comment