Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
87 changes: 87 additions & 0 deletions alembic/versions/b7e1c9f2a3d4_consolidated_integrations_phase_1.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
"""consolidated integrations catalog

Revision ID: b7e1c9f2a3d4
Revises: a3d7c9e8b4f2
Create Date: 2026-05-25 21:00:00.000000

Adds the consolidated Integrations catalog model: an integration catalog table.

Additive only -- no existing tables are dropped. Secret, OAuthIntegration,
WorkspaceOAuthProvider, and MCPIntegration storage continue to drive credential
and MCP behavior.
"""

from collections.abc import Sequence

import sqlalchemy as sa
from sqlalchemy.dialects import postgresql

from alembic import op

# revision identifiers, used by Alembic.
revision: str = "b7e1c9f2a3d4"
down_revision: str | None = "a3d7c9e8b4f2"
branch_labels: str | Sequence[str] | None = None
depends_on: str | Sequence[str] | None = None


INTEGRATION_SOURCE_VALUES = ("platform", "workspace")


def upgrade() -> None:
# --- enums ---------------------------------------------------------
sa.Enum(*INTEGRATION_SOURCE_VALUES, name="integrationsource").create(op.get_bind())

# --- integration ---------------------------------------------------
op.create_table(
"integration",
sa.Column("id", sa.UUID(), nullable=False),
sa.Column("workspace_id", sa.UUID(), nullable=True),
sa.Column("namespace", sa.String(), nullable=False),
sa.Column("display_name", sa.String(), nullable=False),
sa.Column("description", sa.String(), nullable=True),
sa.Column("icon_url", sa.String(), nullable=True),
sa.Column(
"source",
postgresql.ENUM(
*INTEGRATION_SOURCE_VALUES,
name="integrationsource",
create_type=False,
),
nullable=False,
),
sa.Column(
"created_at",
sa.TIMESTAMP(timezone=True),
server_default=sa.text("now()"),
nullable=False,
),
sa.Column(
"updated_at",
sa.TIMESTAMP(timezone=True),
server_default=sa.text("now()"),
nullable=False,
),
sa.ForeignKeyConstraint(
["workspace_id"],
["workspace.id"],
name=op.f("fk_integration_workspace_id_workspace"),
ondelete="CASCADE",
),
sa.PrimaryKeyConstraint("id", name=op.f("pk_integration")),
sa.UniqueConstraint(
"workspace_id",
"namespace",
name="uq_integration_workspace_namespace",
Comment on lines +72 to +75

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Add null-safe uniqueness for platform integration namespaces

This uniqueness rule does not actually prevent duplicate platform rows because PostgreSQL treats NULL values as distinct in unique constraints. Since platform entries use workspace_id = NULL, multiple rows with the same namespace can be inserted, which can lead to duplicated catalog entries and unstable behavior in seed/list flows that assume one platform row per namespace.

Useful? React with 👍 / 👎.

),
)
op.create_index(op.f("ix_integration_id"), "integration", ["id"], unique=True)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: Redundant unique index created on primary key column id; primary key already creates a unique index.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At alembic/versions/b7e1c9f2a3d4_consolidated_integrations_phase_1.py, line 78:

<comment>Redundant unique index created on primary key column `id`; primary key already creates a unique index.</comment>

<file context>
@@ -0,0 +1,87 @@
+            name="uq_integration_workspace_namespace",
+        ),
+    )
+    op.create_index(op.f("ix_integration_id"), "integration", ["id"], unique=True)
+    op.create_index("ix_integration_namespace", "integration", ["namespace"])
+
</file context>

op.create_index("ix_integration_namespace", "integration", ["namespace"])


def downgrade() -> None:
op.drop_index("ix_integration_namespace", table_name="integration")
op.drop_index(op.f("ix_integration_id"), table_name="integration")
op.drop_table("integration")

sa.Enum(name="integrationsource").drop(op.get_bind())
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
"""backfill secret namespaces into integrations

Revision ID: c8f2d1e4a5b6
Revises: b7e1c9f2a3d4
Create Date: 2026-05-25 23:00:00.000000

For secret names that don't already have an Integration row, a workspace-scoped
``Integration`` is created on demand so the Integrations catalog can project
legacy static credentials without moving credential storage.

Additive only -- the legacy ``secret`` table remains the source of truth for
static/API-key credentials.
"""

from collections.abc import Sequence

from alembic import op

# revision identifiers, used by Alembic.
revision: str = "c8f2d1e4a5b6"
down_revision: str | None = "b7e1c9f2a3d4"
branch_labels: str | Sequence[str] | None = None
depends_on: str | Sequence[str] | None = None


def upgrade() -> None:
# Backfill workspace-scoped Integration rows for every Secret name that
# doesn't already have a catalog entry. Source=workspace so we can tell
# these apart from the platform-seeded providers.
op.execute(
"""
INSERT INTO integration (
id, workspace_id, namespace, display_name, description,
source, created_at, updated_at
)
SELECT
gen_random_uuid(),
s.workspace_id,
s.name,
INITCAP(REPLACE(s.name, '_', ' ')),
'Backfilled from legacy credential.',
'workspace',
NOW(),
NOW()
FROM (
SELECT DISTINCT workspace_id, name
FROM secret
) AS s
WHERE NOT EXISTS (
SELECT 1
FROM integration i
WHERE i.namespace = s.name
AND (
i.workspace_id IS NULL
OR i.workspace_id = s.workspace_id
)
)
"""
)


def downgrade() -> None:
# This migration only adds catalog rows for pre-existing secret names.
# Leave them in place on downgrade; deleting them could hide credentials
# from the catalog after a downgrade/upgrade loop.
pass

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: Do not use a silent no-op downgrade for a one-way data migration; raise an explicit NotImplementedError so rollback attempts don’t appear successful while leaving upgraded data in place.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At alembic/versions/c8f2d1e4a5b6_backfill_secret_namespaces_into_integrations.py, line 66:

<comment>Do not use a silent no-op downgrade for a one-way data migration; raise an explicit `NotImplementedError` so rollback attempts don’t appear successful while leaving upgraded data in place.</comment>

<file context>
@@ -0,0 +1,66 @@
+    # This migration only adds catalog rows for pre-existing secret names.
+    # Leave them in place on downgrade; deleting them could hide credentials
+    # from the catalog after a downgrade/upgrade loop.
+    pass
</file context>
Suggested change
pass
raise NotImplementedError(
"One-way data backfill; downgrade cannot safely reconstruct prior state. Restore database from backup/snapshot, then roll back the app."
)

Loading
Loading