727d86614e
P0 (7): Auth-bypass removed, migrations fixed, plugin-upload disabled, RLS FORCE+WITH CHECK, plugin double-registration fixed, persistent volume, domain removed P1 (11): User/tenant model, Redis centralized, worker separated, transactional outbox, XSS fixed, DMS chunked streaming, permissions unified, password reset, metrics secured, config/docs fixed, cross-tenant FK P2 (4): Contact model normalized, cross-imports reduced 94%, commands+state machines for contacts/dms/mail/calendar, SPA path-traversal 8 new migrations, 99 unit tests, 13 commands, 8 contracts, 72 files changed
183 lines
7.1 KiB
Python
183 lines
7.1 KiB
Python
"""Cross-tenant referential integrity: composite FKs on (tenant_id, contact_id).
|
|
|
|
Revision ID: 0036_cross_tenant_fk
|
|
Revises: 0035_comm_search_index
|
|
Create Date: 2026-07-25
|
|
|
|
Changes:
|
|
1. Add UNIQUE (tenant_id, id) on contacts — prerequisite for composite FK.
|
|
2. Replace contactpersons.contact_id FK with composite (tenant_id, contact_id)
|
|
→ contacts(tenant_id, id).
|
|
3. Replace contact_merge_history.source_contact_id FK with composite
|
|
(tenant_id, source_contact_id) → contacts(tenant_id, id).
|
|
4. Replace contact_merge_history.target_contact_id FK with composite
|
|
(tenant_id, target_contact_id) → contacts(tenant_id, id).
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from typing import Union
|
|
|
|
import sqlalchemy as sa
|
|
from alembic import op
|
|
|
|
# revision identifiers
|
|
revision: str = "0036_cross_tenant_fk"
|
|
down_revision: Union[str, None] = "0035_comm_search_index"
|
|
branch_labels: Union[str, None] = None
|
|
depends_on: Union[str, None] = None
|
|
|
|
|
|
def _constraint_exists(name: str) -> str:
|
|
"""Return SQL that checks if a constraint exists."""
|
|
return (
|
|
f"SELECT 1 FROM information_schema.table_constraints "
|
|
f"WHERE constraint_name = '{name}'"
|
|
)
|
|
|
|
|
|
def _fk_exists(name: str) -> str:
|
|
"""Return SQL that checks if a foreign key constraint exists."""
|
|
return (
|
|
f"SELECT 1 FROM information_schema.table_constraints "
|
|
f"WHERE constraint_name = '{name}' AND constraint_type = 'FOREIGN KEY'"
|
|
)
|
|
|
|
|
|
def upgrade() -> None:
|
|
conn = op.get_bind()
|
|
|
|
# ── 1. Add UNIQUE (tenant_id, id) on contacts ──────────────────────────
|
|
unique_name = "uq_contacts_tenant_id"
|
|
result = conn.execute(sa.text(_constraint_exists(unique_name))).fetchone()
|
|
if result is None:
|
|
op.execute(
|
|
f"ALTER TABLE contacts ADD CONSTRAINT {unique_name} "
|
|
f"UNIQUE (tenant_id, id)"
|
|
)
|
|
|
|
# ── 2. contactpersons: replace single-column FK with composite FK ──────
|
|
# Find and drop the existing FK on contactpersons.contact_id
|
|
old_cp_fk_result = conn.execute(
|
|
sa.text(
|
|
"SELECT conname FROM pg_constraint c "
|
|
"JOIN pg_class cls ON c.conrelid = cls.oid "
|
|
"JOIN pg_namespace nsp ON c.connamespace = nsp.oid "
|
|
"WHERE cls.relname = 'contactpersons' "
|
|
"AND nsp.nspname = 'public' "
|
|
"AND c.contype = 'f' "
|
|
"AND EXISTS ("
|
|
" SELECT 1 FROM pg_attribute a "
|
|
" WHERE a.attrelid = c.conrelid AND a.attname = 'contact_id' "
|
|
" AND a.attnum = ANY(c.conkey)"
|
|
")"
|
|
)
|
|
).fetchone()
|
|
|
|
if old_cp_fk_result is not None:
|
|
old_cp_fk_name = old_cp_fk_result[0]
|
|
op.execute(f"ALTER TABLE contactpersons DROP CONSTRAINT IF EXISTS {old_cp_fk_name}")
|
|
|
|
# Add composite FK on contactpersons (tenant_id, contact_id) → contacts(tenant_id, id)
|
|
cp_composite_fk = "fk_contactpersons_tenant_contact"
|
|
result = conn.execute(sa.text(_fk_exists(cp_composite_fk))).fetchone()
|
|
if result is None:
|
|
op.execute(
|
|
f"ALTER TABLE contactpersons ADD CONSTRAINT {cp_composite_fk} "
|
|
f"FOREIGN KEY (tenant_id, contact_id) "
|
|
f"REFERENCES contacts (tenant_id, id) ON DELETE CASCADE"
|
|
)
|
|
|
|
# ── 3. contact_merge_history: replace source_contact_id FK ─────────────
|
|
old_src_fk_result = conn.execute(
|
|
sa.text(
|
|
"SELECT conname FROM pg_constraint c "
|
|
"JOIN pg_class cls ON c.conrelid = cls.oid "
|
|
"JOIN pg_namespace nsp ON c.connamespace = nsp.oid "
|
|
"WHERE cls.relname = 'contact_merge_history' "
|
|
"AND nsp.nspname = 'public' "
|
|
"AND c.contype = 'f' "
|
|
"AND EXISTS ("
|
|
" SELECT 1 FROM pg_attribute a "
|
|
" WHERE a.attrelid = c.conrelid AND a.attname = 'source_contact_id' "
|
|
" AND a.attnum = ANY(c.conkey)"
|
|
")"
|
|
)
|
|
).fetchone()
|
|
|
|
if old_src_fk_result is not None:
|
|
old_src_fk_name = old_src_fk_result[0]
|
|
op.execute(f"ALTER TABLE contact_merge_history DROP CONSTRAINT IF EXISTS {old_src_fk_name}")
|
|
|
|
src_composite_fk = "fk_merge_history_tenant_source"
|
|
result = conn.execute(sa.text(_fk_exists(src_composite_fk))).fetchone()
|
|
if result is None:
|
|
op.execute(
|
|
f"ALTER TABLE contact_merge_history ADD CONSTRAINT {src_composite_fk} "
|
|
f"FOREIGN KEY (tenant_id, source_contact_id) "
|
|
f"REFERENCES contacts (tenant_id, id) ON DELETE SET NULL"
|
|
)
|
|
|
|
# ── 4. contact_merge_history: replace target_contact_id FK ──────────────
|
|
old_tgt_fk_result = conn.execute(
|
|
sa.text(
|
|
"SELECT conname FROM pg_constraint c "
|
|
"JOIN pg_class cls ON c.conrelid = cls.oid "
|
|
"JOIN pg_namespace nsp ON c.connamespace = nsp.oid "
|
|
"WHERE cls.relname = 'contact_merge_history' "
|
|
"AND nsp.nspname = 'public' "
|
|
"AND c.contype = 'f' "
|
|
"AND EXISTS ("
|
|
" SELECT 1 FROM pg_attribute a "
|
|
" WHERE a.attrelid = c.conrelid AND a.attname = 'target_contact_id' "
|
|
" AND a.attnum = ANY(c.conkey)"
|
|
")"
|
|
)
|
|
).fetchone()
|
|
|
|
if old_tgt_fk_result is not None:
|
|
old_tgt_fk_name = old_tgt_fk_result[0]
|
|
op.execute(f"ALTER TABLE contact_merge_history DROP CONSTRAINT IF EXISTS {old_tgt_fk_name}")
|
|
|
|
tgt_composite_fk = "fk_merge_history_tenant_target"
|
|
result = conn.execute(sa.text(_fk_exists(tgt_composite_fk))).fetchone()
|
|
if result is None:
|
|
op.execute(
|
|
f"ALTER TABLE contact_merge_history ADD CONSTRAINT {tgt_composite_fk} "
|
|
f"FOREIGN KEY (tenant_id, target_contact_id) "
|
|
f"REFERENCES contacts (tenant_id, id) ON DELETE CASCADE"
|
|
)
|
|
|
|
|
|
def downgrade() -> None:
|
|
conn = op.get_bind()
|
|
|
|
# Restore single-column FKs and remove composite FKs
|
|
|
|
# ── contact_merge_history: target ──
|
|
op.execute("ALTER TABLE contact_merge_history DROP CONSTRAINT IF EXISTS fk_merge_history_tenant_target")
|
|
op.execute(
|
|
"ALTER TABLE contact_merge_history ADD CONSTRAINT "
|
|
"contact_merge_history_target_contact_id_fkey "
|
|
"FOREIGN KEY (target_contact_id) REFERENCES contacts (id) ON DELETE CASCADE"
|
|
)
|
|
|
|
# ── contact_merge_history: source ──
|
|
op.execute("ALTER TABLE contact_merge_history DROP CONSTRAINT IF EXISTS fk_merge_history_tenant_source")
|
|
op.execute(
|
|
"ALTER TABLE contact_merge_history ADD CONSTRAINT "
|
|
"contact_merge_history_source_contact_id_fkey "
|
|
"FOREIGN KEY (source_contact_id) REFERENCES contacts (id) ON DELETE SET NULL"
|
|
)
|
|
|
|
# ── contactpersons ──
|
|
op.execute("ALTER TABLE contactpersons DROP CONSTRAINT IF EXISTS fk_contactpersons_tenant_contact")
|
|
op.execute(
|
|
"ALTER TABLE contactpersons ADD CONSTRAINT "
|
|
"contactpersons_contact_id_fkey "
|
|
"FOREIGN KEY (contact_id) REFERENCES contacts (id) ON DELETE CASCADE"
|
|
)
|
|
|
|
# ── Remove unique (tenant_id, id) on contacts ──
|
|
op.execute("ALTER TABLE contacts DROP CONSTRAINT IF EXISTS uq_contacts_tenant_id")
|