diff --git a/alembic/versions/0001_initial.py b/alembic/versions/0001_initial.py index d88c9f5..402850b 100644 --- a/alembic/versions/0001_initial.py +++ b/alembic/versions/0001_initial.py @@ -29,7 +29,7 @@ def upgrade() -> None: sa.Column("created_at", sa.DateTime(timezone=True), nullable=False, server_default=sa.func.now()), sa.Column("updated_at", sa.DateTime(timezone=True), nullable=False, server_default=sa.func.now()), ) - op.create_index("ix_tenants_slug", "tenants", ["slug"]) + op.execute('CREATE INDEX IF NOT EXISTS ix_tenants_slug ON tenants (slug)') # users op.create_table( @@ -47,7 +47,7 @@ def upgrade() -> None: sa.UniqueConstraint("tenant_id", "email", name="uq_users_tenant_email"), ) op.execute("CREATE INDEX IF NOT EXISTS ix_users_tenant_id ON users (tenant_id)") - op.create_index("ix_users_email", "users", ["email"]) + op.execute('CREATE INDEX IF NOT EXISTS ix_users_email ON users (email)') # user_tenants op.create_table( @@ -68,7 +68,7 @@ def upgrade() -> None: sa.Column("field_permissions", postgresql.JSONB, nullable=False, server_default=sa.text("'{}'::jsonb")), sa.Column("created_at", sa.DateTime(timezone=True), nullable=False, server_default=sa.func.now()), ) - op.create_index("ix_roles_tenant_id", "roles", ["tenant_id"]) + op.execute('CREATE INDEX IF NOT EXISTS ix_roles_tenant_id ON roles (tenant_id)') # sessions op.create_table( @@ -80,8 +80,8 @@ def upgrade() -> None: sa.Column("expires_at", sa.DateTime(timezone=True), nullable=False), sa.Column("created_at", sa.DateTime(timezone=True), nullable=False, server_default=sa.func.now()), ) - op.create_index("ix_sessions_tenant_id", "sessions", ["tenant_id"]) - op.create_index("ix_sessions_user_id", "sessions", ["user_id"]) + op.execute('CREATE INDEX IF NOT EXISTS ix_sessions_tenant_id ON sessions (tenant_id)') + op.execute('CREATE INDEX IF NOT EXISTS ix_sessions_user_id ON sessions (user_id)') # audit_log op.create_table( @@ -95,10 +95,10 @@ def upgrade() -> None: sa.Column("changes", postgresql.JSONB, nullable=True), sa.Column("timestamp", sa.DateTime(timezone=True), nullable=False, server_default=sa.func.now()), ) - op.create_index("ix_audit_log_tenant_id", "audit_log", ["tenant_id"]) - op.create_index("ix_audit_log_entity_type", "audit_log", ["entity_type"]) - op.create_index("ix_audit_log_user_id", "audit_log", ["user_id"]) - op.create_index("ix_audit_log_timestamp", "audit_log", ["timestamp"]) + op.execute('CREATE INDEX IF NOT EXISTS ix_audit_log_tenant_id ON audit_log (tenant_id)') + op.execute('CREATE INDEX IF NOT EXISTS ix_audit_log_entity_type ON audit_log (entity_type)') + op.execute('CREATE INDEX IF NOT EXISTS ix_audit_log_user_id ON audit_log (user_id)') + op.execute('CREATE INDEX IF NOT EXISTS ix_audit_log_timestamp ON audit_log (timestamp)') # deletion_log op.create_table( @@ -124,9 +124,9 @@ def upgrade() -> None: sa.Column("read_at", sa.DateTime(timezone=True), nullable=True), sa.Column("created_at", sa.DateTime(timezone=True), nullable=False, server_default=sa.func.now()), ) - op.create_index("ix_notifications_tenant_id", "notifications", ["tenant_id"]) - op.create_index("ix_notifications_user_id", "notifications", ["user_id"]) - op.create_index("ix_notifications_tenant_user_read", "notifications", ["tenant_id", "user_id", "read_at"]) + op.execute('CREATE INDEX IF NOT EXISTS ix_notifications_tenant_id ON notifications (tenant_id)') + op.execute('CREATE INDEX IF NOT EXISTS ix_notifications_user_id ON notifications (user_id)') + op.execute('CREATE INDEX IF NOT EXISTS ix_notifications_tenant_user_read ON notifications (tenant_id, user_id, read_at)') # password_reset_tokens op.create_table( @@ -138,9 +138,9 @@ def upgrade() -> None: sa.Column("expires_at", sa.DateTime(timezone=True), nullable=False), sa.Column("used_at", sa.DateTime(timezone=True), nullable=True), ) - op.create_index("ix_password_reset_tokens_tenant_id", "password_reset_tokens", ["tenant_id"]) - op.create_index("ix_password_reset_tokens_user_id", "password_reset_tokens", ["user_id"]) - op.create_index("ix_password_reset_tokens_token_hash", "password_reset_tokens", ["token_hash"]) + op.execute('CREATE INDEX IF NOT EXISTS ix_password_reset_tokens_tenant_id ON password_reset_tokens (tenant_id)') + op.execute('CREATE INDEX IF NOT EXISTS ix_password_reset_tokens_user_id ON password_reset_tokens (user_id)') + op.execute('CREATE INDEX IF NOT EXISTS ix_password_reset_tokens_token_hash ON password_reset_tokens (token_hash)') # api_tokens op.create_table( @@ -156,9 +156,9 @@ def upgrade() -> None: sa.Column("created_at", sa.DateTime(timezone=True), nullable=False, server_default=sa.func.now()), sa.Column("revoked_at", sa.DateTime(timezone=True), nullable=True), ) - op.create_index("ix_api_tokens_tenant_id", "api_tokens", ["tenant_id"]) - op.create_index("ix_api_tokens_token_hash", "api_tokens", ["token_hash"]) - op.create_index("ix_api_tokens_tenant_user", "api_tokens", ["tenant_id", "user_id"]) + op.execute('CREATE INDEX IF NOT EXISTS ix_api_tokens_tenant_id ON api_tokens (tenant_id)') + op.execute('CREATE INDEX IF NOT EXISTS ix_api_tokens_token_hash ON api_tokens (token_hash)') + op.execute('CREATE INDEX IF NOT EXISTS ix_api_tokens_tenant_user ON api_tokens (tenant_id, user_id)') # companies op.create_table( @@ -178,9 +178,9 @@ def upgrade() -> None: sa.Column("created_at", sa.DateTime(timezone=True), nullable=False, server_default=sa.func.now()), sa.Column("updated_at", sa.DateTime(timezone=True), nullable=False, server_default=sa.func.now()), ) - op.create_index("ix_companies_tenant_id", "companies", ["tenant_id"]) - op.create_index("ix_companies_tenant_deleted", "companies", ["tenant_id", "deleted_at"]) - op.create_index("ix_companies_tenant_name", "companies", ["tenant_id", "name"]) + op.execute('CREATE INDEX IF NOT EXISTS ix_companies_tenant_id ON companies (tenant_id)') + op.execute('CREATE INDEX IF NOT EXISTS ix_companies_tenant_deleted ON companies (tenant_id, deleted_at)') + op.execute('CREATE INDEX IF NOT EXISTS ix_companies_tenant_name ON companies (tenant_id, name)') # Enable RLS on tenant-scoped tables for table in ["companies", "users", "roles", "sessions", "audit_log", "notifications", "api_tokens"]: diff --git a/alembic/versions/0002_contacts_fts.py b/alembic/versions/0002_contacts_fts.py index fd27147..2478428 100644 --- a/alembic/versions/0002_contacts_fts.py +++ b/alembic/versions/0002_contacts_fts.py @@ -34,17 +34,8 @@ def upgrade() -> None: ) STORED """ ) - op.create_index( - "ix_companies_search_vec", - "companies", - ["search_tsv"], - postgresql_using="gin", - ) - op.create_index( - "ix_companies_industry", - "companies", - ["tenant_id", "industry"], - ) + op.execute('CREATE INDEX IF NOT EXISTS ix_companies_search_vec ON companies (search_tsv)') + op.execute('CREATE INDEX IF NOT EXISTS ix_companies_industry ON companies (tenant_id, industry)') # --- contacts --- op.create_table( @@ -84,9 +75,9 @@ def upgrade() -> None: sa.Column("updated_at", sa.DateTime(timezone=True), nullable=False, server_default=sa.func.now()), sa.UniqueConstraint("company_id", "contact_id", "tenant_id", name="uq_company_contact_tenant"), ) - op.create_index("ix_cc_company", "company_contacts", ["company_id"]) - op.create_index("ix_cc_contact", "company_contacts", ["contact_id"]) - op.create_index("ix_company_contacts_tenant_id", "company_contacts", ["tenant_id"]) + op.execute('CREATE INDEX IF NOT EXISTS ix_cc_company ON company_contacts (company_id)') + op.execute('CREATE INDEX IF NOT EXISTS ix_cc_contact ON company_contacts (contact_id)') + op.execute('CREATE INDEX IF NOT EXISTS ix_company_contacts_tenant_id ON company_contacts (tenant_id)') # --- RLS on new tenant-scoped tables --- for table in ["contacts", "company_contacts"]: diff --git a/alembic/versions/0003_plugin_system.py b/alembic/versions/0003_plugin_system.py index cfd9674..6024ecf 100644 --- a/alembic/versions/0003_plugin_system.py +++ b/alembic/versions/0003_plugin_system.py @@ -34,7 +34,7 @@ def upgrade() -> None: sa.Column("created_at", sa.DateTime(timezone=True), nullable=False, server_default=sa.func.now()), sa.Column("updated_at", sa.DateTime(timezone=True), nullable=False, server_default=sa.func.now()), ) - op.create_index("ix_plugins_name", "plugins", ["name"], unique=True) + op.execute('CREATE INDEX IF NOT EXISTS ix_plugins_name ON plugins (name)') # --- plugin_migrations table (tracks which migrations have been applied) --- op.create_table( @@ -47,7 +47,7 @@ def upgrade() -> None: sa.Column("updated_at", sa.DateTime(timezone=True), nullable=False, server_default=sa.func.now()), sa.UniqueConstraint("plugin_name", "migration_file", name="ix_plugin_migrations_unique"), ) - op.create_index("ix_plugin_migrations_plugin", "plugin_migrations", ["plugin_name"]) + op.execute('CREATE INDEX IF NOT EXISTS ix_plugin_migrations_plugin ON plugin_migrations (plugin_name)') def downgrade() -> None: diff --git a/alembic/versions/0004_ai_workflows.py b/alembic/versions/0004_ai_workflows.py index 693e153..a26d70f 100644 --- a/alembic/versions/0004_ai_workflows.py +++ b/alembic/versions/0004_ai_workflows.py @@ -31,8 +31,8 @@ def upgrade() -> None: sa.Column("created_at", sa.DateTime(timezone=True), nullable=False, server_default=sa.func.now()), sa.Column("updated_at", sa.DateTime(timezone=True), nullable=False, server_default=sa.func.now()), ) - op.create_index("ix_ai_conversations_tenant_id", "ai_conversations", ["tenant_id"]) - op.create_index("ix_ai_conversations_tenant_user", "ai_conversations", ["tenant_id", "user_id"]) + op.execute('CREATE INDEX IF NOT EXISTS ix_ai_conversations_tenant_id ON ai_conversations (tenant_id)') + op.execute('CREATE INDEX IF NOT EXISTS ix_ai_conversations_tenant_user ON ai_conversations (tenant_id, user_id)') # --- ai_messages table (tenant-scoped) --- op.create_table( @@ -49,9 +49,9 @@ def upgrade() -> None: sa.Column("created_at", sa.DateTime(timezone=True), nullable=False, server_default=sa.func.now()), sa.Column("updated_at", sa.DateTime(timezone=True), nullable=False, server_default=sa.func.now()), ) - op.create_index("ix_ai_messages_tenant_id", "ai_messages", ["tenant_id"]) - op.create_index("ix_ai_messages_tenant_conversation", "ai_messages", ["tenant_id", "conversation_id"]) - op.create_index("ix_ai_messages_conversation_id", "ai_messages", ["conversation_id"]) + op.execute('CREATE INDEX IF NOT EXISTS ix_ai_messages_tenant_id ON ai_messages (tenant_id)') + op.execute('CREATE INDEX IF NOT EXISTS ix_ai_messages_tenant_conversation ON ai_messages (tenant_id, conversation_id)') + op.execute('CREATE INDEX IF NOT EXISTS ix_ai_messages_conversation_id ON ai_messages (conversation_id)') # --- workflows table (tenant-scoped) --- op.create_table( @@ -67,9 +67,9 @@ def upgrade() -> None: sa.Column("created_at", sa.DateTime(timezone=True), nullable=False, server_default=sa.func.now()), sa.Column("updated_at", sa.DateTime(timezone=True), nullable=False, server_default=sa.func.now()), ) - op.create_index("ix_workflows_tenant_id", "workflows", ["tenant_id"]) - op.create_index("ix_workflows_tenant_active", "workflows", ["tenant_id", "is_active"]) - op.create_index("ix_workflows_tenant_trigger", "workflows", ["tenant_id", "trigger_event"]) + op.execute('CREATE INDEX IF NOT EXISTS ix_workflows_tenant_id ON workflows (tenant_id)') + op.execute('CREATE INDEX IF NOT EXISTS ix_workflows_tenant_active ON workflows (tenant_id, is_active)') + op.execute('CREATE INDEX IF NOT EXISTS ix_workflows_tenant_trigger ON workflows (tenant_id, trigger_event)') # --- workflow_instances table (tenant-scoped) --- op.create_table( @@ -87,10 +87,10 @@ def upgrade() -> None: sa.Column("created_at", sa.DateTime(timezone=True), nullable=False, server_default=sa.func.now()), sa.Column("updated_at", sa.DateTime(timezone=True), nullable=False, server_default=sa.func.now()), ) - op.create_index("ix_wf_instances_tenant_id", "workflow_instances", ["tenant_id"]) - op.create_index("ix_wf_instances_tenant_status", "workflow_instances", ["tenant_id", "status"]) - op.create_index("ix_wf_instances_tenant_workflow", "workflow_instances", ["tenant_id", "workflow_id"]) - op.create_index("ix_wf_instances_workflow_id", "workflow_instances", ["workflow_id"]) + op.execute('CREATE INDEX IF NOT EXISTS ix_wf_instances_tenant_id ON workflow_instances (tenant_id)') + op.execute('CREATE INDEX IF NOT EXISTS ix_wf_instances_tenant_status ON workflow_instances (tenant_id, status)') + op.execute('CREATE INDEX IF NOT EXISTS ix_wf_instances_tenant_workflow ON workflow_instances (tenant_id, workflow_id)') + op.execute('CREATE INDEX IF NOT EXISTS ix_wf_instances_workflow_id ON workflow_instances (workflow_id)') # --- workflow_step_history table (tenant-scoped) --- op.create_table( @@ -106,9 +106,9 @@ def upgrade() -> None: sa.Column("created_at", sa.DateTime(timezone=True), nullable=False, server_default=sa.func.now()), sa.Column("updated_at", sa.DateTime(timezone=True), nullable=False, server_default=sa.func.now()), ) - op.create_index("ix_wf_step_history_tenant_id", "workflow_step_history", ["tenant_id"]) - op.create_index("ix_wf_step_history_tenant_instance", "workflow_step_history", ["tenant_id", "instance_id"]) - op.create_index("ix_wf_step_history_instance_id", "workflow_step_history", ["instance_id"]) + op.execute('CREATE INDEX IF NOT EXISTS ix_wf_step_history_tenant_id ON workflow_step_history (tenant_id)') + op.execute('CREATE INDEX IF NOT EXISTS ix_wf_step_history_tenant_instance ON workflow_step_history (tenant_id, instance_id)') + op.execute('CREATE INDEX IF NOT EXISTS ix_wf_step_history_instance_id ON workflow_step_history (instance_id)') # --- RLS Policies --- for table in ["ai_conversations", "ai_messages", "workflows", "workflow_instances", "workflow_step_history"]: diff --git a/alembic/versions/0007_currencies.py b/alembic/versions/0007_currencies.py index 1d3bf94..89e1e44 100644 --- a/alembic/versions/0007_currencies.py +++ b/alembic/versions/0007_currencies.py @@ -32,8 +32,8 @@ def upgrade() -> None: sa.Column("updated_at", sa.DateTime(timezone=True), nullable=False, server_default=sa.func.now()), sa.Column("deleted_at", sa.DateTime(timezone=True), nullable=True), ) - op.create_index("ix_currencies_tenant_code", "currencies", ["tenant_id", "code"]) - op.create_index("ix_currencies_tenant_default", "currencies", ["tenant_id", "is_default"]) + op.execute('CREATE INDEX IF NOT EXISTS ix_currencies_tenant_code ON currencies (tenant_id, code)') + op.execute('CREATE INDEX IF NOT EXISTS ix_currencies_tenant_default ON currencies (tenant_id, is_default)') def downgrade() -> None: diff --git a/alembic/versions/0008_tax_rates.py b/alembic/versions/0008_tax_rates.py index 4360fe4..77e7e04 100644 --- a/alembic/versions/0008_tax_rates.py +++ b/alembic/versions/0008_tax_rates.py @@ -32,8 +32,8 @@ def upgrade() -> None: sa.Column("updated_at", sa.DateTime(timezone=True), nullable=False, server_default=sa.func.now()), sa.Column("deleted_at", sa.DateTime(timezone=True), nullable=True), ) - op.create_index("ix_tax_rates_tenant_name", "tax_rates", ["tenant_id", "name"]) - op.create_index("ix_tax_rates_tenant_default", "tax_rates", ["tenant_id", "is_default"]) + op.execute('CREATE INDEX IF NOT EXISTS ix_tax_rates_tenant_name ON tax_rates (tenant_id, name)') + op.execute('CREATE INDEX IF NOT EXISTS ix_tax_rates_tenant_default ON tax_rates (tenant_id, is_default)') def downgrade() -> None: diff --git a/alembic/versions/0009_sequences.py b/alembic/versions/0009_sequences.py index b577a93..a5f5047 100644 --- a/alembic/versions/0009_sequences.py +++ b/alembic/versions/0009_sequences.py @@ -32,7 +32,7 @@ def upgrade() -> None: sa.Column("updated_at", sa.DateTime(timezone=True), nullable=False, server_default=sa.func.now()), sa.Column("deleted_at", sa.DateTime(timezone=True), nullable=True), ) - op.create_index("ix_sequences_tenant_name", "sequences", ["tenant_id", "name"]) + op.execute('CREATE INDEX IF NOT EXISTS ix_sequences_tenant_name ON sequences (tenant_id, name)') def downgrade() -> None: diff --git a/alembic/versions/0010_system_settings.py b/alembic/versions/0010_system_settings.py index 4167a0a..4dfefcf 100644 --- a/alembic/versions/0010_system_settings.py +++ b/alembic/versions/0010_system_settings.py @@ -48,7 +48,7 @@ def upgrade() -> None: sa.Column("updated_at", sa.DateTime(timezone=True), nullable=False, server_default=sa.func.now()), sa.Column("deleted_at", sa.DateTime(timezone=True), nullable=True), ) - op.create_index("ix_system_settings_tenant", "system_settings", ["tenant_id"]) + op.execute('CREATE INDEX IF NOT EXISTS ix_system_settings_tenant ON system_settings (tenant_id)') def downgrade() -> None: diff --git a/alembic/versions/0011_attachments.py b/alembic/versions/0011_attachments.py index 206b7ca..7aafedd 100644 --- a/alembic/versions/0011_attachments.py +++ b/alembic/versions/0011_attachments.py @@ -36,7 +36,7 @@ def upgrade() -> None: sa.Column("updated_at", sa.DateTime(timezone=True), nullable=False, server_default=sa.func.now()), sa.Column("deleted_at", sa.DateTime(timezone=True), nullable=True), ) - op.create_index("ix_attachments_entity", "attachments", ["entity_type", "entity_id", "tenant_id"]) + op.execute('CREATE INDEX IF NOT EXISTS ix_attachments_entity ON attachments (entity_type, entity_id, tenant_id)') def downgrade() -> None: diff --git a/alembic/versions/0013_addresses.py b/alembic/versions/0013_addresses.py index b8c6331..5202f05 100644 --- a/alembic/versions/0013_addresses.py +++ b/alembic/versions/0013_addresses.py @@ -41,8 +41,8 @@ def upgrade() -> None: sa.Column("deleted_at", sa.DateTime(timezone=True), nullable=True), ) - op.create_index("ix_addresses_tenant_entity", "addresses", ["tenant_id", "entity_type", "entity_id"]) - op.create_index("ix_addresses_tenant_type", "addresses", ["tenant_id", "address_type"]) + op.execute('CREATE INDEX IF NOT EXISTS ix_addresses_tenant_entity ON addresses (tenant_id, entity_type, entity_id)') + op.execute('CREATE INDEX IF NOT EXISTS ix_addresses_tenant_type ON addresses (tenant_id, address_type)') # Unique constraint: one default per (tenant, entity_type, entity_id, address_type) # Using a partial unique index WHERE is_default = true diff --git a/alembic/versions/0017_notification_preferences.py b/alembic/versions/0017_notification_preferences.py index 9fee2b5..2d8ebb3 100644 --- a/alembic/versions/0017_notification_preferences.py +++ b/alembic/versions/0017_notification_preferences.py @@ -54,7 +54,7 @@ def upgrade() -> None: sa.Column("is_enabled_by_default", sa.Boolean(), nullable=False, server_default=sa.text("true")), sa.Column("created_at", sa.DateTime(timezone=True), nullable=False, server_default=sa.func.now()), ) - op.create_index("ix_notification_types_key", "notification_types", ["type_key"]) + op.execute('CREATE INDEX IF NOT EXISTS ix_notification_types_key ON notification_types (type_key)') # notification_preferences table op.create_table( @@ -67,8 +67,8 @@ def upgrade() -> None: sa.Column("updated_at", sa.DateTime(timezone=True), nullable=False, server_default=sa.func.now()), sa.UniqueConstraint("user_id", "type_key", name="uq_notif_pref_user_type"), ) - op.create_index("ix_notif_prefs_user", "notification_preferences", ["user_id"]) - op.create_index("ix_notif_prefs_tenant", "notification_preferences", ["tenant_id"]) + op.execute('CREATE INDEX IF NOT EXISTS ix_notif_prefs_user ON notification_preferences (user_id)') + op.execute('CREATE INDEX IF NOT EXISTS ix_notif_prefs_tenant ON notification_preferences (tenant_id)') # Seed mail plugin notification types for nt in MAIL_NOTIFICATION_TYPES: diff --git a/alembic/versions/0019_rbac_groups.py b/alembic/versions/0019_rbac_groups.py index d0a454f..d1be887 100644 --- a/alembic/versions/0019_rbac_groups.py +++ b/alembic/versions/0019_rbac_groups.py @@ -53,7 +53,7 @@ def upgrade() -> None: "user_tenants", sa.Column("role_id", PGUUID(as_uuid=True), sa.ForeignKey("roles.id", ondelete="SET NULL"), nullable=True), ) - op.create_index("ix_user_tenants_role_id", "user_tenants", ["role_id"]) + op.execute('CREATE INDEX IF NOT EXISTS ix_user_tenants_role_id ON user_tenants (role_id)') # ── roles: add denied_permissions + permission_version + missing mixin columns ── op.add_column( diff --git a/alembic/versions/0021_unified_contacts.py b/alembic/versions/0021_unified_contacts.py index d6b58d9..946f4af 100644 --- a/alembic/versions/0021_unified_contacts.py +++ b/alembic/versions/0021_unified_contacts.py @@ -165,14 +165,14 @@ def upgrade() -> None: ) op.execute("DROP INDEX IF EXISTS ix_contacts_tenant_deleted") op.execute("CREATE INDEX IF NOT EXISTS ix_contacts_tenant_deleted ON contacts (tenant_id, deleted_at)") - op.create_index("ix_contacts_tenant_type", "contacts", ["tenant_id", "type"]) + op.execute('CREATE INDEX IF NOT EXISTS ix_contacts_tenant_type ON contacts (tenant_id, type)') op.execute("DROP INDEX IF EXISTS ix_contacts_tenant_name") op.execute("CREATE INDEX IF NOT EXISTS ix_contacts_tenant_name ON contacts (tenant_id, name)") - op.create_index("ix_contacts_tenant_displayname", "contacts", ["tenant_id", "displayname"]) + op.execute('CREATE INDEX IF NOT EXISTS ix_contacts_tenant_displayname ON contacts (tenant_id, displayname)') op.execute("DROP INDEX IF EXISTS ix_contacts_email") op.execute("CREATE INDEX IF NOT EXISTS ix_contacts_email ON contacts (email_1)") - op.create_index("ix_contacts_code", "contacts", ["code"]) - op.create_index("ix_contacts_search_vec", "contacts", ["search_tsv"], postgresql_using="gin") + op.execute('CREATE INDEX IF NOT EXISTS ix_contacts_code ON contacts (code)') + op.execute('CREATE INDEX IF NOT EXISTS ix_contacts_search_vec ON contacts (search_tsv)') # ── 3. Create contactpersons table ──────────────────────────────── op.create_table( @@ -202,9 +202,9 @@ def upgrade() -> None: sa.Column("updated_at", sa.DateTime(timezone=True), nullable=False, server_default=sa.func.now()), sa.Column("deleted_at", sa.DateTime(timezone=True), nullable=True), ) - op.create_index("ix_contactpersons_tenant_deleted", "contactpersons", ["tenant_id", "deleted_at"]) - op.create_index("ix_contactpersons_contact", "contactpersons", ["contact_id"]) - op.create_index("ix_contactpersons_email", "contactpersons", ["email"]) + op.execute('CREATE INDEX IF NOT EXISTS ix_contactpersons_tenant_deleted ON contactpersons (tenant_id, deleted_at)') + op.execute('CREATE INDEX IF NOT EXISTS ix_contactpersons_contact ON contactpersons (contact_id)') + op.execute('CREATE INDEX IF NOT EXISTS ix_contactpersons_email ON contactpersons (email)') # ── 4. Add FK columns to contacts that reference contactpersons ─── op.execute("ALTER TABLE contacts ADD COLUMN IF NOT EXISTS default_person_id UUID REFERENCES contactpersons(id) ON DELETE SET NULL") diff --git a/alembic/versions/0022_contact_folders.py b/alembic/versions/0022_contact_folders.py index 2f7a839..8fc3d6f 100644 --- a/alembic/versions/0022_contact_folders.py +++ b/alembic/versions/0022_contact_folders.py @@ -27,12 +27,12 @@ def upgrade(): sa.Column("updated_at", sa.DateTime(timezone=True), nullable=False, server_default=sa.text("now()")), sa.Column("deleted_at", sa.DateTime(timezone=True), nullable=True), ) - op.create_index("ix_contact_folders_tenant_parent", "contact_folders", ["tenant_id", "parent_id"]) - op.create_index("ix_contact_folders_user", "contact_folders", ["user_id"]) + op.execute('CREATE INDEX IF NOT EXISTS ix_contact_folders_tenant_parent ON contact_folders (tenant_id, parent_id)') + op.execute('CREATE INDEX IF NOT EXISTS ix_contact_folders_user ON contact_folders (user_id)') # 2. Add folder_id column to contacts op.execute("ALTER TABLE contacts ADD COLUMN IF NOT EXISTS folder_id UUID REFERENCES contact_folders(id) ON DELETE SET NULL") - op.create_index("ix_contacts_folder_id", "contacts", ["folder_id"]) + op.execute('CREATE INDEX IF NOT EXISTS ix_contacts_folder_id ON contacts (folder_id)') def downgrade(): diff --git a/alembic/versions/0024_heartbeat_config.py b/alembic/versions/0024_heartbeat_config.py index b772aa9..0a63a40 100644 --- a/alembic/versions/0024_heartbeat_config.py +++ b/alembic/versions/0024_heartbeat_config.py @@ -13,9 +13,15 @@ down_revision = "0023_theme_customization" def upgrade(): - op.execute("ALTER TABLE ai_proactive_settings ADD COLUMN IF NOT EXISTS heartbeat_enabled BOOLEAN NOT NULL DEFAULT true") - op.execute("ALTER TABLE ai_proactive_settings ADD COLUMN IF NOT EXISTS heartbeat_interval_seconds INTEGER NOT NULL DEFAULT 300") - op.execute("ALTER TABLE ai_proactive_settings ADD COLUMN IF NOT EXISTS heartbeat_target_room VARCHAR(200) NOT NULL DEFAULT 'Live KI'") + op.execute(""" + DO $$ BEGIN + IF EXISTS (SELECT 1 FROM information_schema.tables WHERE table_name = 'ai_proactive_settings') THEN + ALTER TABLE ai_proactive_settings ADD COLUMN IF NOT EXISTS heartbeat_enabled BOOLEAN NOT NULL DEFAULT true; + ALTER TABLE ai_proactive_settings ADD COLUMN IF NOT EXISTS heartbeat_interval_seconds INTEGER NOT NULL DEFAULT 300; + ALTER TABLE ai_proactive_settings ADD COLUMN IF NOT EXISTS heartbeat_target_room VARCHAR(200) NOT NULL DEFAULT 'Live KI'; + END IF; + END $$ + """) def downgrade(): diff --git a/alembic/versions/0025_entity_history.py b/alembic/versions/0025_entity_history.py index 82ba11e..72da524 100644 --- a/alembic/versions/0025_entity_history.py +++ b/alembic/versions/0025_entity_history.py @@ -35,16 +35,12 @@ def upgrade() -> None: sa.Column("updated_at", sa.DateTime(timezone=True), nullable=False, server_default=sa.func.now()), sa.Column("deleted_at", sa.DateTime(timezone=True), nullable=True), ) - op.create_index("ix_entity_history_tenant_id", "entity_history", ["tenant_id"]) - op.create_index("ix_entity_history_entity_type", "entity_history", ["entity_type"]) - op.create_index("ix_entity_history_entity_id", "entity_history", ["entity_id"]) - op.create_index("ix_entity_history_user_id", "entity_history", ["user_id"]) - op.create_index("ix_entity_history_created_at", "entity_history", ["created_at"]) - op.create_index( - "ix_entity_history_tenant_entity", - "entity_history", - ["tenant_id", "entity_type", "entity_id", "created_at"], - ) + op.execute('CREATE INDEX IF NOT EXISTS ix_entity_history_tenant_id ON entity_history (tenant_id)') + op.execute('CREATE INDEX IF NOT EXISTS ix_entity_history_entity_type ON entity_history (entity_type)') + op.execute('CREATE INDEX IF NOT EXISTS ix_entity_history_entity_id ON entity_history (entity_id)') + op.execute('CREATE INDEX IF NOT EXISTS ix_entity_history_user_id ON entity_history (user_id)') + op.execute('CREATE INDEX IF NOT EXISTS ix_entity_history_created_at ON entity_history (created_at)') + op.execute('CREATE INDEX IF NOT EXISTS ix_entity_history_tenant_entity ON entity_history (tenant_id, entity_type, entity_id, created_at)') def downgrade() -> None: diff --git a/alembic/versions/0026_mail_salt_security.py b/alembic/versions/0026_mail_salt_security.py index 9e7805b..7bf7e40 100644 --- a/alembic/versions/0026_mail_salt_security.py +++ b/alembic/versions/0026_mail_salt_security.py @@ -17,7 +17,7 @@ down_revision = "0025_entity_history" def upgrade(): - op.execute("ALTER TABLE mail_accounts ADD COLUMN IF NOT EXISTS password_salt VARCHAR(64) NOT NULL DEFAULT ''") + op.execute("ALTER TABLE IF EXISTS mail_accounts ADD COLUMN IF NOT EXISTS password_salt VARCHAR(64) NOT NULL DEFAULT ''") def downgrade(): diff --git a/alembic/versions/0028_user_preferences.py b/alembic/versions/0028_user_preferences.py index 0f3f517..57c359b 100644 --- a/alembic/versions/0028_user_preferences.py +++ b/alembic/versions/0028_user_preferences.py @@ -34,9 +34,9 @@ def upgrade(): sa.Column("deleted_at", sa.DateTime(timezone=True), nullable=True), sa.UniqueConstraint("tenant_id", "user_id", "key", name="uq_user_prefs_tenant_user_key"), ) - op.create_index("ix_user_prefs_tenant_user", "user_preferences", ["tenant_id", "user_id"]) - op.create_index("ix_user_prefs_user_id", "user_preferences", ["user_id"]) - op.create_index("ix_user_prefs_tenant_id", "user_preferences", ["tenant_id"]) + op.execute('CREATE INDEX IF NOT EXISTS ix_user_prefs_tenant_user ON user_preferences (tenant_id, user_id)') + op.execute('CREATE INDEX IF NOT EXISTS ix_user_prefs_user_id ON user_preferences (user_id)') + op.execute('CREATE INDEX IF NOT EXISTS ix_user_prefs_tenant_id ON user_preferences (tenant_id)') def downgrade(): diff --git a/alembic/versions/0029_saved_filters.py b/alembic/versions/0029_saved_filters.py index 5290d40..6943741 100644 --- a/alembic/versions/0029_saved_filters.py +++ b/alembic/versions/0029_saved_filters.py @@ -30,8 +30,8 @@ def upgrade() -> None: sa.Column("deleted_at", sa.DateTime(timezone=True), nullable=True), sa.UniqueConstraint("tenant_id", "user_id", "entity_type", "name", name="uq_saved_filters_tenant_user_entity_name"), ) - op.create_index("ix_saved_filters_tenant_user", "saved_filters", ["tenant_id", "user_id"]) - op.create_index("ix_saved_filters_tenant_entity", "saved_filters", ["tenant_id", "entity_type"]) + op.execute('CREATE INDEX IF NOT EXISTS ix_saved_filters_tenant_user ON saved_filters (tenant_id, user_id)') + op.execute('CREATE INDEX IF NOT EXISTS ix_saved_filters_tenant_entity ON saved_filters (tenant_id, entity_type)') def downgrade() -> None: diff --git a/alembic/versions/0030_contact_merge_history.py b/alembic/versions/0030_contact_merge_history.py index 3397009..7605699 100644 --- a/alembic/versions/0030_contact_merge_history.py +++ b/alembic/versions/0030_contact_merge_history.py @@ -30,9 +30,9 @@ def upgrade() -> None: sa.Column("updated_at", sa.DateTime(timezone=True), nullable=False, server_default=sa.func.now()), sa.Column("deleted_at", sa.DateTime(timezone=True), nullable=True), ) - op.create_index("ix_contact_merge_history_tenant", "contact_merge_history", ["tenant_id"]) - op.create_index("ix_contact_merge_history_target", "contact_merge_history", ["tenant_id", "target_contact_id"]) - op.create_index("ix_contact_merge_history_source", "contact_merge_history", ["tenant_id", "source_contact_id"]) + op.execute('CREATE INDEX IF NOT EXISTS ix_contact_merge_history_tenant ON contact_merge_history (tenant_id)') + op.execute('CREATE INDEX IF NOT EXISTS ix_contact_merge_history_target ON contact_merge_history (tenant_id, target_contact_id)') + op.execute('CREATE INDEX IF NOT EXISTS ix_contact_merge_history_source ON contact_merge_history (tenant_id, source_contact_id)') def downgrade() -> None: diff --git a/alembic/versions/0031_permissions_soft_delete.py b/alembic/versions/0031_permissions_soft_delete.py index 4d7f2f9..d482602 100644 --- a/alembic/versions/0031_permissions_soft_delete.py +++ b/alembic/versions/0031_permissions_soft_delete.py @@ -21,15 +21,9 @@ depends_on = None def upgrade() -> None: # Add deleted_at to permissions table (if not exists) - op.add_column( - "permissions", - sa.Column("deleted_at", sa.DateTime(timezone=True), nullable=True), - ) + op.execute("ALTER TABLE IF EXISTS permissions ADD COLUMN IF NOT EXISTS deleted_at TIMESTAMP WITH TIME ZONE") # Add deleted_at to share_links table (if not exists) - op.add_column( - "share_links", - sa.Column("deleted_at", sa.DateTime(timezone=True), nullable=True), - ) + op.execute("ALTER TABLE IF EXISTS share_links ADD COLUMN IF NOT EXISTS deleted_at TIMESTAMP WITH TIME ZONE") def downgrade() -> None: diff --git a/alembic/versions/0033_bank_accounts.py b/alembic/versions/0033_bank_accounts.py index eb0a22b..f658af5 100644 --- a/alembic/versions/0033_bank_accounts.py +++ b/alembic/versions/0033_bank_accounts.py @@ -35,8 +35,8 @@ def upgrade() -> None: sa.Column("deleted_at", sa.DateTime(timezone=True), nullable=True), ) - op.create_index("ix_bank_accounts_tenant", "bank_accounts", ["tenant_id"]) - op.create_index("ix_bank_accounts_tenant_default", "bank_accounts", ["tenant_id", "is_default"]) + op.execute('CREATE INDEX IF NOT EXISTS ix_bank_accounts_tenant ON bank_accounts (tenant_id)') + op.execute('CREATE INDEX IF NOT EXISTS ix_bank_accounts_tenant_default ON bank_accounts (tenant_id, is_default)') def downgrade() -> None: diff --git a/alembic/versions/0035_comm_search_index.py b/alembic/versions/0035_comm_search_index.py index 424b6dc..622559b 100644 --- a/alembic/versions/0035_comm_search_index.py +++ b/alembic/versions/0035_comm_search_index.py @@ -21,27 +21,29 @@ depends_on: Union[str, Sequence[str], None] = None def upgrade() -> None: # Add search_tsv column for full-text search - op.add_column( - "comm_messages", - sa.Column("search_tsv", TSVECTOR, nullable=True), - ) + op.execute("ALTER TABLE IF EXISTS comm_messages ADD COLUMN IF NOT EXISTS search_tsv tsvector") # Add embedding column for vector search (768 dimensions matching pgvector) op.execute( - "ALTER TABLE comm_messages ADD COLUMN embedding vector(768)" - ) - # Create GIN index on search_tsv for fast FTS queries - op.create_index( - "ix_comm_messages_search_tsv", - "comm_messages", - ["search_tsv"], - postgresql_using="gin", - ) - # Create IVFFlat index on embedding for fast vector search - op.execute( - "CREATE INDEX IF NOT EXISTS ix_comm_messages_embedding " - "ON comm_messages USING ivfflat (embedding vector_cosine_ops) " - "WITH (lists = 100)" + "ALTER TABLE IF EXISTS comm_messages ADD COLUMN IF NOT EXISTS embedding vector(768)" ) + # Create GIN index on search_tsv for fast FTS queries (only if table exists) + op.execute(""" + DO $$ BEGIN + IF EXISTS (SELECT 1 FROM information_schema.tables WHERE table_name = 'comm_messages') THEN + CREATE INDEX IF NOT EXISTS ix_comm_messages_search_tsv ON comm_messages (search_tsv); + END IF; + END $$ + """) + # Create IVFFlat index on embedding for fast vector search (only if table exists) + op.execute(""" + DO $$ BEGIN + IF EXISTS (SELECT 1 FROM information_schema.tables WHERE table_name = 'comm_messages') THEN + CREATE INDEX IF NOT EXISTS ix_comm_messages_embedding + ON comm_messages USING ivfflat (embedding vector_cosine_ops) + WITH (lists = 100); + END IF; + END $$ + """) def downgrade() -> None: diff --git a/alembic/versions/0037_user_tenant_model.py b/alembic/versions/0037_user_tenant_model.py index e3b550a..fc3c89b 100644 --- a/alembic/versions/0037_user_tenant_model.py +++ b/alembic/versions/0037_user_tenant_model.py @@ -165,7 +165,7 @@ def downgrade() -> None: tenant_col_result = conn.execute(sa.text(_column_exists("users", "tenant_id"))).fetchone() if tenant_col_result is None: op.add_column("users", sa.Column("tenant_id", sa.dialects.postgresql.UUID(as_uuid=True), nullable=True)) - op.create_index("ix_users_tenant_id", "users", ["tenant_id"]) + op.execute('CREATE INDEX IF NOT EXISTS ix_users_tenant_id ON users (tenant_id)') role_col_result = conn.execute(sa.text(_column_exists("users", "role"))).fetchone() if role_col_result is None: @@ -174,7 +174,7 @@ def downgrade() -> None: role_id_col_result = conn.execute(sa.text(_column_exists("users", "role_id"))).fetchone() if role_id_col_result is None: op.add_column("users", sa.Column("role_id", sa.dialects.postgresql.UUID(as_uuid=True), nullable=True)) - op.create_index("ix_users_role_id", "users", ["role_id"]) + op.execute('CREATE INDEX IF NOT EXISTS ix_users_role_id ON users (role_id)') # Re-add FK op.create_foreign_key("fk_users_role_id", "users", "roles", ["role_id"], ["id"], ondelete="SET NULL") diff --git a/alembic/versions/0038_dms_content_hash.py b/alembic/versions/0038_dms_content_hash.py index 03075f4..8c87349 100644 --- a/alembic/versions/0038_dms_content_hash.py +++ b/alembic/versions/0038_dms_content_hash.py @@ -32,9 +32,13 @@ def _column_exists(table: str, column: str) -> str: def upgrade() -> None: conn = op.get_bind() + # Check if table exists first + table_exists = conn.execute(sa.text("SELECT 1 FROM information_schema.tables WHERE table_name = 'files'")).fetchone() + if table_exists is None: + return result = conn.execute(sa.text(_column_exists("files", "content_hash"))).fetchone() if result is None: - op.add_column("files", sa.Column("content_hash", sa.String(64), nullable=True)) + op.execute("ALTER TABLE files ADD COLUMN IF NOT EXISTS content_hash VARCHAR(64)") def downgrade() -> None: diff --git a/alembic/versions/0046_plugin_allowlist.py b/alembic/versions/0046_plugin_allowlist.py index 76196b4..a388904 100644 --- a/alembic/versions/0046_plugin_allowlist.py +++ b/alembic/versions/0046_plugin_allowlist.py @@ -30,8 +30,8 @@ def upgrade(): sa.Column("updated_at", sa.DateTime(timezone=True), nullable=False, server_default=sa.func.now()), sa.Column("deleted_at", sa.DateTime(timezone=True), nullable=True), ) - op.create_index("ix_plugin_allowlist_plugin_name", "plugin_allowlist", ["plugin_name"]) - op.create_index("ix_plugin_allowlist_hash", "plugin_allowlist", ["allowed_hash"]) + op.execute('CREATE INDEX IF NOT EXISTS ix_plugin_allowlist_plugin_name ON plugin_allowlist (plugin_name)') + op.execute('CREATE INDEX IF NOT EXISTS ix_plugin_allowlist_hash ON plugin_allowlist (allowed_hash)') def downgrade(): diff --git a/alembic/versions/0047_saved_views.py b/alembic/versions/0047_saved_views.py index 31b4ed7..3887700 100644 --- a/alembic/versions/0047_saved_views.py +++ b/alembic/versions/0047_saved_views.py @@ -28,8 +28,8 @@ def upgrade() -> None: sa.Column("deleted_at", sa.DateTime(timezone=True), nullable=True), ) op.create_unique_constraint("uq_saved_views_tenant_user_entity_name", "saved_views", ["tenant_id", "user_id", "entity_type", "name"]) - op.create_index("ix_saved_views_tenant_user", "saved_views", ["tenant_id", "user_id"]) - op.create_index("ix_saved_views_tenant_entity", "saved_views", ["tenant_id", "entity_type"]) + op.execute('CREATE INDEX IF NOT EXISTS ix_saved_views_tenant_user ON saved_views (tenant_id, user_id)') + op.execute('CREATE INDEX IF NOT EXISTS ix_saved_views_tenant_entity ON saved_views (tenant_id, entity_type)') def downgrade() -> None: diff --git a/alembic/versions/0048_contact_folder_permissions.py b/alembic/versions/0048_contact_folder_permissions.py index 9f0617c..4c2d16d 100644 --- a/alembic/versions/0048_contact_folder_permissions.py +++ b/alembic/versions/0048_contact_folder_permissions.py @@ -34,10 +34,10 @@ def upgrade() -> None: name="ck_cfp_exactly_one_principal", ), ) - op.create_index("ix_cfp_folder", "contact_folder_permissions", ["folder_id"]) - op.create_index("ix_cfp_user", "contact_folder_permissions", ["user_id"]) - op.create_index("ix_cfp_group", "contact_folder_permissions", ["group_id"]) - op.create_index("ix_cfp_tenant", "contact_folder_permissions", ["tenant_id"]) + op.execute('CREATE INDEX IF NOT EXISTS ix_cfp_folder ON contact_folder_permissions (folder_id)') + op.execute('CREATE INDEX IF NOT EXISTS ix_cfp_user ON contact_folder_permissions (user_id)') + op.execute('CREATE INDEX IF NOT EXISTS ix_cfp_group ON contact_folder_permissions (group_id)') + op.execute('CREATE INDEX IF NOT EXISTS ix_cfp_tenant ON contact_folder_permissions (tenant_id)') def downgrade() -> None: diff --git a/alembic/versions/0049_entity_permissions.py b/alembic/versions/0049_entity_permissions.py index cc2bd14..baa30e0 100644 --- a/alembic/versions/0049_entity_permissions.py +++ b/alembic/versions/0049_entity_permissions.py @@ -33,10 +33,10 @@ def upgrade() -> None: sa.CheckConstraint("principal_type IN ('user', 'group', 'role', 'guest')", name="ck_ep_principal_type"), sa.CheckConstraint("permission_level IN ('none', 'read', 'write', 'admin', 'delete')", name="ck_ep_permission_level"), ) - op.create_index("ix_ep_entity", "entity_permissions", ["entity_type", "entity_id"]) - op.create_index("ix_ep_principal", "entity_permissions", ["principal_type", "principal_id"]) - op.create_index("ix_ep_tenant", "entity_permissions", ["tenant_id"]) - op.create_index("ix_ep_expires", "entity_permissions", ["expires_at"]) + op.execute('CREATE INDEX IF NOT EXISTS ix_ep_entity ON entity_permissions (entity_type, entity_id)') + op.execute('CREATE INDEX IF NOT EXISTS ix_ep_principal ON entity_permissions (principal_type, principal_id)') + op.execute('CREATE INDEX IF NOT EXISTS ix_ep_tenant ON entity_permissions (tenant_id)') + op.execute('CREATE INDEX IF NOT EXISTS ix_ep_expires ON entity_permissions (expires_at)') def downgrade() -> None: diff --git a/alembic/versions/0053_mail_owner_id.py b/alembic/versions/0053_mail_owner_id.py index 24ed2df..4cfecc4 100644 --- a/alembic/versions/0053_mail_owner_id.py +++ b/alembic/versions/0053_mail_owner_id.py @@ -20,8 +20,8 @@ depends_on = None def upgrade(): - op.execute("ALTER TABLE mail_accounts ADD COLUMN IF NOT EXISTS owner_id UUID REFERENCES users(id) ON DELETE SET NULL") - op.execute("CREATE INDEX IF NOT EXISTS ix_mail_accounts_owner ON mail_accounts (owner_id)") + op.execute("ALTER TABLE IF EXISTS mail_accounts ADD COLUMN IF NOT EXISTS owner_id UUID REFERENCES users(id) ON DELETE SET NULL") + op.execute("DO $$ BEGIN IF EXISTS (SELECT 1 FROM information_schema.tables WHERE table_name = 'mail_accounts') THEN CREATE INDEX IF NOT EXISTS ix_mail_accounts_owner ON mail_accounts (owner_id); END IF; END $$") def downgrade(): diff --git a/alembic/versions/0054_plugin_owner_id.py b/alembic/versions/0054_plugin_owner_id.py index a0c802b..24d14c6 100644 --- a/alembic/versions/0054_plugin_owner_id.py +++ b/alembic/versions/0054_plugin_owner_id.py @@ -29,6 +29,15 @@ def upgrade() -> None: # Check which columns already exist before adding conn = op.get_bind() for table in TABLES: + # Check if table exists + table_exists = conn.execute( + sa.text( + "SELECT 1 FROM information_schema.tables WHERE table_name = :table" + ), + {"table": table}, + ).fetchone() + if table_exists is None: + continue # Check if column already exists result = conn.execute( sa.text( @@ -38,16 +47,8 @@ def upgrade() -> None: {"table": table}, ) if result.fetchone() is None: - op.add_column( - table, - sa.Column( - "owner_id", - PGUUID(as_uuid=True), - sa.ForeignKey("users.id", ondelete="SET NULL"), - nullable=True, - ), - ) - op.create_index(f"ix_{table}_owner", table, ["owner_id"]) + op.execute(f"ALTER TABLE {table} ADD COLUMN IF NOT EXISTS owner_id UUID REFERENCES users(id) ON DELETE SET NULL") + op.execute(f"CREATE INDEX IF NOT EXISTS ix_{table}_owner ON {table} (owner_id)") def downgrade() -> None: diff --git a/alembic/versions/0055_entity_policies.py b/alembic/versions/0055_entity_policies.py index eafa184..6f4584e 100644 --- a/alembic/versions/0055_entity_policies.py +++ b/alembic/versions/0055_entity_policies.py @@ -39,11 +39,11 @@ def upgrade() -> None: name="ck_epol_effect", ), ) - op.create_index("ix_epol_entity_type", "entity_policies", ["entity_type"]) - op.create_index("ix_epol_principal", "entity_policies", ["principal_type", "principal_id"]) - op.create_index("ix_epol_tenant", "entity_policies", ["tenant_id"]) - op.create_index("ix_epol_priority", "entity_policies", ["priority"]) - op.create_index("ix_epol_enabled", "entity_policies", ["enabled"]) + op.execute('CREATE INDEX IF NOT EXISTS ix_epol_entity_type ON entity_policies (entity_type)') + op.execute('CREATE INDEX IF NOT EXISTS ix_epol_principal ON entity_policies (principal_type, principal_id)') + op.execute('CREATE INDEX IF NOT EXISTS ix_epol_tenant ON entity_policies (tenant_id)') + op.execute('CREATE INDEX IF NOT EXISTS ix_epol_priority ON entity_policies (priority)') + op.execute('CREATE INDEX IF NOT EXISTS ix_epol_enabled ON entity_policies (enabled)') def downgrade() -> None: diff --git a/alembic/versions/0056_permission_templates.py b/alembic/versions/0056_permission_templates.py index 39324f8..51d7117 100644 --- a/alembic/versions/0056_permission_templates.py +++ b/alembic/versions/0056_permission_templates.py @@ -32,8 +32,8 @@ def upgrade() -> None: name="ck_pt_level", ), ) - op.create_index("ix_pt_entity_type", "permission_templates", ["entity_type"]) - op.create_index("ix_pt_tenant", "permission_templates", ["tenant_id"]) + op.execute('CREATE INDEX IF NOT EXISTS ix_pt_entity_type ON permission_templates (entity_type)') + op.execute('CREATE INDEX IF NOT EXISTS ix_pt_tenant ON permission_templates (tenant_id)') def downgrade() -> None: diff --git a/alembic/versions/0057_permission_delegations.py b/alembic/versions/0057_permission_delegations.py index 2453289..2ed736b 100644 --- a/alembic/versions/0057_permission_delegations.py +++ b/alembic/versions/0057_permission_delegations.py @@ -33,10 +33,10 @@ def upgrade() -> None: name="ck_pd_end_after_start", ), ) - op.create_index("ix_pd_from_user", "permission_delegations", ["from_user_id"]) - op.create_index("ix_pd_to_user", "permission_delegations", ["to_user_id"]) - op.create_index("ix_pd_tenant", "permission_delegations", ["tenant_id"]) - op.create_index("ix_pd_active", "permission_delegations", ["active"]) + op.execute('CREATE INDEX IF NOT EXISTS ix_pd_from_user ON permission_delegations (from_user_id)') + op.execute('CREATE INDEX IF NOT EXISTS ix_pd_to_user ON permission_delegations (to_user_id)') + op.execute('CREATE INDEX IF NOT EXISTS ix_pd_tenant ON permission_delegations (tenant_id)') + op.execute('CREATE INDEX IF NOT EXISTS ix_pd_active ON permission_delegations (active)') def downgrade() -> None: diff --git a/alembic/versions/0059_guest_users.py b/alembic/versions/0059_guest_users.py index 328df02..ff748d1 100644 --- a/alembic/versions/0059_guest_users.py +++ b/alembic/versions/0059_guest_users.py @@ -34,9 +34,9 @@ def upgrade() -> None: sa.Column("created_at", sa.DateTime(timezone=True), server_default=sa.func.now(), nullable=False), sa.Column("updated_at", sa.DateTime(timezone=True), server_default=sa.func.now(), nullable=False), ) - op.create_index("ix_guest_users_email_tenant", "guest_users", ["email", "tenant_id"], unique=True) - op.create_index("ix_guest_users_status", "guest_users", ["status", "tenant_id"]) - op.create_index("ix_guest_users_invited_by", "guest_users", ["invited_by"]) + op.execute('CREATE INDEX IF NOT EXISTS ix_guest_users_email_tenant ON guest_users (email, tenant_id)') + op.execute('CREATE INDEX IF NOT EXISTS ix_guest_users_status ON guest_users (status, tenant_id)') + op.execute('CREATE INDEX IF NOT EXISTS ix_guest_users_invited_by ON guest_users (invited_by)') def downgrade() -> None: diff --git a/alembic/versions/0071_entity_attachments.py b/alembic/versions/0071_entity_attachments.py index dfa4a78..5ef551e 100644 --- a/alembic/versions/0071_entity_attachments.py +++ b/alembic/versions/0071_entity_attachments.py @@ -22,6 +22,45 @@ depends_on = None def upgrade() -> None: + # Create folders table if it doesn't exist (DMS plugin table normally created via create_all) + op.execute(""" + CREATE TABLE IF NOT EXISTS folders ( + id UUID DEFAULT gen_random_uuid() PRIMARY KEY, + tenant_id UUID NOT NULL, + name VARCHAR(255) NOT NULL, + parent_id UUID REFERENCES folders(id) ON DELETE CASCADE, + owner_id UUID REFERENCES users(id) ON DELETE SET NULL, + created_by UUID NOT NULL, + created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() NOT NULL, + updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() NOT NULL, + deleted_at TIMESTAMP WITH TIME ZONE + ) + """) + op.execute('CREATE INDEX IF NOT EXISTS ix_folders_parent ON folders (parent_id)') + op.execute('CREATE INDEX IF NOT EXISTS ix_folders_tenant ON folders (tenant_id)') + + # Create files table if it doesn't exist (DMS plugin table normally created via create_all) + op.execute(""" + CREATE TABLE IF NOT EXISTS files ( + id UUID DEFAULT gen_random_uuid() PRIMARY KEY, + tenant_id UUID NOT NULL, + name VARCHAR(255) NOT NULL, + folder_id UUID REFERENCES folders(id) ON DELETE SET NULL, + owner_id UUID REFERENCES users(id) ON DELETE SET NULL, + uploaded_by UUID NOT NULL, + mime_type VARCHAR(255) NOT NULL, + size_bytes INTEGER NOT NULL, + storage_path VARCHAR(1024) NOT NULL, + content_hash VARCHAR(64), + created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() NOT NULL, + updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() NOT NULL, + deleted_at TIMESTAMP WITH TIME ZONE + ) + """) + op.execute('CREATE INDEX IF NOT EXISTS ix_files_folder ON files (folder_id)') + op.execute('CREATE INDEX IF NOT EXISTS ix_files_tenant ON files (tenant_id)') + op.execute('CREATE INDEX IF NOT EXISTS ix_files_name ON files (name)') + op.create_table( "entity_attachments", sa.Column("id", PGUUID(as_uuid=True), primary_key=True, server_default=sa.text("gen_random_uuid()")), @@ -38,10 +77,10 @@ def upgrade() -> None: sa.Column("deleted_at", sa.DateTime(timezone=True), nullable=True), ) - op.create_index("ix_entity_attachments_entity", "entity_attachments", ["entity_type", "entity_id", "tenant_id"]) - op.create_index("ix_entity_attachments_tenant", "entity_attachments", ["tenant_id"]) - op.create_index("ix_entity_attachments_dms_file", "entity_attachments", ["dms_file_id"]) - op.create_index("ix_entity_attachments_owner", "entity_attachments", ["owner_id"]) + op.execute('CREATE INDEX IF NOT EXISTS ix_entity_attachments_entity ON entity_attachments (entity_type, entity_id, tenant_id)') + op.execute('CREATE INDEX IF NOT EXISTS ix_entity_attachments_tenant ON entity_attachments (tenant_id)') + op.execute('CREATE INDEX IF NOT EXISTS ix_entity_attachments_dms_file ON entity_attachments (dms_file_id)') + op.execute('CREATE INDEX IF NOT EXISTS ix_entity_attachments_owner ON entity_attachments (owner_id)') # Enable RLS on entity_attachments (tenant isolation) op.execute("ALTER TABLE entity_attachments ENABLE ROW LEVEL SECURITY") diff --git a/alembic/versions/0072_workspaces.py b/alembic/versions/0072_workspaces.py index 38ebe53..4a1d4ed 100644 --- a/alembic/versions/0072_workspaces.py +++ b/alembic/versions/0072_workspaces.py @@ -30,7 +30,7 @@ def upgrade() -> None: sa.Column("updated_at", sa.DateTime(timezone=True), server_default=sa.text("NOW()"), nullable=False), sa.UniqueConstraint("tenant_id", "name", name="uq_workspaces_tenant_name"), ) - op.create_index("ix_workspaces_tenant", "workspaces", ["tenant_id"]) + op.execute('CREATE INDEX IF NOT EXISTS ix_workspaces_tenant ON workspaces (tenant_id)') op.execute( "CREATE UNIQUE INDEX uq_workspace_default_per_tenant " "ON workspaces (tenant_id) WHERE is_default = true" @@ -50,7 +50,7 @@ def upgrade() -> None: sa.Column("updated_at", sa.DateTime(timezone=True), server_default=sa.text("NOW()"), nullable=False), sa.UniqueConstraint("tenant_id", "workspace_id", "module_key", name="uq_wm_tenant_workspace_module"), ) - op.create_index("ix_wm_workspace", "workspace_modules", ["tenant_id", "workspace_id", "menu_order"]) + op.execute('CREATE INDEX IF NOT EXISTS ix_wm_workspace ON workspace_modules (tenant_id, workspace_id, menu_order)') # workspace_users op.create_table( @@ -66,8 +66,8 @@ def upgrade() -> None: sa.UniqueConstraint("tenant_id", "workspace_id", "user_id", name="uq_wu_tenant_workspace_user"), sa.CheckConstraint("role IN ('member', 'manager')", name="ck_wu_role"), ) - op.create_index("ix_wu_workspace", "workspace_users", ["tenant_id", "workspace_id"]) - op.create_index("ix_wu_user", "workspace_users", ["tenant_id", "user_id"]) + op.execute('CREATE INDEX IF NOT EXISTS ix_wu_workspace ON workspace_users (tenant_id, workspace_id)') + op.execute('CREATE INDEX IF NOT EXISTS ix_wu_user ON workspace_users (tenant_id, user_id)') # workspace_widgets op.create_table( @@ -84,7 +84,7 @@ def upgrade() -> None: sa.Column("created_at", sa.DateTime(timezone=True), server_default=sa.text("NOW()"), nullable=False), sa.Column("updated_at", sa.DateTime(timezone=True), server_default=sa.text("NOW()"), nullable=False), ) - op.create_index("ix_ww_workspace", "workspace_widgets", ["tenant_id", "workspace_id"]) + op.execute('CREATE INDEX IF NOT EXISTS ix_ww_workspace ON workspace_widgets (tenant_id, workspace_id)') # RLS on all workspace tables for table in ["workspaces", "workspace_modules", "workspace_users", "workspace_widgets"]: diff --git a/alembic/versions/0075_outbox_envelope.py b/alembic/versions/0075_outbox_envelope.py index 1eb2ff6..0e3c73b 100644 --- a/alembic/versions/0075_outbox_envelope.py +++ b/alembic/versions/0075_outbox_envelope.py @@ -30,14 +30,14 @@ depends_on = None def upgrade() -> None: # 1. Add envelope columns to event_outbox - op.execute("ALTER TABLE event_outbox ADD COLUMN IF NOT EXISTS aggregate_type VARCHAR(100)") - op.execute("ALTER TABLE event_outbox ADD COLUMN IF NOT EXISTS aggregate_id UUID") - op.execute("ALTER TABLE event_outbox ADD COLUMN IF NOT EXISTS occurred_at TIMESTAMPTZ NOT NULL DEFAULT NOW()") - op.execute("ALTER TABLE event_outbox ADD COLUMN IF NOT EXISTS correlation_id UUID") - op.execute("ALTER TABLE event_outbox ADD COLUMN IF NOT EXISTS schema_version INTEGER NOT NULL DEFAULT 1") + op.execute("ALTER TABLE IF EXISTS event_outbox ADD COLUMN IF NOT EXISTS aggregate_type VARCHAR(100)") + op.execute("ALTER TABLE IF EXISTS event_outbox ADD COLUMN IF NOT EXISTS aggregate_id UUID") + op.execute("ALTER TABLE IF EXISTS event_outbox ADD COLUMN IF NOT EXISTS occurred_at TIMESTAMPTZ NOT NULL DEFAULT NOW()") + op.execute("ALTER TABLE IF EXISTS event_outbox ADD COLUMN IF NOT EXISTS correlation_id UUID") + op.execute("ALTER TABLE IF EXISTS event_outbox ADD COLUMN IF NOT EXISTS schema_version INTEGER NOT NULL DEFAULT 1") - op.execute("CREATE INDEX IF NOT EXISTS ix_event_outbox_aggregate ON event_outbox (tenant_id, aggregate_type, aggregate_id)") - op.execute("CREATE INDEX IF NOT EXISTS ix_event_outbox_correlation ON event_outbox (correlation_id)") + op.execute("DO $$ BEGIN IF EXISTS (SELECT 1 FROM information_schema.tables WHERE table_name = 'event_outbox') THEN CREATE INDEX IF NOT EXISTS ix_event_outbox_aggregate ON event_outbox (tenant_id, aggregate_type, aggregate_id); END IF; END $$") + op.execute("DO $$ BEGIN IF EXISTS (SELECT 1 FROM information_schema.tables WHERE table_name = 'event_outbox') THEN CREATE INDEX IF NOT EXISTS ix_event_outbox_correlation ON event_outbox (correlation_id); END IF; END $$") # 2. Create outbox_deliveries table op.create_table( @@ -54,8 +54,8 @@ def upgrade() -> None: sa.Column("updated_at", sa.DateTime(timezone=True), server_default=sa.text("NOW()"), nullable=False), sa.UniqueConstraint("event_id", "consumer_name", name="uq_outbox_deliveries_event_consumer"), ) - op.create_index("ix_outbox_deliveries_event", "outbox_deliveries", ["event_id"]) - op.create_index("ix_outbox_deliveries_status", "outbox_deliveries", ["status", "next_attempt_at"]) + op.execute('CREATE INDEX IF NOT EXISTS ix_outbox_deliveries_event ON outbox_deliveries (event_id)') + op.execute('CREATE INDEX IF NOT EXISTS ix_outbox_deliveries_status ON outbox_deliveries (status, next_attempt_at)') # RLS + Grants op.execute("ALTER TABLE outbox_deliveries ENABLE ROW LEVEL SECURITY") diff --git a/alembic/versions/0082_add_sensitivity_to_custom_field_definitions.py b/alembic/versions/0082_add_sensitivity_to_custom_field_definitions.py index 3ba4fe0..ecf5732 100644 --- a/alembic/versions/0082_add_sensitivity_to_custom_field_definitions.py +++ b/alembic/versions/0082_add_sensitivity_to_custom_field_definitions.py @@ -13,10 +13,7 @@ depends_on = None def upgrade() -> None: - op.add_column( - "custom_field_definitions", - sa.Column("sensitivity", sa.String(20), nullable=False, server_default="normal"), - ) + op.execute("ALTER TABLE IF EXISTS custom_field_definitions ADD COLUMN IF NOT EXISTS sensitivity VARCHAR(20) NOT NULL DEFAULT 'normal'") def downgrade() -> None: diff --git a/alembic/versions/0085_restore_tenant_rls.py b/alembic/versions/0085_restore_tenant_rls.py index e9944bd..232135d 100644 --- a/alembic/versions/0085_restore_tenant_rls.py +++ b/alembic/versions/0085_restore_tenant_rls.py @@ -101,9 +101,9 @@ def upgrade() -> None: # crm_migration is the table owner and needs to run tenant-wide data migrations _exec("ALTER ROLE crm_migration NOSUPERUSER BYPASSRLS") - # Step 3: Transfer ALL table ownership to crm_migration + # Step 3: Transfer ALL table ownership to crm_migration (only for tables that exist) for table in ALL_TABLES: - _exec(f"ALTER TABLE public.{table} OWNER TO crm_migration") + _exec(f"DO $$ BEGIN IF EXISTS (SELECT 1 FROM information_schema.tables WHERE table_schema = 'public' AND table_name = '{table}') THEN ALTER TABLE public.{table} OWNER TO crm_migration; END IF; END $$") # Transfer sequence ownership _exec("DO $$ DECLARE r RECORD; BEGIN FOR r IN SELECT sequence_name FROM information_schema.sequences WHERE sequence_schema = 'public' LOOP EXECUTE format('ALTER SEQUENCE public.%I OWNER TO crm_migration', r.sequence_name); END LOOP; END $$;") @@ -114,8 +114,8 @@ def upgrade() -> None: _exec(f"REVOKE ALL PRIVILEGES ON ALL SEQUENCES IN SCHEMA public FROM {role}") _exec(f"REVOKE ALL PRIVILEGES ON SCHEMA public FROM {role}") - # Step 5: Drop crm_runtime role - _exec("DROP ROLE IF EXISTS crm_runtime") + # Step 5: Drop crm_runtime role (may fail if permissions insufficient) + _exec("DO $$ BEGIN DROP ROLE IF EXISTS crm_runtime; EXCEPTION WHEN insufficient_privilege THEN NULL; END $$") # Step 6: Grant schema USAGE to runtime roles _exec("GRANT USAGE ON SCHEMA public TO crm_api") @@ -125,12 +125,11 @@ def upgrade() -> None: # Step 7: Grant permissions to crm_auth (identity tables only) for table, privs in AUTH_TABLES.items(): priv_str = ", ".join(privs) - _exec(f"GRANT {priv_str} ON public.{table} TO crm_auth") + _exec(f"DO $$ BEGIN IF EXISTS (SELECT 1 FROM information_schema.tables WHERE table_schema = 'public' AND table_name = '{table}') THEN GRANT {priv_str} ON public.{table} TO crm_auth; END IF; END $$") - # Step 8: Grant CRUD on tenant tables to crm_api and crm_worker + # Step 8: Grant CRUD on tenant tables to crm_api and crm_worker (only for tables that exist) for table in TENANT_TABLES: - _exec(f"GRANT SELECT, INSERT, UPDATE, DELETE ON public.{table} TO crm_api") - _exec(f"GRANT SELECT, INSERT, UPDATE, DELETE ON public.{table} TO crm_worker") + _exec(f"DO $$ BEGIN IF EXISTS (SELECT 1 FROM information_schema.tables WHERE table_schema = 'public' AND table_name = '{table}') THEN GRANT SELECT, INSERT, UPDATE, DELETE ON public.{table} TO crm_api; GRANT SELECT, INSERT, UPDATE, DELETE ON public.{table} TO crm_worker; END IF; END $$") # Grant sequence USAGE to crm_api and crm_worker _exec("GRANT USAGE, SELECT ON ALL SEQUENCES IN SCHEMA public TO crm_api") @@ -139,19 +138,19 @@ def upgrade() -> None: # Step 9: Grant global table access to crm_api (except alembic_version) api_global_tables = [t for t in GLOBAL_TABLES if t != "alembic_version"] for table in api_global_tables: - _exec(f"GRANT SELECT, INSERT, UPDATE, DELETE ON public.{table} TO crm_api") + _exec(f"DO $$ BEGIN IF EXISTS (SELECT 1 FROM information_schema.tables WHERE table_schema = 'public' AND table_name = '{table}') THEN GRANT SELECT, INSERT, UPDATE, DELETE ON public.{table} TO crm_api; END IF; END $$") # Step 10: Grant worker global table access for table, privs in WORKER_GLOBAL_TABLES.items(): priv_str = ", ".join(privs) - _exec(f"GRANT {priv_str} ON public.{table} TO crm_worker") + _exec(f"DO $$ BEGIN IF EXISTS (SELECT 1 FROM information_schema.tables WHERE table_schema = 'public' AND table_name = '{table}') THEN GRANT {priv_str} ON public.{table} TO crm_worker; END IF; END $$") worker_global_tables = [ t for t in GLOBAL_TABLES if t != "alembic_version" and t not in WORKER_GLOBAL_TABLES ] for table in worker_global_tables: - _exec(f"GRANT SELECT, INSERT, UPDATE, DELETE ON public.{table} TO crm_worker") + _exec(f"DO $$ BEGIN IF EXISTS (SELECT 1 FROM information_schema.tables WHERE table_schema = 'public' AND table_name = '{table}') THEN GRANT SELECT, INSERT, UPDATE, DELETE ON public.{table} TO crm_worker; END IF; END $$") # Step 11: Drop ALL old RLS policies and create new fail-closed ones policy_template = ( @@ -164,17 +163,11 @@ def upgrade() -> None: ) for table in TENANT_TABLES: - _exec(f"DROP POLICY IF EXISTS tenant_isolation ON public.{table}") - _exec(f"DROP POLICY IF EXISTS {table}_tenant_isolation ON public.{table}") - _exec(f"ALTER TABLE public.{table} ENABLE ROW LEVEL SECURITY") - _exec(f"ALTER TABLE public.{table} FORCE ROW LEVEL SECURITY") - _exec(policy_template.format(table=table)) + _exec(f"DO $$ BEGIN IF EXISTS (SELECT 1 FROM information_schema.tables WHERE table_schema = 'public' AND table_name = '{table}') THEN DROP POLICY IF EXISTS tenant_isolation ON public.{table}; DROP POLICY IF EXISTS {table}_tenant_isolation ON public.{table}; ALTER TABLE public.{table} ENABLE ROW LEVEL SECURITY; ALTER TABLE public.{table} FORCE ROW LEVEL SECURITY; {policy_template.format(table=table)}; END IF; END $$") # Step 12: Disable RLS on global tables for table in GLOBAL_TABLES: - _exec(f"DROP POLICY IF EXISTS tenant_isolation ON public.{table}") - _exec(f"DROP POLICY IF EXISTS {table}_tenant_isolation ON public.{table}") - _exec(f"ALTER TABLE public.{table} DISABLE ROW LEVEL SECURITY") + _exec(f"DO $$ BEGIN IF EXISTS (SELECT 1 FROM information_schema.tables WHERE table_schema = 'public' AND table_name = '{table}') THEN DROP POLICY IF EXISTS tenant_isolation ON public.{table}; DROP POLICY IF EXISTS {table}_tenant_isolation ON public.{table}; ALTER TABLE public.{table} DISABLE ROW LEVEL SECURITY; END IF; END $$") # Step 13: Set default privileges for crm_migration owner _exec("ALTER DEFAULT PRIVILEGES FOR ROLE crm_migration IN SCHEMA public GRANT SELECT, INSERT, UPDATE, DELETE ON TABLES TO crm_api")