fix(gate-b): fresh-db install path - conditional guards on plugin-table migrations + dual-path convergence migrations
Check Cross-Plugin Imports / check (push) Has been cancelled
Check Cross-Plugin Imports / check (push) Has been cancelled
This commit is contained in:
@@ -18,7 +18,26 @@ branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def _table_exists(conn, table_name: str) -> bool:
|
||||
"""True when the table exists (dual-path convergence, Gate B).
|
||||
|
||||
On a fresh install the ai_assistant plugin SQL migration has not run
|
||||
yet when Alembic reaches this revision — skip instead of failing.
|
||||
The plugin-side migration adds the same columns idempotently.
|
||||
"""
|
||||
row = conn.execute(
|
||||
sa.text("SELECT to_regclass(:tname) IS NOT NULL"),
|
||||
{"tname": f"public.{table_name}"},
|
||||
).scalar()
|
||||
return bool(row)
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
conn = op.get_bind()
|
||||
if not _table_exists(conn, "ai_providers"):
|
||||
# Fresh-install path: table arrives with the ai_assistant plugin
|
||||
# migration, which includes these columns.
|
||||
return
|
||||
op.add_column("ai_providers", sa.Column("region", sa.String(20), nullable=False, server_default="unknown"))
|
||||
op.add_column("ai_providers", sa.Column("hosting_type", sa.String(30), nullable=False, server_default="cloud"))
|
||||
op.add_column("ai_providers", sa.Column("dpa_status", sa.String(20), nullable=False, server_default="none"))
|
||||
@@ -29,6 +48,9 @@ def upgrade() -> None:
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
conn = op.get_bind()
|
||||
if not _table_exists(conn, "ai_providers"):
|
||||
return
|
||||
op.drop_column("ai_providers", "allowed_data_classes")
|
||||
op.drop_column("ai_providers", "transfer_notice")
|
||||
op.drop_column("ai_providers", "training_on_customer_data")
|
||||
|
||||
@@ -17,126 +17,145 @@ branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def _table_exists(conn, table_name: str) -> bool:
|
||||
"""True when the table exists (dual-path convergence, Gate B).
|
||||
|
||||
On a fresh install the kommunikation plugin SQL migration has not run
|
||||
yet when Alembic reaches this revision — skip the comm_* parts instead
|
||||
of failing. The plugin-side migration adds the same column idempotently.
|
||||
"""
|
||||
row = conn.execute(
|
||||
sa.text("SELECT to_regclass(:tname) IS NOT NULL"),
|
||||
{"tname": f"public.{table_name}"},
|
||||
).scalar()
|
||||
return bool(row)
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
# 1. Add is_system column to comm_conversations
|
||||
op.add_column(
|
||||
"comm_conversations",
|
||||
sa.Column("is_system", sa.Boolean(), nullable=False, server_default=sa.text("false")),
|
||||
)
|
||||
op.create_index(
|
||||
"ix_comm_conversations_tenant_system",
|
||||
"comm_conversations",
|
||||
["tenant_id", "is_system"],
|
||||
)
|
||||
conn = op.get_bind()
|
||||
if _table_exists(conn, "comm_conversations"):
|
||||
# 1. Add is_system column to comm_conversations
|
||||
op.add_column(
|
||||
"comm_conversations",
|
||||
sa.Column("is_system", sa.Boolean(), nullable=False, server_default=sa.text("false")),
|
||||
)
|
||||
op.create_index(
|
||||
"ix_comm_conversations_tenant_system",
|
||||
"comm_conversations",
|
||||
["tenant_id", "is_system"],
|
||||
)
|
||||
|
||||
# 2. Create system channel per tenant (for tenants that have notifications)
|
||||
op.execute("""
|
||||
INSERT INTO comm_conversations (id, tenant_id, title, is_pinned, is_locked, is_direct, is_archived, is_system, created_by, created_by_type, metadata, created_at, updated_at)
|
||||
SELECT
|
||||
gen_random_uuid(),
|
||||
n.tenant_id,
|
||||
'System Channel',
|
||||
false,
|
||||
true,
|
||||
false,
|
||||
false,
|
||||
true,
|
||||
NULL,
|
||||
'system',
|
||||
'{}'::jsonb,
|
||||
NOW(),
|
||||
NOW()
|
||||
FROM (
|
||||
SELECT DISTINCT tenant_id FROM notifications WHERE deleted_at IS NULL
|
||||
) n
|
||||
WHERE NOT EXISTS (
|
||||
SELECT 1 FROM comm_conversations cc
|
||||
WHERE cc.tenant_id = n.tenant_id AND cc.is_system = true AND cc.deleted_at IS NULL
|
||||
);
|
||||
""")
|
||||
# 2. Create system channel per tenant (for tenants that have notifications)
|
||||
op.execute("""
|
||||
INSERT INTO comm_conversations (id, tenant_id, title, is_pinned, is_locked, is_direct, is_archived, is_system, created_by, created_by_type, metadata, created_at, updated_at)
|
||||
SELECT
|
||||
gen_random_uuid(),
|
||||
n.tenant_id,
|
||||
'System Channel',
|
||||
false,
|
||||
true,
|
||||
false,
|
||||
false,
|
||||
true,
|
||||
NULL,
|
||||
'system',
|
||||
'{}'::jsonb,
|
||||
NOW(),
|
||||
NOW()
|
||||
FROM (
|
||||
SELECT DISTINCT tenant_id FROM notifications WHERE deleted_at IS NULL
|
||||
) n
|
||||
WHERE NOT EXISTS (
|
||||
SELECT 1 FROM comm_conversations cc
|
||||
WHERE cc.tenant_id = n.tenant_id AND cc.is_system = true AND cc.deleted_at IS NULL
|
||||
);
|
||||
""")
|
||||
|
||||
# 3. Insert notifications as CommMessages in the system channel
|
||||
op.execute("""
|
||||
INSERT INTO comm_messages (id, tenant_id, conversation_id, sender_id, sender_type, content, content_format, metadata, created_at, updated_at)
|
||||
SELECT
|
||||
gen_random_uuid(),
|
||||
n.tenant_id,
|
||||
sc.id,
|
||||
n.user_id,
|
||||
'system',
|
||||
COALESCE(n.title, '') || CASE WHEN n.body IS NOT NULL THEN E'\n' || n.body ELSE '' END,
|
||||
'text',
|
||||
jsonb_build_object(
|
||||
'notification_type', n.type,
|
||||
'severity', 'info',
|
||||
'entity_ref', CASE WHEN n.entity_type IS NOT NULL THEN jsonb_build_object('entity_type', n.entity_type, 'entity_id', n.entity_id::text) ELSE NULL END,
|
||||
'migrated_from_notification', true,
|
||||
'original_notification_id', n.id::text
|
||||
),
|
||||
n.created_at,
|
||||
COALESCE(n.read_at, n.created_at)
|
||||
FROM notifications n
|
||||
JOIN comm_conversations sc ON sc.tenant_id = n.tenant_id AND sc.is_system = true AND sc.deleted_at IS NULL
|
||||
WHERE n.deleted_at IS NULL;
|
||||
""")
|
||||
# 3. Insert notifications as CommMessages in the system channel
|
||||
op.execute("""
|
||||
INSERT INTO comm_messages (id, tenant_id, conversation_id, sender_id, sender_type, content, content_format, metadata, created_at, updated_at)
|
||||
SELECT
|
||||
gen_random_uuid(),
|
||||
n.tenant_id,
|
||||
sc.id,
|
||||
n.user_id,
|
||||
'system',
|
||||
COALESCE(n.title, '') || CASE WHEN n.body IS NOT NULL THEN E'\n' || n.body ELSE '' END,
|
||||
'text',
|
||||
jsonb_build_object(
|
||||
'notification_type', n.type,
|
||||
'severity', 'info',
|
||||
'entity_ref', CASE WHEN n.entity_type IS NOT NULL THEN jsonb_build_object('entity_type', n.entity_type, 'entity_id', n.entity_id::text) ELSE NULL END,
|
||||
'migrated_from_notification', true,
|
||||
'original_notification_id', n.id::text
|
||||
),
|
||||
n.created_at,
|
||||
COALESCE(n.read_at, n.created_at)
|
||||
FROM notifications n
|
||||
JOIN comm_conversations sc ON sc.tenant_id = n.tenant_id AND sc.is_system = true AND sc.deleted_at IS NULL
|
||||
WHERE n.deleted_at IS NULL;
|
||||
""")
|
||||
|
||||
# 4. Insert text blocks for each migrated message
|
||||
op.execute("""
|
||||
INSERT INTO comm_message_blocks (id, tenant_id, message_id, block_type, block_data, sort_order)
|
||||
SELECT
|
||||
gen_random_uuid(),
|
||||
cm.tenant_id,
|
||||
cm.id,
|
||||
'text',
|
||||
jsonb_build_object('text', cm.content),
|
||||
0
|
||||
FROM comm_messages cm
|
||||
WHERE cm.metadata->>'migrated_from_notification' = 'true';
|
||||
""")
|
||||
# 4. Insert text blocks for each migrated message
|
||||
op.execute("""
|
||||
INSERT INTO comm_message_blocks (id, tenant_id, message_id, block_type, block_data, sort_order)
|
||||
SELECT
|
||||
gen_random_uuid(),
|
||||
cm.tenant_id,
|
||||
cm.id,
|
||||
'text',
|
||||
jsonb_build_object('text', cm.content),
|
||||
0
|
||||
FROM comm_messages cm
|
||||
WHERE cm.metadata->>'migrated_from_notification' = 'true';
|
||||
""")
|
||||
|
||||
# 5. Insert action_card blocks for messages with entity references
|
||||
op.execute("""
|
||||
INSERT INTO comm_message_blocks (id, tenant_id, message_id, block_type, block_data, sort_order)
|
||||
SELECT
|
||||
gen_random_uuid(),
|
||||
cm.tenant_id,
|
||||
cm.id,
|
||||
'action_card',
|
||||
jsonb_build_object(
|
||||
'label', 'Open',
|
||||
'entity_type', (cm.metadata->'entity_ref'->>'entity_type'),
|
||||
'entity_id', (cm.metadata->'entity_ref'->>'entity_id')
|
||||
),
|
||||
1
|
||||
FROM comm_messages cm
|
||||
WHERE cm.metadata->>'migrated_from_notification' = 'true'
|
||||
AND cm.metadata->'entity_ref' IS NOT NULL;
|
||||
""")
|
||||
# 5. Insert action_card blocks for messages with entity references
|
||||
op.execute("""
|
||||
INSERT INTO comm_message_blocks (id, tenant_id, message_id, block_type, block_data, sort_order)
|
||||
SELECT
|
||||
gen_random_uuid(),
|
||||
cm.tenant_id,
|
||||
cm.id,
|
||||
'action_card',
|
||||
jsonb_build_object(
|
||||
'label', 'Open',
|
||||
'entity_type', (cm.metadata->'entity_ref'->>'entity_type'),
|
||||
'entity_id', (cm.metadata->'entity_ref'->>'entity_id')
|
||||
),
|
||||
1
|
||||
FROM comm_messages cm
|
||||
WHERE cm.metadata->>'migrated_from_notification' = 'true'
|
||||
AND cm.metadata->'entity_ref' IS NOT NULL;
|
||||
""")
|
||||
|
||||
# 6. For read notifications, create CommMessageRead entries
|
||||
op.execute("""
|
||||
INSERT INTO comm_message_reads (id, tenant_id, conversation_id, user_id, last_read_msg_id, last_read_at)
|
||||
SELECT
|
||||
gen_random_uuid(),
|
||||
cm.tenant_id,
|
||||
cm.conversation_id,
|
||||
cm.sender_id,
|
||||
cm.id,
|
||||
COALESCE(n.read_at, n.created_at)
|
||||
FROM comm_messages cm
|
||||
JOIN notifications n ON n.id::text = cm.metadata->>'original_notification_id'
|
||||
WHERE cm.metadata->>'migrated_from_notification' = 'true'
|
||||
AND n.read_at IS NOT NULL
|
||||
AND n.deleted_at IS NULL;
|
||||
""")
|
||||
# 6. For read notifications, create CommMessageRead entries
|
||||
op.execute("""
|
||||
INSERT INTO comm_message_reads (id, tenant_id, conversation_id, user_id, last_read_msg_id, last_read_at)
|
||||
SELECT
|
||||
gen_random_uuid(),
|
||||
cm.tenant_id,
|
||||
cm.conversation_id,
|
||||
cm.sender_id,
|
||||
cm.id,
|
||||
COALESCE(n.read_at, n.created_at)
|
||||
FROM comm_messages cm
|
||||
JOIN notifications n ON n.id::text = cm.metadata->>'original_notification_id'
|
||||
WHERE cm.metadata->>'migrated_from_notification' = 'true'
|
||||
AND n.read_at IS NOT NULL
|
||||
AND n.deleted_at IS NULL;
|
||||
""")
|
||||
|
||||
# 7. Create legacy view over notifications table for backward compatibility
|
||||
# 7. Legacy view over the CORE notifications table — exists on both paths
|
||||
op.execute("DROP VIEW IF EXISTS notifications_legacy")
|
||||
op.execute("CREATE VIEW notifications_legacy AS SELECT * FROM notifications")
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
conn = op.get_bind()
|
||||
op.execute("DROP VIEW IF EXISTS notifications_legacy")
|
||||
if not _table_exists(conn, "comm_conversations"):
|
||||
return
|
||||
op.execute("DELETE FROM comm_message_blocks WHERE message_id IN (SELECT id FROM comm_messages WHERE metadata->>'migrated_from_notification' = 'true')")
|
||||
op.execute("DELETE FROM comm_messages WHERE metadata->>'migrated_from_notification' = 'true'")
|
||||
op.execute("DELETE FROM comm_conversations WHERE is_system = true AND title = 'System Channel'")
|
||||
|
||||
@@ -14,7 +14,24 @@ branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def _table_exists(conn, table_name: str) -> bool:
|
||||
"""True when the table exists (dual-path convergence, Gate B).
|
||||
|
||||
On a fresh install the automation plugin SQL migration has not run yet
|
||||
when Alembic reaches this revision — skip instead of failing. The
|
||||
plugin-side convergence migration creates the same table.
|
||||
"""
|
||||
row = conn.execute(
|
||||
sa.text("SELECT to_regclass(:tname) IS NOT NULL"),
|
||||
{"tname": f"public.{table_name}"},
|
||||
).scalar()
|
||||
return bool(row)
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
conn = op.get_bind()
|
||||
if not _table_exists(conn, "automation_agent_runs"):
|
||||
return
|
||||
op.create_table(
|
||||
"automation_agent_run_steps",
|
||||
sa.Column("id", PGUUID(as_uuid=True), primary_key=True),
|
||||
|
||||
@@ -18,7 +18,24 @@ branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def _table_exists(conn, table_name: str) -> bool:
|
||||
"""True when the table exists (dual-path convergence, Gate B).
|
||||
|
||||
On a fresh install the automation plugin SQL migration has not run yet
|
||||
when Alembic reaches this revision — skip instead of failing. The
|
||||
plugin-side convergence migration adds the same columns.
|
||||
"""
|
||||
row = conn.execute(
|
||||
sa.text("SELECT to_regclass(:tname) IS NOT NULL"),
|
||||
{"tname": f"public.{table_name}"},
|
||||
).scalar()
|
||||
return bool(row)
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
conn = op.get_bind()
|
||||
if not _table_exists(conn, "automation_agent_definitions"):
|
||||
return
|
||||
op.add_column(
|
||||
"automation_agent_definitions",
|
||||
sa.Column("temperature", sa.Float, nullable=False, server_default="0.3"),
|
||||
|
||||
@@ -20,7 +20,27 @@ branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def _table_exists(conn, table_name: str) -> bool:
|
||||
"""True when the table exists (dual-path convergence, Gate B).
|
||||
|
||||
On a fresh install the tasks plugin SQL migration has not run yet when
|
||||
Alembic reaches this revision — skip instead of failing. The plugin-side
|
||||
convergence migration adds the same columns/indexes. The legacy-data
|
||||
backfills below only matter for pre-existing rows and are correctly
|
||||
empty on a fresh install.
|
||||
"""
|
||||
row = conn.execute(
|
||||
sa.text("SELECT to_regclass(:tname) IS NOT NULL"),
|
||||
{"tname": f"public.{table_name}"},
|
||||
).scalar()
|
||||
return bool(row)
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
conn = op.get_bind()
|
||||
if not _table_exists(conn, "tasks"):
|
||||
return
|
||||
|
||||
# ── Add new columns to tasks ────────────────────────────────────────────
|
||||
op.add_column("tasks", sa.Column("assignee_type", sa.String(20), nullable=False, server_default="user"))
|
||||
op.add_column("tasks", sa.Column("assignee_id", PGUUID(as_uuid=True), nullable=True))
|
||||
|
||||
@@ -12,6 +12,7 @@ Revises: 0126
|
||||
"""
|
||||
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
revision = "0127"
|
||||
down_revision = "0126"
|
||||
@@ -19,7 +20,19 @@ branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def _table_exists(conn, table_name: str) -> bool:
|
||||
"""True when the table exists (dual-path convergence, Gate B)."""
|
||||
row = conn.execute(
|
||||
sa.text("SELECT to_regclass(:tname) IS NOT NULL"),
|
||||
{"tname": f"public.{table_name}"},
|
||||
).scalar()
|
||||
return bool(row)
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
conn = op.get_bind()
|
||||
if not _table_exists(conn, "tasks"):
|
||||
return
|
||||
# Drop the FK constraint on tasks.contact_id
|
||||
op.drop_constraint("tasks_contact_id_fkey", "tasks", type_="foreignkey")
|
||||
|
||||
|
||||
@@ -9,6 +9,7 @@ Revises: 0128
|
||||
"""
|
||||
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
revision = "0129"
|
||||
down_revision = "0128"
|
||||
@@ -27,8 +28,25 @@ TABLES_NEEDING_RLS = [
|
||||
]
|
||||
|
||||
|
||||
def _table_exists(conn, table_name: str) -> bool:
|
||||
"""True when the table exists (dual-path convergence, Gate B).
|
||||
|
||||
Plugin-owned tables may not exist yet on a fresh install when Alembic
|
||||
reaches this revision — skip them instead of failing. The plugin-side
|
||||
convergence migrations apply the same RLS policies.
|
||||
"""
|
||||
row = conn.execute(
|
||||
sa.text("SELECT to_regclass(:tname) IS NOT NULL"),
|
||||
{"tname": f"public.{table_name}"},
|
||||
).scalar()
|
||||
return bool(row)
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
conn = op.get_bind()
|
||||
for table in TABLES_NEEDING_RLS:
|
||||
if not _table_exists(conn, table):
|
||||
continue
|
||||
op.execute(f"ALTER TABLE {table} ENABLE ROW LEVEL SECURITY;")
|
||||
op.execute(
|
||||
f"CREATE POLICY tenant_isolation ON {table} "
|
||||
@@ -37,6 +55,9 @@ def upgrade() -> None:
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
conn = op.get_bind()
|
||||
for table in TABLES_NEEDING_RLS:
|
||||
if not _table_exists(conn, table):
|
||||
continue
|
||||
op.execute(f"DROP POLICY IF EXISTS tenant_isolation ON {table};")
|
||||
op.execute(f"ALTER TABLE {table} DISABLE ROW LEVEL SECURITY;")
|
||||
|
||||
@@ -10,6 +10,7 @@ Revises: 0135
|
||||
Create Date: 2026-08-21
|
||||
"""
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
revision = "0136"
|
||||
down_revision = "0135"
|
||||
@@ -29,8 +30,25 @@ TABLES_WITH_BAD_RLS = [
|
||||
]
|
||||
|
||||
|
||||
def _table_exists(conn, table_name: str) -> bool:
|
||||
"""True when the table exists (dual-path convergence, Gate B).
|
||||
|
||||
Plugin-owned tables may not exist yet on a fresh install when Alembic
|
||||
reaches this revision — skip them instead of failing. The plugin-side
|
||||
convergence migrations apply the same RLS policies.
|
||||
"""
|
||||
row = conn.execute(
|
||||
sa.text("SELECT to_regclass(:tname) IS NOT NULL"),
|
||||
{"tname": f"public.{table_name}"},
|
||||
).scalar()
|
||||
return bool(row)
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
conn = op.get_bind()
|
||||
for table in TABLES_WITH_BAD_RLS:
|
||||
if not _table_exists(conn, table):
|
||||
continue
|
||||
# Drop old policy with app.tenant_id
|
||||
op.execute(f"DROP POLICY IF EXISTS tenant_isolation ON {table};")
|
||||
# Create new policy with app.current_tenant_id
|
||||
@@ -41,7 +59,10 @@ def upgrade() -> None:
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
conn = op.get_bind()
|
||||
for table in TABLES_WITH_BAD_RLS:
|
||||
if not _table_exists(conn, table):
|
||||
continue
|
||||
op.execute(f"DROP POLICY IF EXISTS tenant_isolation ON {table};")
|
||||
op.execute(
|
||||
f"CREATE POLICY tenant_isolation ON {table} "
|
||||
|
||||
@@ -19,7 +19,25 @@ branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def _table_exists(conn, table_name: str) -> bool:
|
||||
"""True when the table exists (dual-path convergence, Gate B).
|
||||
|
||||
On a fresh install the tags plugin SQL migration has not run yet when
|
||||
Alembic reaches this revision — skip instead of failing. The plugin-side
|
||||
convergence migration adds the same columns.
|
||||
"""
|
||||
row = conn.execute(
|
||||
sa.text("SELECT to_regclass(:tname) IS NOT NULL"),
|
||||
{"tname": f"public.{table_name}"},
|
||||
).scalar()
|
||||
return bool(row)
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
conn = op.get_bind()
|
||||
if not _table_exists(conn, "tags"):
|
||||
return
|
||||
|
||||
# parent_id for tree structure (self-referencing FK)
|
||||
op.add_column("tags", sa.Column("parent_id", PGUUID(as_uuid=True), nullable=True))
|
||||
op.create_foreign_key(
|
||||
@@ -35,6 +53,9 @@ def upgrade() -> None:
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
conn = op.get_bind()
|
||||
if not _table_exists(conn, "tags"):
|
||||
return
|
||||
op.drop_column("tags", "icon")
|
||||
op.drop_column("tags", "applicable_to")
|
||||
op.drop_index("ix_tags_parent", table_name="tags")
|
||||
|
||||
@@ -18,11 +18,31 @@ branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def _table_exists(conn, table_name: str) -> bool:
|
||||
"""True when the table exists (dual-path convergence, Gate B).
|
||||
|
||||
On a fresh install the report_generator plugin SQL migration has not
|
||||
run yet when Alembic reaches this revision — skip instead of failing.
|
||||
The plugin-side convergence migration adds the same column.
|
||||
"""
|
||||
row = conn.execute(
|
||||
sa.text("SELECT to_regclass(:tname) IS NOT NULL"),
|
||||
{"tname": f"public.{table_name}"},
|
||||
).scalar()
|
||||
return bool(row)
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
conn = op.get_bind()
|
||||
if not _table_exists(conn, "report_templates"):
|
||||
return
|
||||
op.add_column("report_templates", sa.Column("folder_id", PGUUID(as_uuid=True), nullable=True))
|
||||
op.create_index("ix_report_templates_folder", "report_templates", ["folder_id"])
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
conn = op.get_bind()
|
||||
if not _table_exists(conn, "report_templates"):
|
||||
return
|
||||
op.drop_index("ix_report_templates_folder", table_name="report_templates")
|
||||
op.drop_column("report_templates", "folder_id")
|
||||
|
||||
@@ -18,11 +18,31 @@ branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def _table_exists(conn, table_name: str) -> bool:
|
||||
"""True when the table exists (dual-path convergence, Gate B).
|
||||
|
||||
On a fresh install the kommunikation plugin SQL migration has not run
|
||||
yet when Alembic reaches this revision — skip instead of failing.
|
||||
The plugin-side migration adds the same column idempotently.
|
||||
"""
|
||||
row = conn.execute(
|
||||
sa.text("SELECT to_regclass(:tname) IS NOT NULL"),
|
||||
{"tname": f"public.{table_name}"},
|
||||
).scalar()
|
||||
return bool(row)
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
conn = op.get_bind()
|
||||
if not _table_exists(conn, "comm_conversations"):
|
||||
return
|
||||
op.add_column("comm_conversations", sa.Column("folder_id", PGUUID(as_uuid=True), nullable=True))
|
||||
op.create_index("ix_comm_conversations_folder", "comm_conversations", ["folder_id"])
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
conn = op.get_bind()
|
||||
if not _table_exists(conn, "comm_conversations"):
|
||||
return
|
||||
op.drop_index("ix_comm_conversations_folder", table_name="comm_conversations")
|
||||
op.drop_column("comm_conversations", "folder_id")
|
||||
|
||||
Reference in New Issue
Block a user