Phase 1: Migrationsaudit + Forward-Migrationen 0093-0096

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
This commit is contained in:
Agent Zero
2026-08-03 13:29:16 +02:00
parent a760a759eb
commit 3eb11b1745
5 changed files with 325 additions and 0 deletions
@@ -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"
)
@@ -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
@@ -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)"
)
@@ -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")
+91
View File
@@ -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) |