From 3eb11b17455383efbc6ac79b3ac7190806cba1f9 Mon Sep 17 00:00:00 2001 From: Agent Zero Date: Mon, 3 Aug 2026 13:29:16 +0200 Subject: [PATCH] Phase 1: Migrationsaudit + Forward-Migrationen 0093-0096 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Audit (docs/migration_history_audit.md): - files.size_bytes: INTEGER (Alembic) vs BIGINT (Produktion/Plugin) - GIN-Indizes: Fehlendes USING GIN in Alembic 0002 - guest_users: ix_guest_users_email_tenant fehlt UNIQUE in Alembic 0059 - plugins.name: Doppelter Unique-Index in Produktion Forward-Migrationen: - 0093: files.size_bytes INTEGER → BIGINT - 0094: GIN-Indizes reparieren + plugins.name doppelten Index entfernen - 0095: guest_users email+tenant_id UNIQUE INDEX (mit Dubletten-Check) - 0096: Workspace tenant_integrity (tenant-bound FKs) Tests: 41/41 bestanden (17 Workspace + 24 Command) Alembic Head: 0096 --- .../0093_fix_files_size_bytes_bigint.py | 34 +++++++ .../0094_fix_gin_indexes_and_plugins_dedup.py | 53 +++++++++++ .../versions/0095_fix_guest_users_unique.py | 54 +++++++++++ .../0096_workspace_tenant_integrity.py | 93 +++++++++++++++++++ docs/migration_history_audit.md | 91 ++++++++++++++++++ 5 files changed, 325 insertions(+) create mode 100644 alembic/versions/0093_fix_files_size_bytes_bigint.py create mode 100644 alembic/versions/0094_fix_gin_indexes_and_plugins_dedup.py create mode 100644 alembic/versions/0095_fix_guest_users_unique.py create mode 100644 alembic/versions/0096_workspace_tenant_integrity.py create mode 100644 docs/migration_history_audit.md diff --git a/alembic/versions/0093_fix_files_size_bytes_bigint.py b/alembic/versions/0093_fix_files_size_bytes_bigint.py new file mode 100644 index 0000000..3a4c863 --- /dev/null +++ b/alembic/versions/0093_fix_files_size_bytes_bigint.py @@ -0,0 +1,34 @@ +"""Fix files.size_bytes type: INTEGER → BIGINT. + +The DMS plugin migration (0001_initial.sql) created size_bytes as BIGINT, +but Alembic migration 0071 created it as INTEGER. +Production already has BIGINT (from plugin migration). +This migration aligns Alembic with production. + +Revision ID: 0093 +Revises: 0092 +""" + +from __future__ import annotations + +from alembic import op + +revision = "0093" +down_revision = "0092" +branch_labels = None +depends_on = None + + +def upgrade() -> None: + # Align size_bytes with production (BIGINT) + op.execute( + "ALTER TABLE IF EXISTS files " + "ALTER COLUMN size_bytes TYPE BIGINT" + ) + + +def downgrade() -> None: + op.execute( + "ALTER TABLE IF EXISTS files " + "ALTER COLUMN size_bytes TYPE INTEGER" + ) diff --git a/alembic/versions/0094_fix_gin_indexes_and_plugins_dedup.py b/alembic/versions/0094_fix_gin_indexes_and_plugins_dedup.py new file mode 100644 index 0000000..bab1cc8 --- /dev/null +++ b/alembic/versions/0094_fix_gin_indexes_and_plugins_dedup.py @@ -0,0 +1,53 @@ +"""Fix GIN indexes and remove duplicate plugins.name index. + +Alembic 0002 created search indexes without USING GIN. +Production already has GIN indexes (corrected by later migrations or manual). +This migration ensures GIN indexes exist for both fresh install and existing DBs. + +Also removes the redundant ix_plugins_name unique index (plugins_name_key +already enforces uniqueness from the column definition). + +Revision ID: 0094 +Revises: 0093 +""" + +from __future__ import annotations + +from alembic import op + +revision = "0094" +down_revision = "0093" +branch_labels = None +depends_on = None + +# GIN indexes that should exist with USING GIN +GIN_INDEXES = [ + ("contacts", "ix_contacts_search_tsv", "search_tsv"), + ("audit_log", "ix_audit_log_search_tsv", "search_tsv"), + ("calendar_entries", "ix_cal_entries_search_tsv", "search_tsv"), + ("comm_messages", "ix_comm_messages_search_tsv", "search_tsv"), + ("files", "ix_files_content_tsv", "content_tsv"), + ("mails", "ix_mails_body_tsv", "body_tsv"), + ("tags", "ix_tags_search_tsv", "search_tsv"), +] + + +def upgrade() -> None: + # Fix GIN indexes: drop and recreate with USING GIN (idempotent) + for table, index_name, column in GIN_INDEXES: + op.execute(f"DROP INDEX IF EXISTS {index_name}") + op.execute( + f"CREATE INDEX IF NOT EXISTS {index_name} " + f"ON {table} USING gin ({column})" + ) + + # Remove redundant plugins.name index (plugins_name_key already enforces uniqueness) + op.execute("DROP INDEX IF EXISTS ix_plugins_name") + + +def downgrade() -> None: + # Recreate the dropped index without GIN (not truly reversible to wrong state) + op.execute( + "CREATE INDEX IF NOT EXISTS ix_plugins_name ON plugins (name)" + ) + # GIN indexes cannot be meaningfully downgraded to non-GIN diff --git a/alembic/versions/0095_fix_guest_users_unique.py b/alembic/versions/0095_fix_guest_users_unique.py new file mode 100644 index 0000000..007c614 --- /dev/null +++ b/alembic/versions/0095_fix_guest_users_unique.py @@ -0,0 +1,54 @@ +"""Fix guest_users email+tenant_id unique index. + +Alembic 0059 created ix_guest_users_email_tenant as a normal (non-unique) index. +The SQLAlchemy model defines it as unique=True, and production already has +a UNIQUE INDEX. This migration aligns Alembic with production. + +Before creating the unique index, checks for duplicate (email, tenant_id) pairs. +If duplicates exist, the migration aborts with a data cleanup report. + +Revision ID: 0095 +Revises: 0094 +""" + +from __future__ import annotations + +from alembic import op +import sqlalchemy as sa + +revision = "0095" +down_revision = "0094" +branch_labels = None +depends_on = None + + +def upgrade() -> None: + # Check for duplicates before creating unique index + conn = op.get_bind() + duplicates = conn.execute( + sa.text( + "SELECT email, tenant_id, count(*) FROM guest_users " + "GROUP BY email, tenant_id HAVING count(*) > 1" + ) + ).fetchall() + + if duplicates: + raise RuntimeError( + f"Cannot create unique index: {len(duplicates)} duplicate (email, tenant_id) pairs found. " + "Data cleanup required before migration." + ) + + # Drop the non-unique index and recreate as unique + op.execute("DROP INDEX IF EXISTS ix_guest_users_email_tenant") + op.execute( + "CREATE UNIQUE INDEX IF NOT EXISTS ix_guest_users_email_tenant " + "ON guest_users (email, tenant_id)" + ) + + +def downgrade() -> None: + op.execute("DROP INDEX IF EXISTS ix_guest_users_email_tenant") + op.execute( + "CREATE INDEX IF NOT EXISTS ix_guest_users_email_tenant " + "ON guest_users (email, tenant_id)" + ) diff --git a/alembic/versions/0096_workspace_tenant_integrity.py b/alembic/versions/0096_workspace_tenant_integrity.py new file mode 100644 index 0000000..d9f99eb --- /dev/null +++ b/alembic/versions/0096_workspace_tenant_integrity.py @@ -0,0 +1,93 @@ +"""Workspace tenant integrity constraints. + +Plan 4.3: Add tenant-bound foreign keys to workspace child tables. + +- workspaces: UNIQUE (tenant_id, id) +- workspace_modules: FK (tenant_id, workspace_id) → workspaces (tenant_id, id) +- workspace_widgets: FK (tenant_id, workspace_id) → workspaces (tenant_id, id) +- workspace_users: FK (tenant_id, workspace_id) → workspaces (tenant_id, id) +- workspace_users: FK (tenant_id, user_id) → user_tenants (tenant_id, user_id) + +Revision ID: 0096 +Revises: 0095 +""" + +from __future__ import annotations + +from alembic import op + +revision = "0096" +down_revision = "0095" +branch_labels = None +depends_on = None + + +def upgrade() -> None: + # 1. Add UNIQUE (tenant_id, id) on workspaces + op.execute( + "CREATE UNIQUE INDEX IF NOT EXISTS uq_workspaces_tenant_id " + "ON workspaces (tenant_id, id)" + ) + + # 2. Drop existing FKs on workspace_modules (workspace_id → workspaces.id) + # and replace with tenant-bound FK + op.execute("ALTER TABLE workspace_modules DROP CONSTRAINT IF EXISTS workspace_modules_workspace_id_fkey") + op.execute( + "ALTER TABLE workspace_modules " + "ADD CONSTRAINT fk_wm_tenant_workspace " + "FOREIGN KEY (tenant_id, workspace_id) " + "REFERENCES workspaces (tenant_id, id) ON DELETE CASCADE" + ) + + # 3. Drop existing FK on workspace_widgets and replace with tenant-bound FK + op.execute("ALTER TABLE workspace_widgets DROP CONSTRAINT IF EXISTS workspace_widgets_workspace_id_fkey") + op.execute( + "ALTER TABLE workspace_widgets " + "ADD CONSTRAINT fk_ww_tenant_workspace " + "FOREIGN KEY (tenant_id, workspace_id) " + "REFERENCES workspaces (tenant_id, id) ON DELETE CASCADE" + ) + + # 4. Drop existing FK on workspace_users and replace with tenant-bound FK + op.execute("ALTER TABLE workspace_users DROP CONSTRAINT IF EXISTS workspace_users_workspace_id_fkey") + op.execute( + "ALTER TABLE workspace_users " + "ADD CONSTRAINT fk_wu_tenant_workspace " + "FOREIGN KEY (tenant_id, workspace_id) " + "REFERENCES workspaces (tenant_id, id) ON DELETE CASCADE" + ) + + # 5. Add FK on workspace_users (tenant_id, user_id) → user_tenants (tenant_id, user_id) + op.execute( + "ALTER TABLE workspace_users " + "ADD CONSTRAINT fk_wu_tenant_user " + "FOREIGN KEY (tenant_id, user_id) " + "REFERENCES user_tenants (tenant_id, user_id) ON DELETE CASCADE" + ) + + +def downgrade() -> None: + # Remove tenant-bound FKs, restore simple FKs + op.execute("ALTER TABLE workspace_users DROP CONSTRAINT IF EXISTS fk_wu_tenant_user") + op.execute("ALTER TABLE workspace_users DROP CONSTRAINT IF EXISTS fk_wu_tenant_workspace") + op.execute( + "ALTER TABLE workspace_users " + "ADD CONSTRAINT workspace_users_workspace_id_fkey " + "FOREIGN KEY (workspace_id) REFERENCES workspaces (id) ON DELETE CASCADE" + ) + + op.execute("ALTER TABLE workspace_widgets DROP CONSTRAINT IF EXISTS fk_ww_tenant_workspace") + op.execute( + "ALTER TABLE workspace_widgets " + "ADD CONSTRAINT workspace_widgets_workspace_id_fkey " + "FOREIGN KEY (workspace_id) REFERENCES workspaces (id) ON DELETE CASCADE" + ) + + op.execute("ALTER TABLE workspace_modules DROP CONSTRAINT IF EXISTS fk_wm_tenant_workspace") + op.execute( + "ALTER TABLE workspace_modules " + "ADD CONSTRAINT workspace_modules_workspace_id_fkey " + "FOREIGN KEY (workspace_id) REFERENCES workspaces (id) ON DELETE CASCADE" + ) + + op.execute("DROP INDEX IF EXISTS uq_workspaces_tenant_id") diff --git a/docs/migration_history_audit.md b/docs/migration_history_audit.md new file mode 100644 index 0000000..89bf53c --- /dev/null +++ b/docs/migration_history_audit.md @@ -0,0 +1,91 @@ +# Migration History Audit + +**Erstellt:** 2026-08-03 +**Alembic-Head:** 0092 +**Produktions-Stand:** 0092 + +--- + +## Bestätigte Schema-Diskrepanzen + +### 1. files.size_bytes — Typ-Diskrepanz + +| Quelle | Typ | +|--------|-----| +| Alembic 0071 | INTEGER | +| DMS Plugin Migration 0001 | BIGINT | +| SQLAlchemy Model | Integer | +| **Produktion** | **bigint** | + +**Klassifizierung:** Echte Schemaänderung +**Forward-Migration:** 0093 — `ALTER COLUMN size_bytes TYPE BIGINT` + +### 2. GIN-Indizes — Fehlendes USING GIN + +Alembic 0002 erstellt: +```sql +CREATE INDEX ix_companies_search_vec ON companies (search_tsv) +``` + +Produktion hat: +```sql +CREATE INDEX ix_companies_search_vec ON companies USING gin (search_tsv) +``` + +Betroffene Tabellen/Indizes (in Produktion als GIN vorhanden): +- contacts.ix_contacts_search_tsv +- audit_log.ix_audit_log_search_tsv +- calendar_entries.ix_cal_entries_search_tsv +- comm_messages.ix_comm_messages_search_tsv +- files.ix_files_content_tsv +- mails.ix_mails_body_tsv +- tags.ix_tags_search_tsv + +**Klassifizierung:** Echte Schemaänderung (Index-Typ) +**Forward-Migration:** 0094 — GIN-Indizes neu erstellen mit USING GIN + +### 3. guest_users — Fehlender UNIQUE Constraint + +Alembic 0059 erstellt: +```sql +CREATE INDEX ix_guest_users_email_tenant ON guest_users (email, tenant_id) +``` + +Model und Produktion haben: +```sql +CREATE UNIQUE INDEX ix_guest_users_email_tenant ON guest_users (email, tenant_id) +``` + +**Klassifizierung:** Echte Schemaänderung (Unique fehlt in Alembic) +**Forward-Migration:** 0095 — Index als UNIQUE neu erstellen + +### 4. plugins.name — Doppelter Unique-Index + +Produktion hat zwei UNIQUE-Indizes auf plugins.name: +- `plugins_name_key` (von `unique=True` in Column-Definition) +- `ix_plugins_name` (von explizitem `CREATE INDEX` in 0003, als UNIQUE in Produktion) + +Alembic 0003 erstellt `ix_plugins_name` ohne `UNIQUE`, aber Column hat `unique=True`. + +**Klassifizierung:** Nur Idempotenzänderung (Redundanz) +**Forward-Migration:** 0094 — Doppelten Index entfernen + +--- + +## Keine Diskrepanz gefunden + +- tenants.slug: unique=True in 0001 + Model + Produktion → ✅ +- plugin_migrations: UniqueConstraint in 0003 + Model + Produktion → ✅ +- RLS-Policies: Alle korrekt in Produktion → ✅ +- Workspace-Tabellen: RLS fail-closed, Tabellen korrekt → ✅ + +--- + +## Forward-Migration-Plan + +| Migration | Inhalt | +|-----------|--------| +| 0093 | files.size_bytes INTEGER → BIGINT | +| 0094 | GIN-Indizes reparieren + plugins.name doppelten Index entfernen | +| 0095 | guest_users email+tenant_id UNIQUE INDEX | +| 0096 | Workspace tenant_integrity (Plan 4.3) |