diff --git a/.github/scripts/check-project-rollout.py b/.github/scripts/check-project-rollout.py new file mode 100644 index 00000000000..a2f04734326 --- /dev/null +++ b/.github/scripts/check-project-rollout.py @@ -0,0 +1,100 @@ +#!/usr/bin/env python3 +"""Read-only, fail-closed ECS retirement check for the Project column contract migration. + +The expected digest acknowledges fully deployed #8830 transaction-barrier and durable-authority support +and retirement of every incompatible server and relevant old worker job before switching. Every live and supported +rollback application must tolerate project_workspace being absent. +AWS checks below independently verify ECS retirement, not worker drainage. +""" +import argparse +import datetime +import json +import re +import subprocess +import sys + + +def aws(region, *args): + result = subprocess.run( + ['aws', '--region', region, '--no-cli-pager', '--cli-connect-timeout', '10', '--cli-read-timeout', '30', *args, '--output', 'json'], + capture_output=True, text=True, timeout=90, check=False, + ) + if result.returncode: + raise RuntimeError('AWS preflight read failed; check deployment-read permissions') + return json.loads(result.stdout) + + +def latest_execution(region, pipeline): + executions = aws(region, 'codepipeline', 'list-pipeline-executions', '--pipeline-name', pipeline).get('pipelineExecutionSummaries', []) + def epoch(execution): + value = execution['startTime'] + return value if isinstance(value, (int, float)) else datetime.datetime.fromisoformat(value.replace('Z', '+00:00')).timestamp() + if not executions: + raise RuntimeError('No application deployment execution was found') + return max(executions, key=epoch) + + +def matches_release(execution, digest): + return execution.get('status') == 'Succeeded' and any( + revision.get('actionName') == 'ECR_Source' and revision.get('revisionId') == digest + for revision in execution.get('sourceRevisions', []) + ) + + +def verify(environment, region, digest): + if not re.fullmatch(r'sha256:[0-9a-f]{64}', digest): + raise RuntimeError('Set the environment-specific PROJECT_COLUMN_ENFORCEMENT_READY_IMAGE_DIGEST after verifying #8830 authority-aware readers/writers, transaction barriers and all incompatible server/worker drainage') + pipeline = f'sim-{environment}-{region}-app-deployment' + execution = latest_execution(region, pipeline) + if not matches_release(execution, digest): + raise RuntimeError('The latest app pipeline has not completed for the acknowledged image; traffic cutover alone is insufficient') + execution_id = execution['pipelineExecutionId'] + group = aws(region, 'deploy', 'get-deployment-group', '--application-name', f'sim-{environment}-{region}-ecs-app', + '--deployment-group-name', f'sim-{environment}-{region}-app-dg')['deploymentGroupInfo'] + services = group.get('ecsServices', []) + if len(services) != 1: + raise RuntimeError('Expected exactly one application ECS service') + cluster, service = services[0]['clusterName'], services[0]['serviceName'] + description = aws(region, 'ecs', 'describe-services', '--cluster', cluster, '--services', service) + if description.get('failures') or len(description.get('services', [])) != 1: + raise RuntimeError('Cannot inspect the application ECS service') + record = description['services'][0] + if record.get('desiredCount', 0) < 1 or record.get('runningCount') != record['desiredCount'] or record.get('pendingCount') != 0: + raise RuntimeError('Application ECS service is not stable') + arns = set() + for status in ('RUNNING', 'STOPPED'): + arns.update(aws(region, 'ecs', 'list-tasks', '--cluster', cluster, '--service-name', service, + '--desired-status', status).get('taskArns', [])) + live = [] + ordered = sorted(arns) + for start in range(0, len(ordered), 100): + response = aws(region, 'ecs', 'describe-tasks', '--cluster', cluster, '--tasks', *ordered[start:start + 100]) + if response.get('failures'): + raise RuntimeError('Cannot account for every ECS task') + if len(response.get('tasks', [])) != len(ordered[start:start + 100]): + raise RuntimeError('Incomplete ECS task response') + live.extend(task for task in response['tasks'] if task.get('lastStatus') != 'STOPPED') + if len(live) != record['desiredCount']: + raise RuntimeError('Old, stopping, or pending ECS tasks remain') + for task in live: + app = [container for container in task.get('containers', []) if container.get('name') == 'app'] + if task.get('lastStatus') != 'RUNNING' or task.get('desiredStatus') != 'RUNNING' or len(app) != 1 or app[0].get('imageDigest') != digest: + raise RuntimeError('A live ECS task does not match the acknowledged compatible release') + latest = latest_execution(region, pipeline) + if latest.get('pipelineExecutionId') != execution_id or not matches_release(latest, digest): + raise RuntimeError('Application deployment changed during preflight') + print(json.dumps({'ecsRetired': True, 'expectedImageDigest': digest, 'pipelineExecutionId': execution_id, + 'operatorAcknowledgedColumnWritersAndWorkers': True})) + + +if __name__ == '__main__': + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument('--environment', required=True, choices=['production', 'staging']) + parser.add_argument('--region', required=True) + parser.add_argument('--expected-image-digest', required=True) + args = parser.parse_args() + try: + verify(args.environment, args.region, args.expected_image_digest) + except (RuntimeError, ValueError, KeyError, TypeError, subprocess.TimeoutExpired) as error: + print(f'Project column rollout preflight refused: {error}', file=sys.stderr) + sys.exit(1) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 3d94c3fd75f..2776a99e77e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -113,6 +113,9 @@ jobs: # in place before the new app version deploys (replaces the removed ECS # migration sidecar) migrate: + permissions: + contents: read + id-token: write name: migrate needs: [checks] # Explicit need results instead of the implicit success(): a skipped job @@ -132,9 +135,16 @@ jobs: STAGING_DATABASE_URL: ${{ secrets.STAGING_DATABASE_URL }} MIGRATION_DATABASE_URL: ${{ secrets.MIGRATION_DATABASE_URL }} STAGING_MIGRATION_DATABASE_URL: ${{ secrets.STAGING_MIGRATION_DATABASE_URL }} + AWS_ROLE_TO_ASSUME: ${{ secrets.AWS_ROLE_TO_ASSUME }} + STAGING_AWS_ROLE_TO_ASSUME: ${{ secrets.STAGING_AWS_ROLE_TO_ASSUME }} + AWS_REGION: ${{ secrets.AWS_REGION }} + STAGING_AWS_REGION: ${{ secrets.STAGING_AWS_REGION }} # Same ordering for dev (schema push before the dev image lands in ECR) migrate-dev: + permissions: + contents: read + id-token: write name: migrate-dev if: github.event_name == 'push' && github.ref == 'refs/heads/dev' uses: ./.github/workflows/migrate.yml diff --git a/.github/workflows/migrate.yml b/.github/workflows/migrate.yml index a598e28982b..2a91393d41d 100644 --- a/.github/workflows/migrate.yml +++ b/.github/workflows/migrate.yml @@ -18,6 +18,14 @@ on: required: false STAGING_MIGRATION_DATABASE_URL: required: false + AWS_ROLE_TO_ASSUME: + required: false + STAGING_AWS_ROLE_TO_ASSUME: + required: false + AWS_REGION: + required: false + STAGING_AWS_REGION: + required: false workflow_dispatch: inputs: environment: @@ -31,6 +39,7 @@ on: permissions: contents: read + id-token: write jobs: migrate: @@ -51,6 +60,33 @@ jobs: provider: ${{ vars.CI_PROVIDER }} node-version: '' + - name: Check Project column expansion and whether enforcement needs rollout evidence + id: project-contract + if: inputs.environment != 'dev' + working-directory: ./packages/db + env: + DATABASE_URL: ${{ inputs.environment == 'production' && secrets.DATABASE_URL || inputs.environment == 'staging' && secrets.STAGING_DATABASE_URL || '' }} + MIGRATION_DATABASE_URL: ${{ inputs.environment == 'production' && secrets.MIGRATION_DATABASE_URL || inputs.environment == 'staging' && secrets.STAGING_MIGRATION_DATABASE_URL || '' }} + run: bun --no-env-file scripts/project-contract-required.ts >> "$GITHUB_OUTPUT" + + - name: Configure AWS for Project connector retirement verification + if: steps.project-contract.outputs.required == 'true' + uses: aws-actions/configure-aws-credentials@e7f100cf4c008499ea8adda475de1042d6975c7b + with: + role-to-assume: ${{ inputs.environment == 'production' && secrets.AWS_ROLE_TO_ASSUME || secrets.STAGING_AWS_ROLE_TO_ASSUME }} + aws-region: ${{ inputs.environment == 'production' && secrets.AWS_REGION || secrets.STAGING_AWS_REGION }} + + # Acknowledge #8830 only after authority-aware readers/writers deploy and all incompatible + # servers and relevant old Trigger.dev runs drain. The runner commits the column-authority + # switch before backfill, validation, and connector retirement; retries never revert it. + # The preflight independently checks ECS retirement; it does not inspect worker runs. + - name: Require completed authority-aware rollout before Project cutover + if: steps.project-contract.outputs.required == 'true' + env: + ENVIRONMENT: ${{ inputs.environment }} + EXPECTED_IMAGE_DIGEST: ${{ inputs.environment == 'production' && vars.PROJECT_COLUMN_ENFORCEMENT_READY_IMAGE_DIGEST_PRODUCTION || inputs.environment == 'staging' && vars.PROJECT_COLUMN_ENFORCEMENT_READY_IMAGE_DIGEST_STAGING || '' }} + run: python3 .github/scripts/check-project-rollout.py --environment "$ENVIRONMENT" --region "$AWS_REGION" --expected-image-digest "$EXPECTED_IMAGE_DIGEST" + # The expression maps the explicit environment input to exactly one repo # secret, so the job never holds another environment's database URL. An # unknown environment resolves to empty and the guard below fails the job. diff --git a/apps/desktop/e2e/fixtures/live-sim.ts b/apps/desktop/e2e/fixtures/live-sim.ts index ed3ff12f055..aca1cddabcc 100644 --- a/apps/desktop/e2e/fixtures/live-sim.ts +++ b/apps/desktop/e2e/fixtures/live-sim.ts @@ -651,9 +651,16 @@ export class SimDatabase { } async close(): Promise { - if (this.userIds.length > 0) - await this.sql`delete from "user" where id in ${this.sql(this.userIds)}`.catch(() => {}) - await this.sql.end({ timeout: 5 }) + try { + if (this.userIds.length > 0) + await this.sql.begin(async (tx) => { + await tx`delete from workspace where owner_id in ${tx(this.userIds)}` + await tx`delete from project where owner_id in ${tx(this.userIds)}` + await tx`delete from "user" where id in ${tx(this.userIds)}` + }) + } finally { + await this.sql.end({ timeout: 5 }) + } } /** A user with a workspace, a session as the desktop sign-in creates, and one chat per title. */ @@ -669,8 +676,10 @@ export class SimDatabase { await tx`insert into "user" (id, name, email, normalized_email, email_verified, created_at, updated_at) values (${userId}, ${name}, ${email}, ${email}, true, now(), now())` await tx`insert into user_stats (id, user_id) values (${generateId()}, ${userId})` - await tx`insert into workspace (id, name, owner_id, billed_account_user_id) - values (${workspaceId}, 'Desktop tools E2E', ${userId}, ${userId})` + await tx`insert into project (id, name, owner_id) + values (${workspaceId}, 'E2E fixture project', ${userId})` + await tx`insert into workspace (id, project_id, name, owner_id, billed_account_user_id) + values (${workspaceId}, ${workspaceId}, 'Desktop tools E2E', ${userId}, ${userId})` await tx`insert into permissions (id, user_id, entity_type, entity_id, permission_type) values (${generateId()}, ${userId}, 'workspace', ${workspaceId}, 'admin')` await tx`insert into session (id, token, user_id, user_agent, expires_at, created_at, updated_at) diff --git a/apps/sim/app/api/copilot/chat/stream/desktop-executor.integration.ts b/apps/sim/app/api/copilot/chat/stream/desktop-executor.integration.ts index 2a0bb1c84bd..b58def23553 100644 --- a/apps/sim/app/api/copilot/chat/stream/desktop-executor.integration.ts +++ b/apps/sim/app/api/copilot/chat/stream/desktop-executor.integration.ts @@ -4,6 +4,7 @@ * Redis through the production reconnect route, for the live tail and the replay batch a view * attaches with after a reload. */ +import { deleteWorkspaceFixture, insertWorkspaceFixture } from '@sim/db/testing/workspace-fixtures' import { authMock, authMockFns } from '@sim/testing/mocks/auth.mock' import { afterAll, beforeAll, describe, expect, it, vi } from 'vitest' @@ -72,7 +73,7 @@ describe.runIf(Boolean(redisUrl))('desktop executor binding on a chat stream', ( createdAt: now, updatedAt: now, }) - await db.insert(workspace).values({ + await insertWorkspaceFixture(db, { id: workspaceId, name: 'Desktop stream fixture', ownerId: userId, @@ -105,7 +106,7 @@ describe.runIf(Boolean(redisUrl))('desktop executor binding on a chat stream', ( await db.delete(copilotChats).where(eq(copilotChats.id, chatId)) await db.delete(desktopDevices).where(eq(desktopDevices.id, deviceId)) await db.delete(permissions).where(eq(permissions.userId, userId)) - await db.delete(workspace).where(eq(workspace.id, workspaceId)) + await deleteWorkspaceFixture(db, eq(workspace.id, workspaceId)) await db.delete(user).where(eq(user.id, userId)) await closeRedisConnection() }) diff --git a/apps/sim/app/api/knowledge/connectors/member-sync/route.integration.ts b/apps/sim/app/api/knowledge/connectors/member-sync/route.integration.ts index 66e77f9ebec..3a4b0660998 100644 --- a/apps/sim/app/api/knowledge/connectors/member-sync/route.integration.ts +++ b/apps/sim/app/api/knowledge/connectors/member-sync/route.integration.ts @@ -1,3 +1,4 @@ +import { deleteWorkspaceFixture } from '@sim/db/testing/workspace-fixtures' /** * The member sync scheduler's reclaim against real PostgreSQL: a members-mode connector whose * member lease went stale is put back on the failure ladder, and a connector in any other access @@ -51,7 +52,7 @@ describe('member sync reclaim in PostgreSQL', () => { }) afterAll(async () => { - await db.delete(workspace).where(eq(workspace.id, ids.workspaceId)) + await deleteWorkspaceFixture(db, eq(workspace.id, ids.workspaceId)) await db.delete(organization).where(eq(organization.id, ids.organizationId)) await db.delete(user).where(inArray(user.id, [ids.aliceId, ids.bobId])) await db.$client.end() diff --git a/apps/sim/app/api/v1/knowledge/route.integration.ts b/apps/sim/app/api/v1/knowledge/route.integration.ts index ab75f05a29d..f19e6e55c55 100644 --- a/apps/sim/app/api/v1/knowledge/route.integration.ts +++ b/apps/sim/app/api/v1/knowledge/route.integration.ts @@ -7,6 +7,7 @@ import type { Principal } from '@sim/auth/principal' import { db } from '@sim/db' import { document, organization, user, workspace } from '@sim/db/schema' +import { deleteWorkspaceFixture } from '@sim/db/testing/workspace-fixtures' import { authMock, authMockFns, createMockRequest } from '@sim/testing' import { generateId } from '@sim/utils/id' import { eq, inArray } from 'drizzle-orm' @@ -85,7 +86,7 @@ describe('knowledge-base document totals in PostgreSQL', () => { }) afterAll(async () => { - await db.delete(workspace).where(eq(workspace.id, ids.workspaceId)) + await deleteWorkspaceFixture(db, eq(workspace.id, ids.workspaceId)) await db.delete(organization).where(eq(organization.id, ids.organizationId)) await db.delete(user).where(inArray(user.id, [ids.aliceId, ids.bobId])) vi.unstubAllGlobals() diff --git a/apps/sim/app/api/webhooks/agentmail/thread-scope.integration.ts b/apps/sim/app/api/webhooks/agentmail/thread-scope.integration.ts index c165e118950..4f7600e0f23 100644 --- a/apps/sim/app/api/webhooks/agentmail/thread-scope.integration.ts +++ b/apps/sim/app/api/webhooks/agentmail/thread-scope.integration.ts @@ -1,3 +1,4 @@ +import { deleteWorkspaceFixture, insertWorkspaceFixture } from '@sim/db/testing/workspace-fixtures' /** * An inbox reply continues its parent task's chat, and the parent is found by the * `In-Reply-To` header — a value every recipient of the agent's reply holds. Against @@ -97,7 +98,7 @@ async function seedWorkspace(ws: InboxWorkspace): Promise { createdAt: now, updatedAt: now, }) - await db.insert(workspace).values({ + await insertWorkspaceFixture(db, { id: ws.id, name: 'Inbox thread scope fixture', ownerId: ws.ownerId, @@ -224,7 +225,7 @@ describe('inbox reply threading stays inside the receiving workspace', () => { afterAll(async () => { const ownerIds = [victim.ownerId, attacker.ownerId] await db.delete(permissions).where(inArray(permissions.userId, ownerIds)) - await db.delete(workspace).where(inArray(workspace.id, [victim.id, attacker.id])) + await deleteWorkspaceFixture(db, inArray(workspace.id, [victim.id, attacker.id])) await db.delete(user).where(inArray(user.id, ownerIds)) }) diff --git a/apps/sim/background/cleanup-soft-deletes.integration.ts b/apps/sim/background/cleanup-soft-deletes.integration.ts index 416377adac8..c5ca5a51849 100644 --- a/apps/sim/background/cleanup-soft-deletes.integration.ts +++ b/apps/sim/background/cleanup-soft-deletes.integration.ts @@ -43,18 +43,28 @@ async function seedRelease(deletedAt: string | null, sizeBytes: number, patch: n describe('Retention cleanup of deleted owned bodies', () => { beforeAll(async () => { - await control`INSERT INTO "user" (id, name, email, email_verified, created_at, updated_at) - VALUES (${userId}, 'Test cleanup fixture', ${`${userId}@example.test`}, true, now(), now())` - await control`INSERT INTO user_stats (id, user_id, storage_used_bytes) - VALUES (${generateId()}, ${userId}, 100)` - await control`INSERT INTO workspace (id, name, owner_id, billed_account_user_id, storage_used_bytes) - VALUES (${workspaceId}, 'Test cleanup fixtures', ${userId}, ${userId}, 100)` + await control.begin(async (tx) => { + await tx`INSERT INTO "user" (id, name, email, email_verified, created_at, updated_at) + VALUES (${userId}, 'Test cleanup fixture', ${`${userId}@example.test`}, true, now(), now())` + await tx`INSERT INTO user_stats (id, user_id, storage_used_bytes) + VALUES (${generateId()}, ${userId}, 100)` + await tx`INSERT INTO project (id, name, owner_id) + VALUES (${workspaceId}, 'Test cleanup project', ${userId})` + await tx`INSERT INTO workspace (id, project_id, name, owner_id, billed_account_user_id, storage_used_bytes) + VALUES (${workspaceId}, ${workspaceId}, 'Test cleanup fixtures', ${userId}, ${userId}, 100)` + }) }) afterAll(async () => { - await control`DELETE FROM workspace WHERE id = ${workspaceId}` - await control`DELETE FROM "user" WHERE id = ${userId}` - await control.end() + try { + await control.begin(async (tx) => { + await tx`DELETE FROM workspace WHERE id = ${workspaceId}` + await tx`DELETE FROM project WHERE id = ${workspaceId} AND owner_id = ${userId}` + await tx`DELETE FROM "user" WHERE id = ${userId}` + }) + } finally { + await control.end() + } }) it('purges an expired test with its source and releases the source bytes', async () => { diff --git a/apps/sim/background/cleanup-table-row-ttl.integration.ts b/apps/sim/background/cleanup-table-row-ttl.integration.ts index e4d98dbb54f..96c824b7667 100644 --- a/apps/sim/background/cleanup-table-row-ttl.integration.ts +++ b/apps/sim/background/cleanup-table-row-ttl.integration.ts @@ -109,8 +109,11 @@ describe.skipIf(!migrated)('Expiration with real PostgreSQL transactions', () => beforeAll(async () => { await control`INSERT INTO "user" (id, name, email, email_verified, created_at, updated_at) VALUES (${userId}, 'Expiration integration fixture', ${`${userId}@example.test`}, true, now(), now())` - await control`INSERT INTO workspace (id, name, owner_id, billed_account_user_id) - VALUES (${workspaceId}, 'Expiration integration fixtures', ${userId}, ${userId})` + await control.begin(async (tx) => { + await tx`INSERT INTO project (id, name, owner_id) VALUES (${workspaceId}, 'Fixture project', ${userId})` + await tx`INSERT INTO workspace (id, project_id, name, owner_id, billed_account_user_id) + VALUES (${workspaceId}, ${workspaceId}, 'Expiration integration fixtures', ${userId}, ${userId})` + }) }) beforeEach(async () => { @@ -124,7 +127,10 @@ describe.skipIf(!migrated)('Expiration with real PostgreSQL transactions', () => }) afterAll(async () => { - await control`DELETE FROM workspace WHERE id = ${workspaceId}` + await control.begin(async (tx) => { + await tx`DELETE FROM workspace WHERE id = ${workspaceId}` + await tx`DELETE FROM project WHERE id = ${workspaceId}` + }) await control`DELETE FROM "user" WHERE id = ${userId}` writeFileSync( join(tmpdir(), 'expiration-qa-measurements.json'), diff --git a/apps/sim/ee/access-requests/lib/application/copilot-requests.integration.ts b/apps/sim/ee/access-requests/lib/application/copilot-requests.integration.ts index fc524b01e0c..5680eba9f4d 100644 --- a/apps/sim/ee/access-requests/lib/application/copilot-requests.integration.ts +++ b/apps/sim/ee/access-requests/lib/application/copilot-requests.integration.ts @@ -12,6 +12,7 @@ import { user, workspace, } from '@sim/db/schema' +import { deleteWorkspaceFixture, insertWorkspaceFixture } from '@sim/db/testing/workspace-fixtures' import { envFlagsMock } from '@sim/testing/mocks/env-flags.mock' import { generateId } from '@sim/utils/id' import { and, eq, inArray } from 'drizzle-orm' @@ -92,7 +93,8 @@ describe('chat-delegated workspace access requests', () => { createdAt: now, })) ) - await db.insert(workspace).values( + await insertWorkspaceFixture( + db, [workspaceId, otherWorkspaceId].map((id) => ({ id, name: 'Access request fixture', @@ -138,7 +140,7 @@ describe('chat-delegated workspace access requests', () => { .delete(permissionAccessRequest) .where(eq(permissionAccessRequest.organizationId, organizationId)) await db.delete(auditLog).where(eq(auditLog.workspaceId, workspaceId)) - await db.delete(workspace).where(inArray(workspace.id, [workspaceId, otherWorkspaceId])) + await deleteWorkspaceFixture(db, inArray(workspace.id, [workspaceId, otherWorkspaceId])) await db.delete(organization).where(eq(organization.id, organizationId)) await db.delete(user).where(inArray(user.id, [requesterId, peerId])) }) diff --git a/apps/sim/ee/workspace-forking/lib/create-fork.test.ts b/apps/sim/ee/workspace-forking/lib/create-fork.test.ts index eb6a2dde66e..7baa904052a 100644 --- a/apps/sim/ee/workspace-forking/lib/create-fork.test.ts +++ b/apps/sim/ee/workspace-forking/lib/create-fork.test.ts @@ -140,7 +140,6 @@ function forkParams(selection?: { describe('createFork storage headroom gate', () => { beforeEach(() => { resetDbChainMock() - dbChainMockFns.as.mockReturnValue(workspace) queueTableRows(workspace, [{ projectId: 'project-source' }]) queueTableRows(workspace, [ { project: { id: 'project-source', organizationId: null, archivedAt: null } }, @@ -216,7 +215,6 @@ describe('createFork storage headroom gate', () => { it('refuses when the parent changed organizations after the policy was captured', async () => { resetDbChainMock() - dbChainMockFns.as.mockReturnValue(workspace) queueTableRows(workspace, [{ projectId: 'project-source' }]) queueTableRows(workspace, [ { project: { id: 'project-source', organizationId: null, archivedAt: null } }, @@ -245,7 +243,6 @@ describe('createFork storage headroom gate', () => { */ it('gives the child the source workspace personal API-key and fork-sync policies', async () => { resetDbChainMock() - dbChainMockFns.as.mockReturnValue(workspace) queueTableRows(workspace, [{ projectId: 'project-source' }]) queueTableRows(workspace, [ { project: { id: 'project-source', organizationId: null, archivedAt: null } }, diff --git a/apps/sim/ee/workspace-forking/lib/create-fork.ts b/apps/sim/ee/workspace-forking/lib/create-fork.ts index 0e3e36584d5..bcbcbfa2814 100644 --- a/apps/sim/ee/workspace-forking/lib/create-fork.ts +++ b/apps/sim/ee/workspace-forking/lib/create-fork.ts @@ -1,5 +1,5 @@ import { db } from '@sim/db' -import { permissions, projectWorkspace, workspace } from '@sim/db/schema' +import { permissions, workspace } from '@sim/db/schema' import { createLogger } from '@sim/logger' import type { PermissionType } from '@sim/platform-authz/workspace' import { getErrorMessage } from '@sim/utils/errors' @@ -7,7 +7,6 @@ import { generateId } from '@sim/utils/id' import { and, eq } from 'drizzle-orm' import type { Workspace } from '@/lib/api/contracts/workspaces' import { enqueueOutboxEvent } from '@/lib/core/outbox/service' -import { getProjectMembershipPhase } from '@/lib/projects/environment-source' import { requireForkProject } from '@/lib/projects/membership' import { buildDefaultWorkflowArtifacts } from '@/lib/workflows/defaults' import { insertNewWorkflowRow } from '@/lib/workflows/persistence/new-workflow-row' @@ -211,7 +210,6 @@ export async function createFork(params: CreateForkParams): Promise { - const phase = await getProjectMembershipPhase(tx) await setForkLockTimeout(tx) if (admission) { await lockWorkspaceOperationRequest(tx, admission.workspaceId, admission.requestId) @@ -305,7 +303,7 @@ export async function createFork(params: CreateForkParams): Promise ({ import { memory, memorySecretProvenance, + project, resumeQueue, workflow, workflowExecutionLogs, @@ -326,18 +328,21 @@ describe('Agent memory through PostgreSQL, storage, and provider transports', () memorySecretProvenance, workspaceFiles, workspaceFileSecretProvenance, + project, workspace, workflow, workflowExecutionLogs, resumeQueue, ]) await createTable(table) - await fixture.database.insert(workspace).values({ - id: scope.workspaceId, - name: 'Attachment harness', - ownerId: scope.userId, - billedAccountUserId: scope.userId, - }) + await fixture.database.transaction((tx) => + insertWorkspaceFixture(tx, { + id: scope.workspaceId, + name: 'Attachment harness', + ownerId: scope.userId, + billedAccountUserId: scope.userId, + }) + ) await fixture.database.insert(workflow).values({ id: scope.workflowId, workspaceId: scope.workspaceId, diff --git a/apps/sim/lib/billing/organizations/lock-order.test.ts b/apps/sim/lib/billing/organizations/lock-order.test.ts index 9d155360293..adf7a26fdb9 100644 --- a/apps/sim/lib/billing/organizations/lock-order.test.ts +++ b/apps/sim/lib/billing/organizations/lock-order.test.ts @@ -11,7 +11,7 @@ import { organization, outboxEvent, permissions, - projectMembershipRollout, + project, subscription as subscriptionTable, user, userStats, @@ -183,19 +183,19 @@ describe('workspace payer-change transaction lock ordering', () => { const ops: Array<{ op: 'lock' | 'payer-transfer' | 'update'; table: unknown }> = [] let memberSelectCount = 0 const rowsForTable = (table: unknown, fields?: Record): unknown[] => { - if (table === projectMembershipRollout) return [{ phase: 'column' }] if (table === workspace) { - if (fields?.id === workspace.projectId) return [{ id: null }] + if (fields?.id === workspace.projectId) return [{ id: 'project-1' }] return [ { id: 'workspace-1', - projectId: null, + projectId: 'project-1', billedAccountUserId: 'user-1', organizationId: null, storageUsedBytes: 128, }, ] } + if (table === project) return [{ id: 'project-1', organizationId: null }] if (table === permissions) return [{ userId: 'user-1' }] if (table === member) { memberSelectCount += 1 @@ -220,9 +220,6 @@ describe('workspace payer-change transaction lock ordering', () => { orderBy() { return chain }, - as() { - return table - }, for() { ops.push({ op: 'lock', table }) return chain @@ -235,7 +232,7 @@ describe('workspace payer-change transaction lock ordering', () => { return chain } const tx = { - execute: async () => [{ marker: true, complete: false }], + execute: async () => [{ acquired: true }], select, selectDistinct: select, insert: () => ({ diff --git a/apps/sim/lib/billing/organizations/member-limits.test.ts b/apps/sim/lib/billing/organizations/member-limits.test.ts index 2c89f4a9d71..605f1b745a0 100644 --- a/apps/sim/lib/billing/organizations/member-limits.test.ts +++ b/apps/sim/lib/billing/organizations/member-limits.test.ts @@ -6,7 +6,7 @@ import { queueTableRows, resetDbChainMock, } from '@sim/testing/mocks/database.mock' -import { afterAll, beforeEach, describe, expect, it, vi } from 'vitest' +import { afterAll, afterEach, beforeEach, describe, expect, it, vi } from 'vitest' import { USAGE_LEDGER_STATEMENT_TIMEOUT_MS } from '@/lib/billing/constants' vi.mock('@/lib/billing/core/billing', () => billingCoreMock) @@ -21,9 +21,14 @@ const mockGetOrganizationSubscription = billingCoreMockFns.mockGetOrganizationSu const { eq: mockEq, gte: mockGte, isNull: mockIsNull, lt: mockLt, or: mockOr } = drizzleOrmMock beforeEach(() => { + vi.useFakeTimers({ toFake: ['Date'] }) resetDbChainMock() }) +afterEach(() => { + vi.useRealTimers() +}) + afterAll(() => { resetDbChainMock() }) diff --git a/apps/sim/lib/changelog/repository.integration.ts b/apps/sim/lib/changelog/repository.integration.ts index 789512c931c..53695a644ae 100644 --- a/apps/sim/lib/changelog/repository.integration.ts +++ b/apps/sim/lib/changelog/repository.integration.ts @@ -84,19 +84,29 @@ const change = (deploymentVersionId: string): ChangelogChangeInput => ({ describe('A deployment ships in one release', () => { beforeAll(async () => { - await control`INSERT INTO "user" (id, name, email, email_verified, created_at, updated_at) - VALUES (${userId}, 'Changelog fixture', ${`${userId}@example.test`}, true, now(), now())` - await control`INSERT INTO workspace (id, name, owner_id, billed_account_user_id) - VALUES (${workspaceId}, 'Changelog fixtures', ${userId}, ${userId})` - await control`INSERT INTO workflow (id, user_id, workspace_id, name, last_synced, created_at, updated_at) - VALUES (${workflowId}, ${userId}, ${workspaceId}, 'Fixture workflow', now(), now(), now())` + await control.begin(async (tx) => { + await tx`INSERT INTO "user" (id, name, email, email_verified, created_at, updated_at) + VALUES (${userId}, 'Changelog fixture', ${`${userId}@example.test`}, true, now(), now())` + await tx`INSERT INTO project (id, name, owner_id) + VALUES (${workspaceId}, 'Changelog project', ${userId})` + await tx`INSERT INTO workspace (id, project_id, name, owner_id, billed_account_user_id) + VALUES (${workspaceId}, ${workspaceId}, 'Changelog fixtures', ${userId}, ${userId})` + await tx`INSERT INTO workflow (id, user_id, workspace_id, name, last_synced, created_at, updated_at) + VALUES (${workflowId}, ${userId}, ${workspaceId}, 'Fixture workflow', now(), now(), now())` + }) }) afterAll(async () => { - await control`DELETE FROM changelog_release WHERE workspace_id = ${workspaceId}` - await control`DELETE FROM workspace WHERE id = ${workspaceId}` - await control`DELETE FROM "user" WHERE id = ${userId}` - await control.end() + try { + await control.begin(async (tx) => { + await tx`DELETE FROM changelog_release WHERE workspace_id = ${workspaceId}` + await tx`DELETE FROM workspace WHERE id = ${workspaceId}` + await tx`DELETE FROM project WHERE id = ${workspaceId} AND owner_id = ${userId}` + await tx`DELETE FROM "user" WHERE id = ${userId}` + }) + } finally { + await control.end() + } }) it('rejects a second release claiming a released deployment', async () => { diff --git a/apps/sim/lib/credentials/__integration__/copilot-credential-members.integration.ts b/apps/sim/lib/credentials/__integration__/copilot-credential-members.integration.ts index b1d6a3d3067..be3431a9b9b 100644 --- a/apps/sim/lib/credentials/__integration__/copilot-credential-members.integration.ts +++ b/apps/sim/lib/credentials/__integration__/copilot-credential-members.integration.ts @@ -15,6 +15,7 @@ import { user, workspace, } from '@sim/db/schema' +import { deleteWorkspaceFixture, insertWorkspaceFixture } from '@sim/db/testing/workspace-fixtures' import { envFlagsMock } from '@sim/testing/mocks/env-flags.mock' import { generateId } from '@sim/utils/id' import { and, eq, inArray } from 'drizzle-orm' @@ -105,7 +106,8 @@ describe('chat-delegated credential sharing', () => { createdAt: now, })) ) - await db.insert(workspace).values( + await insertWorkspaceFixture( + db, [workspaceId, otherWorkspaceId].map((id) => ({ id, name: 'Credential sharing fixture', @@ -185,7 +187,7 @@ describe('chat-delegated credential sharing', () => { afterAll(async () => { await db.delete(auditLog).where(eq(auditLog.workspaceId, workspaceId)) - await db.delete(workspace).where(inArray(workspace.id, [workspaceId, otherWorkspaceId])) + await deleteWorkspaceFixture(db, inArray(workspace.id, [workspaceId, otherWorkspaceId])) await db.delete(organization).where(eq(organization.id, organizationId)) await db.delete(user).where(inArray(user.id, userIds)) }) diff --git a/apps/sim/lib/credentials/__integration__/organization-personal-tokens.integration.ts b/apps/sim/lib/credentials/__integration__/organization-personal-tokens.integration.ts index fb1f2ffdd7c..1287fbbd393 100644 --- a/apps/sim/lib/credentials/__integration__/organization-personal-tokens.integration.ts +++ b/apps/sim/lib/credentials/__integration__/organization-personal-tokens.integration.ts @@ -11,6 +11,7 @@ import { user, workspace, } from '@sim/db/schema' +import { deleteWorkspaceFixture, insertWorkspaceFixture } from '@sim/db/testing/workspace-fixtures' import { sha256Hex } from '@sim/security/hash' import { generateId } from '@sim/utils/id' import { and, eq, inArray } from 'drizzle-orm' @@ -106,7 +107,8 @@ describe('organization personal tokens', () => { { id: generateId(), organizationId: ids.org, userId: ids.owner, role: 'member' }, { id: generateId(), organizationId: ids.org, userId: ids.other, role: 'admin' }, ]) - await db.insert(workspace).values( + await insertWorkspaceFixture( + db, [ids.first, ids.second, ids.foreign].map((id) => ({ id, name: 'Token fixture workspace', @@ -187,7 +189,7 @@ describe('organization personal tokens', () => { await db .delete(credentialGroup) .where(inArray(credentialGroup.id, [ids.group, ids.legacyGroup])) - await db.delete(workspace).where(inArray(workspace.id, [ids.first, ids.second, ids.foreign])) + await deleteWorkspaceFixture(db, inArray(workspace.id, [ids.first, ids.second, ids.foreign])) await db.delete(organization).where(inArray(organization.id, [ids.org, ids.foreignOrg])) await db.delete(user).where(inArray(user.id, [ids.owner, ids.other])) }) @@ -287,7 +289,7 @@ describe('organization personal tokens', () => { expect.objectContaining({ id: ids.token, workspaceId: null, organizationId: ids.org }), ]) } - await db.delete(workspace).where(eq(workspace.id, ids.first)) + await deleteWorkspaceFixture(db, eq(workspace.id, ids.first)) await expect(resolve()).resolves.toMatchObject({ accessToken: tokenSecret }) }) diff --git a/apps/sim/lib/credentials/application/credential-sharing.integration.ts b/apps/sim/lib/credentials/application/credential-sharing.integration.ts index a47432367e2..5134670bcfd 100644 --- a/apps/sim/lib/credentials/application/credential-sharing.integration.ts +++ b/apps/sim/lib/credentials/application/credential-sharing.integration.ts @@ -1,3 +1,4 @@ +import { deleteWorkspaceFixture, insertWorkspaceFixture } from '@sim/db/testing/workspace-fixtures' import { generateId } from '@sim/utils/id' import { afterAll, beforeAll, beforeEach, describe, expect, it } from 'vitest' @@ -49,7 +50,7 @@ describe('Credential sharing through user-held API credentials in PostgreSQL', ( role: 'admin', createdAt: new Date(), }) - await db.insert(schema.workspace).values({ + await insertWorkspaceFixture(db, { id: workspaceId, name: 'Sharing test', ownerId: actorId, @@ -85,7 +86,7 @@ describe('Credential sharing through user-held API credentials in PostgreSQL', ( afterAll(async () => { if (!runtime) return const { db, schema, eq, inArray } = runtime - await db.delete(schema.workspace).where(eq(schema.workspace.id, workspaceId)) + await deleteWorkspaceFixture(db, eq(schema.workspace.id, workspaceId)) await db.delete(schema.organization).where(eq(schema.organization.id, organizationId)) await db .delete(schema.user) diff --git a/apps/sim/lib/desktop/application/activity.integration.ts b/apps/sim/lib/desktop/application/activity.integration.ts index 37328eaa26b..9f9eadbdcc9 100644 --- a/apps/sim/lib/desktop/application/activity.integration.ts +++ b/apps/sim/lib/desktop/application/activity.integration.ts @@ -2,6 +2,7 @@ * Background desktop activity against real PostgreSQL and Redis: which of a user's chats a desktop * runs, and whether each is running, waiting on the user's approval, or blocked by an offline desktop. */ +import { deleteWorkspaceFixture, insertWorkspaceFixture } from '@sim/db/testing/workspace-fixtures' import { afterAll, describe, expect, it, vi } from 'vitest' const { redisUrl } = await vi.hoisted(async () => { @@ -42,7 +43,7 @@ describe.runIf(Boolean(redisUrl))('background desktop activity', () => { const deviceIds: string[] = [] afterAll(async () => { - if (workspaceIds.length) await db.delete(workspace).where(inArray(workspace.id, workspaceIds)) + if (workspaceIds.length) await deleteWorkspaceFixture(db, inArray(workspace.id, workspaceIds)) if (deviceIds.length) await db.delete(desktopDevices).where(inArray(desktopDevices.id, deviceIds)) if (userIds.length) await db.delete(user).where(inArray(user.id, userIds)) @@ -65,7 +66,7 @@ describe.runIf(Boolean(redisUrl))('background desktop activity', () => { createdAt: now, updatedAt: now, }) - await db.insert(workspace).values({ + await insertWorkspaceFixture(db, { id: workspaceId, name: 'Desktop activity fixture', ownerId: userId, diff --git a/apps/sim/lib/desktop/application/executor.integration.ts b/apps/sim/lib/desktop/application/executor.integration.ts index 7ef9a70eccf..edaa8ff5cb4 100644 --- a/apps/sim/lib/desktop/application/executor.integration.ts +++ b/apps/sim/lib/desktop/application/executor.integration.ts @@ -4,6 +4,7 @@ * completion. Calls are persisted the way the run loop persists a bound run's desktop calls, and * Stop runs through the real `requestRunStop`. */ +import { deleteWorkspaceFixture, insertWorkspaceFixture } from '@sim/db/testing/workspace-fixtures' import { afterAll, beforeEach, describe, expect, it, vi } from 'vitest' const { redisUrl, presence } = await vi.hoisted(async () => { @@ -87,7 +88,7 @@ describe.runIf(Boolean(redisUrl))('desktop background executor protocol', () => }) const workspaceId = generateId() workspaceIds.set(userId, workspaceId) - await db.insert(workspace).values({ + await insertWorkspaceFixture(db, { id: workspaceId, name: 'Desktop executor fixture', ownerId: userId, @@ -243,7 +244,7 @@ describe.runIf(Boolean(redisUrl))('desktop background executor protocol', () => await db.delete(desktopDevices).where(inArray(desktopDevices.id, deviceIds)) } const workspaces = [...workspaceIds.values()] - if (workspaces.length) await db.delete(workspace).where(inArray(workspace.id, workspaces)) + if (workspaces.length) await deleteWorkspaceFixture(db, inArray(workspace.id, workspaces)) if (userIds.length) await db.delete(user).where(inArray(user.id, userIds)) }) diff --git a/apps/sim/lib/desktop/application/import.integration.ts b/apps/sim/lib/desktop/application/import.integration.ts index b726197731d..54eabf82b48 100644 --- a/apps/sim/lib/desktop/application/import.integration.ts +++ b/apps/sim/lib/desktop/application/import.integration.ts @@ -8,6 +8,7 @@ import { mkdtempSync } from 'node:fs' import { rm } from 'node:fs/promises' import { tmpdir } from 'node:os' import path from 'node:path' +import { deleteWorkspaceFixture, insertWorkspaceFixture } from '@sim/db/testing/workspace-fixtures' import { afterAll, beforeAll, describe, expect, it, vi } from 'vitest' const fixtureStorage = vi.hoisted(() => ({ root: '' })) @@ -52,7 +53,7 @@ describe('desktop imports', () => { if (userIds.length) { // Audit rows outlive their actor (the foreign key sets null), so they go first. await db.delete(auditLog).where(inArray(auditLog.actorId, userIds)) - await db.delete(workspace).where(inArray(workspace.ownerId, userIds)) + await deleteWorkspaceFixture(db, inArray(workspace.ownerId, userIds)) await db.delete(user).where(inArray(user.id, userIds)) } await rm(fixtureStorage.root, { recursive: true, force: true }) @@ -75,7 +76,7 @@ describe('desktop imports', () => { updatedAt: now, }) await db.insert(userStats).values({ id: generateId(), userId }) - await db.insert(workspace).values({ + await insertWorkspaceFixture(db, { id: workspaceId, name: 'Desktop import fixture', ownerId: userId, diff --git a/apps/sim/lib/desktop/executor/bound-turn.integration.ts b/apps/sim/lib/desktop/executor/bound-turn.integration.ts index 6a13a2be313..fe8db90eb05 100644 --- a/apps/sim/lib/desktop/executor/bound-turn.integration.ts +++ b/apps/sim/lib/desktop/executor/bound-turn.integration.ts @@ -5,6 +5,7 @@ * through the real `requestRunStop`, and a waiter that dies is settled by the stale-execution cron. * Deadlines are moved on the row itself, which is all the server reads. */ +import { deleteWorkspaceFixture, insertWorkspaceFixture } from '@sim/db/testing/workspace-fixtures' import { authMock, authMockFns } from '@sim/testing/mocks/auth.mock' import { afterAll, beforeAll, beforeEach, describe, expect, it, vi } from 'vitest' @@ -340,7 +341,7 @@ describe.runIf(Boolean(redisUrl))("a turn bound to a desktop's background execut createdAt: now, updatedAt: now, }) - await db.insert(workspace).values({ + await insertWorkspaceFixture(db, { id: workspaceId, name: 'Bound turn fixture', ownerId: userId, @@ -369,7 +370,7 @@ describe.runIf(Boolean(redisUrl))("a turn bound to a desktop's background execut await db.delete(auditLog).where(inArray(auditLog.resourceId, deviceIds)) await db.delete(desktopDevices).where(inArray(desktopDevices.id, deviceIds)) } - await db.delete(workspace).where(eq(workspace.id, workspaceId)) + await deleteWorkspaceFixture(db, eq(workspace.id, workspaceId)) await db.delete(user).where(eq(user.id, userId)) }) diff --git a/apps/sim/lib/desktop/executor/desktop-call-classifier.integration.ts b/apps/sim/lib/desktop/executor/desktop-call-classifier.integration.ts index 857d0d7ccfb..d945b4fd700 100644 --- a/apps/sim/lib/desktop/executor/desktop-call-classifier.integration.ts +++ b/apps/sim/lib/desktop/executor/desktop-call-classifier.integration.ts @@ -14,6 +14,7 @@ import { user, workspace, } from '@sim/db/schema' +import { deleteWorkspaceFixture, insertWorkspaceFixture } from '@sim/db/testing/workspace-fixtures' import { generateId } from '@sim/utils/id' import { eq, sql } from 'drizzle-orm' import { afterAll, beforeAll, describe, expect, it } from 'vitest' @@ -105,7 +106,7 @@ describe('desktop call classification in SQL and TypeScript', () => { createdAt: now, updatedAt: now, }) - await db.insert(workspace).values({ + await insertWorkspaceFixture(db, { id: workspaceId, name: 'Classifier parity fixture', ownerId: userId, @@ -154,7 +155,7 @@ describe('desktop call classification in SQL and TypeScript', () => { afterAll(async () => { await db.delete(copilotChats).where(eq(copilotChats.id, chatId)) await db.delete(desktopDevices).where(eq(desktopDevices.id, deviceId)) - await db.delete(workspace).where(eq(workspace.id, workspaceId)) + await deleteWorkspaceFixture(db, eq(workspace.id, workspaceId)) await db.delete(user).where(eq(user.id, userId)) }) diff --git a/apps/sim/lib/environment/execution-environment.integration.ts b/apps/sim/lib/environment/execution-environment.integration.ts index b38e792f4a6..8f5e8b45940 100644 --- a/apps/sim/lib/environment/execution-environment.integration.ts +++ b/apps/sim/lib/environment/execution-environment.integration.ts @@ -5,6 +5,7 @@ */ import { db } from '@sim/db' import { environment, permissions, user, workspace, workspaceEnvironment } from '@sim/db/schema' +import { deleteWorkspaceFixture, insertWorkspaceFixture } from '@sim/db/testing/workspace-fixtures' import { generateId } from '@sim/utils/id' import { eq, inArray } from 'drizzle-orm' import { afterAll, beforeAll, describe, expect, it } from 'vitest' @@ -41,7 +42,7 @@ beforeAll(async () => { ...(id === suspended ? { banned: true } : {}), })) ) - await db.insert(workspace).values({ + await insertWorkspaceFixture(db, { id: workspaceId, name: 'Environment', ownerId: owner, @@ -73,7 +74,7 @@ beforeAll(async () => { }) afterAll(async () => { - await db.delete(workspace).where(eq(workspace.id, workspaceId)) + await deleteWorkspaceFixture(db, eq(workspace.id, workspaceId)) await db.delete(user).where(inArray(user.id, userIds)) }) diff --git a/apps/sim/lib/events/pubsub.ts b/apps/sim/lib/events/pubsub.ts index 9550ae4db8d..345a082106a 100644 --- a/apps/sim/lib/events/pubsub.ts +++ b/apps/sim/lib/events/pubsub.ts @@ -162,9 +162,16 @@ class LocalPubSubChannel implements PubSubChannel { } subscribe(handler: (event: T) => void): () => void { - this.emitter.on(this.config.channel, handler) + const isolatedHandler = (event: T) => { + try { + handler(event) + } catch (error) { + logger.error(`Error in ${this.config.label} handler:`, error) + } + } + this.emitter.on(this.config.channel, isolatedHandler) return () => { - this.emitter.off(this.config.channel, handler) + this.emitter.off(this.config.channel, isolatedHandler) } } diff --git a/apps/sim/lib/invitations/__integration__/copilot-workspace-invitations.integration.ts b/apps/sim/lib/invitations/__integration__/copilot-workspace-invitations.integration.ts index 86aa13e33db..fc5908b0024 100644 --- a/apps/sim/lib/invitations/__integration__/copilot-workspace-invitations.integration.ts +++ b/apps/sim/lib/invitations/__integration__/copilot-workspace-invitations.integration.ts @@ -11,6 +11,7 @@ import { user, workspace, } from '@sim/db/schema' +import { deleteWorkspaceFixture, insertWorkspaceFixture } from '@sim/db/testing/workspace-fixtures' import { generateId } from '@sim/utils/id' import { and, eq, inArray } from 'drizzle-orm' import { afterAll, beforeAll, describe, expect, it, vi } from 'vitest' @@ -86,7 +87,7 @@ describe('chat-delegated workspace invitations', () => { createdAt: now, })) ) - await db.insert(workspace).values({ + await insertWorkspaceFixture(db, { id: workspaceId, name: 'Invitation fixture', ownerId: adminId, @@ -131,7 +132,7 @@ describe('chat-delegated workspace invitations', () => { .delete(invitation) .where(inArray(invitation.email, [newInvitee, pendingInvitee, writerInvitee])) await db.delete(auditLog).where(eq(auditLog.workspaceId, workspaceId)) - await db.delete(workspace).where(eq(workspace.id, workspaceId)) + await deleteWorkspaceFixture(db, eq(workspace.id, workspaceId)) await db.delete(organization).where(eq(organization.id, organizationId)) await db.delete(user).where(inArray(user.id, [adminId, writerId])) }) diff --git a/apps/sim/lib/knowledge/__integration__/application-acl.integration.ts b/apps/sim/lib/knowledge/__integration__/application-acl.integration.ts index 814e7f5270e..358e2fed5ac 100644 --- a/apps/sim/lib/knowledge/__integration__/application-acl.integration.ts +++ b/apps/sim/lib/knowledge/__integration__/application-acl.integration.ts @@ -25,6 +25,7 @@ import { user, workspace, } from '@sim/db/schema' +import { deleteWorkspaceFixture } from '@sim/db/testing/workspace-fixtures' import { generateId } from '@sim/utils/id' import { and, eq } from 'drizzle-orm' import { afterAll, beforeAll, describe, expect, it, vi } from 'vitest' @@ -278,7 +279,7 @@ describe('indexed source content through real application access', () => { CONFLUENCE_CLIENT_ID: previousConfluenceClient.id, CONFLUENCE_CLIENT_SECRET: previousConfluenceClient.secret, }) - await db.delete(workspace).where(eq(workspace.id, workspaceId)) + await deleteWorkspaceFixture(db, eq(workspace.id, workspaceId)) await db.delete(user).where(eq(user.id, aliceId)) await db.delete(user).where(eq(user.id, bobId)) await rm(fixtures.storageRoot, { recursive: true, force: true }) diff --git a/apps/sim/lib/knowledge/__integration__/coda-live.integration.ts b/apps/sim/lib/knowledge/__integration__/coda-live.integration.ts index e40cdb28090..3b12fe7cb0b 100644 --- a/apps/sim/lib/knowledge/__integration__/coda-live.integration.ts +++ b/apps/sim/lib/knowledge/__integration__/coda-live.integration.ts @@ -17,6 +17,7 @@ import { user, workspace, } from '@sim/db/schema' +import { deleteWorkspaceFixture } from '@sim/db/testing/workspace-fixtures' import { sleep } from '@sim/utils/helpers' import { generateId } from '@sim/utils/id' import { serializeSignedCookie } from 'better-call' @@ -225,7 +226,7 @@ describe.skipIf(!tokenPath || !fixturePath || !secondEmail)( .where(eq(document.knowledgeBaseId, ids.knowledgeBaseId)) for (const row of rows) if (row.storageKey) await deleteFile({ key: row.storageKey, context: 'knowledge-base' }) - await db.delete(workspace).where(eq(workspace.id, ids.workspaceId)) + await deleteWorkspaceFixture(db, eq(workspace.id, ids.workspaceId)) await db.delete(organization).where(eq(organization.id, ids.organizationId)) await db.delete(user).where(eq(user.id, ids.aliceId)) await db.delete(user).where(eq(user.id, ids.bobId)) diff --git a/apps/sim/lib/knowledge/__integration__/confluence-enrollment.integration.ts b/apps/sim/lib/knowledge/__integration__/confluence-enrollment.integration.ts index d9709792656..d60d0e07bb8 100644 --- a/apps/sim/lib/knowledge/__integration__/confluence-enrollment.integration.ts +++ b/apps/sim/lib/knowledge/__integration__/confluence-enrollment.integration.ts @@ -15,6 +15,7 @@ import { user, workspace, } from '@sim/db/schema' +import { deleteWorkspaceFixture } from '@sim/db/testing/workspace-fixtures' import { generateId } from '@sim/utils/id' import { and, eq } from 'drizzle-orm' import { afterAll, beforeAll, beforeEach, describe, expect, it } from 'vitest' @@ -99,7 +100,7 @@ describe('Confluence mirrored-identity self-enrollment', () => { CONFLUENCE_CLIENT_SECRET: previousClient.secret, }) for (const fixture of [ids, foreign]) { - await db.delete(workspace).where(eq(workspace.id, fixture.workspaceId)) + await deleteWorkspaceFixture(db, eq(workspace.id, fixture.workspaceId)) await db.delete(user).where(eq(user.id, fixture.aliceId)) await db.delete(user).where(eq(user.id, fixture.bobId)) } diff --git a/apps/sim/lib/knowledge/__integration__/confluence-identity.integration.ts b/apps/sim/lib/knowledge/__integration__/confluence-identity.integration.ts index 3cb48da93be..5393141cc9a 100644 --- a/apps/sim/lib/knowledge/__integration__/confluence-identity.integration.ts +++ b/apps/sim/lib/knowledge/__integration__/confluence-identity.integration.ts @@ -9,6 +9,7 @@ import { user, workspace, } from '@sim/db/schema' +import { deleteWorkspaceFixture } from '@sim/db/testing/workspace-fixtures' import { generateId } from '@sim/utils/id' import { eq } from 'drizzle-orm' import { afterAll, beforeAll, describe, expect, it, vi } from 'vitest' @@ -181,7 +182,7 @@ describe('Confluence identities with hidden directory email', () => { afterAll(async () => { vi.unstubAllGlobals() Object.assign(env, previousConfluenceClient) - await db.delete(workspace).where(eq(workspace.id, ids.workspaceId)) + await deleteWorkspaceFixture(db, eq(workspace.id, ids.workspaceId)) await db.delete(user).where(eq(user.id, ids.aliceId)) await db.delete(user).where(eq(user.id, ids.bobId)) await db.$client.end() diff --git a/apps/sim/lib/knowledge/__integration__/connector-deferral.integration.ts b/apps/sim/lib/knowledge/__integration__/connector-deferral.integration.ts index 987a117173e..74bc6686252 100644 --- a/apps/sim/lib/knowledge/__integration__/connector-deferral.integration.ts +++ b/apps/sim/lib/knowledge/__integration__/connector-deferral.integration.ts @@ -8,6 +8,7 @@ import { user, workspace, } from '@sim/db/schema' +import { deleteWorkspaceFixture } from '@sim/db/testing/workspace-fixtures' import { generateId } from '@sim/utils/id' import { eq, inArray } from 'drizzle-orm' import { afterAll, beforeAll, beforeEach, describe, expect, it, vi } from 'vitest' @@ -88,7 +89,7 @@ describe('durable connector capacity deferrals', () => { }) afterAll(async () => { await db.delete(knowledgeBase).where(eq(knowledgeBase.id, ids.knowledgeBaseId)) - await db.delete(workspace).where(eq(workspace.id, ids.workspaceId)) + await deleteWorkspaceFixture(db, eq(workspace.id, ids.workspaceId)) await db.delete(user).where(inArray(user.id, [ids.aliceId, ids.bobId])) vi.restoreAllMocks() vi.unstubAllGlobals() diff --git a/apps/sim/lib/knowledge/__integration__/connector-lease-pages.integration.ts b/apps/sim/lib/knowledge/__integration__/connector-lease-pages.integration.ts index 5d43aa41e3b..83947ca9007 100644 --- a/apps/sim/lib/knowledge/__integration__/connector-lease-pages.integration.ts +++ b/apps/sim/lib/knowledge/__integration__/connector-lease-pages.integration.ts @@ -19,6 +19,7 @@ import { } from '@sim/db/schema' import { installProjectionSourceAcl } from '@sim/db/script-migrations/0021_embedding_search_connector' import { installKnowledgeProjectionAsync } from '@sim/db/script-migrations/0024_knowledge_projection_async' +import { deleteWorkspaceFixture } from '@sim/db/testing/workspace-fixtures' import { generateId } from '@sim/utils/id' import { and, eq, inArray, sql } from 'drizzle-orm' import postgres from 'postgres' @@ -141,7 +142,7 @@ describe('connector lease ACL pages in PostgreSQL', () => { await db.execute(sql`DROP TRIGGER IF EXISTS fail_after_acl_writes ON document`) await db.execute(sql`DELETE FROM lease_page_acl_writes`) await db.execute(sql`DELETE FROM lease_page_projection_writes`) - await db.delete(workspace).where(eq(workspace.id, ids.workspaceId)) + await deleteWorkspaceFixture(db, eq(workspace.id, ids.workspaceId)) await db.delete(organization).where(eq(organization.id, ids.organizationId)) await db.delete(user).where(inArray(user.id, [ids.aliceId, ids.bobId])) }) diff --git a/apps/sim/lib/knowledge/__integration__/connector-lifecycle-locks.integration.ts b/apps/sim/lib/knowledge/__integration__/connector-lifecycle-locks.integration.ts index 6ed755240fe..930026476b3 100644 --- a/apps/sim/lib/knowledge/__integration__/connector-lifecycle-locks.integration.ts +++ b/apps/sim/lib/knowledge/__integration__/connector-lifecycle-locks.integration.ts @@ -9,6 +9,7 @@ import { user, workspace, } from '@sim/db/schema' +import { deleteWorkspaceFixture } from '@sim/db/testing/workspace-fixtures' import { createDeferred } from '@sim/testing/helpers/deferred' import { generateId } from '@sim/utils/id' import { and, eq, inArray, isNotNull, sql } from 'drizzle-orm' @@ -90,7 +91,7 @@ describe('source lifecycle KB guards', () => { }) afterEach(async () => { vi.restoreAllMocks() - await db.delete(workspace).where(eq(workspace.id, ids.workspaceId)) + await deleteWorkspaceFixture(db, eq(workspace.id, ids.workspaceId)) await db.delete(organization).where(eq(organization.id, ids.organizationId)) await db.delete(user).where(inArray(user.id, [ids.aliceId, ids.bobId])) }) diff --git a/apps/sim/lib/knowledge/__integration__/connector-partition-work.integration.ts b/apps/sim/lib/knowledge/__integration__/connector-partition-work.integration.ts index 1b6d75b2985..935f24ac898 100644 --- a/apps/sim/lib/knowledge/__integration__/connector-partition-work.integration.ts +++ b/apps/sim/lib/knowledge/__integration__/connector-partition-work.integration.ts @@ -7,6 +7,7 @@ import { user, workspace, } from '@sim/db/schema' +import { deleteWorkspaceFixture } from '@sim/db/testing/workspace-fixtures' import { and, eq, inArray, sql } from 'drizzle-orm' import { afterAll, afterEach, beforeEach, describe, expect, it, vi } from 'vitest' import { z } from 'zod' @@ -66,7 +67,7 @@ describe('Connector partition checkpoint storage', () => { }) afterEach(async () => { vi.unstubAllEnvs() - await db.delete(workspace).where(eq(workspace.id, owner.workspaceId)) + await deleteWorkspaceFixture(db, eq(workspace.id, owner.workspaceId)) await db.delete(organization).where(eq(organization.id, owner.organizationId)) await db.delete(user).where(inArray(user.id, [owner.aliceId, owner.bobId])) }) diff --git a/apps/sim/lib/knowledge/__integration__/connector-permissions.integration.ts b/apps/sim/lib/knowledge/__integration__/connector-permissions.integration.ts index 5788084bcfa..05647c8a586 100644 --- a/apps/sim/lib/knowledge/__integration__/connector-permissions.integration.ts +++ b/apps/sim/lib/knowledge/__integration__/connector-permissions.integration.ts @@ -1,6 +1,7 @@ /** Real PostgreSQL coverage for provider-independent snapshots and administrator-managed grants. */ import { db } from '@sim/db' import { knowledgeConnector, organization, user, workspace } from '@sim/db/schema' +import { deleteWorkspaceFixture } from '@sim/db/testing/workspace-fixtures' import { eq, inArray } from 'drizzle-orm' import { afterAll, beforeAll, describe, expect, it } from 'vitest' import { seedKnowledgeAclFixture } from '@/lib/knowledge/__integration__/seed-source-access-fixture' @@ -21,7 +22,7 @@ describe('connector-owned permission storage', () => { }) afterAll(async () => { for (const owner of owners) { - await db.delete(workspace).where(eq(workspace.id, owner.workspaceId)) + await deleteWorkspaceFixture(db, eq(workspace.id, owner.workspaceId)) await db.delete(organization).where(eq(organization.id, owner.organizationId)) await db.delete(user).where(inArray(user.id, [owner.aliceId, owner.bobId])) } diff --git a/apps/sim/lib/knowledge/__integration__/connector-persistence-regressions.integration.ts b/apps/sim/lib/knowledge/__integration__/connector-persistence-regressions.integration.ts index e9ead5863f8..5e4a222d58d 100644 --- a/apps/sim/lib/knowledge/__integration__/connector-persistence-regressions.integration.ts +++ b/apps/sim/lib/knowledge/__integration__/connector-persistence-regressions.integration.ts @@ -9,6 +9,7 @@ import { user, workspace, } from '@sim/db/schema' +import { deleteWorkspaceFixture } from '@sim/db/testing/workspace-fixtures' import { generateId } from '@sim/utils/id' import { eq, inArray } from 'drizzle-orm' import { NextRequest } from 'next/server' @@ -52,7 +53,7 @@ describe('connector source metadata cannot break persistence or API projection', afterAll(async () => { await db.delete(knowledgeBase).where(eq(knowledgeBase.id, ids.knowledgeBaseId)) - await db.delete(workspace).where(eq(workspace.id, ids.workspaceId)) + await deleteWorkspaceFixture(db, eq(workspace.id, ids.workspaceId)) await db.delete(organization).where(eq(organization.id, ids.organizationId)) await db.delete(user).where(inArray(user.id, [ids.aliceId, ids.bobId])) await db.$client.end() diff --git a/apps/sim/lib/knowledge/__integration__/connector-save-concurrency.integration.ts b/apps/sim/lib/knowledge/__integration__/connector-save-concurrency.integration.ts index 63d74b6e724..300a146f850 100644 --- a/apps/sim/lib/knowledge/__integration__/connector-save-concurrency.integration.ts +++ b/apps/sim/lib/knowledge/__integration__/connector-save-concurrency.integration.ts @@ -14,6 +14,7 @@ import { user, workspace, } from '@sim/db/schema' +import { deleteWorkspaceFixture } from '@sim/db/testing/workspace-fixtures' import { generateId } from '@sim/utils/id' import { and, eq, inArray, isNotNull, isNull, sql } from 'drizzle-orm' import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, it, vi } from 'vitest' @@ -103,7 +104,7 @@ describe('independent connector saves in one knowledge base', () => { await db .delete(outboxEvent) .where(sql`${outboxEvent.payload}->>'workspaceId' = ${ids.workspaceId}`) - await db.delete(workspace).where(eq(workspace.id, ids.workspaceId)) + await deleteWorkspaceFixture(db, eq(workspace.id, ids.workspaceId)) await db.delete(organization).where(eq(organization.id, ids.organizationId)) await db.delete(user).where(inArray(user.id, [ids.aliceId, ids.bobId])) }) diff --git a/apps/sim/lib/knowledge/__integration__/connector-upload.integration.ts b/apps/sim/lib/knowledge/__integration__/connector-upload.integration.ts index b8940b35ff0..a0aace449cc 100644 --- a/apps/sim/lib/knowledge/__integration__/connector-upload.integration.ts +++ b/apps/sim/lib/knowledge/__integration__/connector-upload.integration.ts @@ -13,6 +13,7 @@ import { workspace, workspaceFiles, } from '@sim/db/schema' +import { deleteWorkspaceFixture } from '@sim/db/testing/workspace-fixtures' import { generateId } from '@sim/utils/id' import { eq, inArray, sql } from 'drizzle-orm' import { afterAll, beforeAll, describe, expect, it, vi } from 'vitest' @@ -52,7 +53,7 @@ describe('connector upload crash recovery', () => { vi.restoreAllMocks() if (events.length) await db.delete(outboxEvent).where(inArray(outboxEvent.id, events)) await db.delete(knowledgeBase).where(eq(knowledgeBase.id, ids.knowledgeBaseId)) - await db.delete(workspace).where(eq(workspace.id, ids.workspaceId)) + await deleteWorkspaceFixture(db, eq(workspace.id, ids.workspaceId)) await db.delete(organization).where(eq(organization.id, ids.organizationId)) await db.delete(user).where(inArray(user.id, [ids.aliceId, ids.bobId])) await rm(fixtureStorage.root, { recursive: true, force: true }) diff --git a/apps/sim/lib/knowledge/__integration__/directory-sync.integration.ts b/apps/sim/lib/knowledge/__integration__/directory-sync.integration.ts index 357cf09fdb0..dd7eabe1f87 100644 --- a/apps/sim/lib/knowledge/__integration__/directory-sync.integration.ts +++ b/apps/sim/lib/knowledge/__integration__/directory-sync.integration.ts @@ -13,6 +13,7 @@ import { user, workspace, } from '@sim/db/schema' +import { deleteWorkspaceFixture } from '@sim/db/testing/workspace-fixtures' import { createMockRequest } from '@sim/testing' import { generateId } from '@sim/utils/id' import { and, eq, inArray, sql } from 'drizzle-orm' @@ -125,7 +126,7 @@ describe('directory failure visibility in PostgreSQL', () => { .where(eq(knowledgeExternalGroup.workspaceId, ids.workspaceId)) }) afterAll(async () => { - await db.delete(workspace).where(eq(workspace.id, ids.workspaceId)) + await deleteWorkspaceFixture(db, eq(workspace.id, ids.workspaceId)) await db.delete(user).where(eq(user.id, ids.aliceId)) await db.delete(user).where(eq(user.id, ids.bobId)) await db.$client.end() diff --git a/apps/sim/lib/knowledge/__integration__/dormant-processing-recovery.integration.ts b/apps/sim/lib/knowledge/__integration__/dormant-processing-recovery.integration.ts index 45de8a58344..a4257fdd310 100644 --- a/apps/sim/lib/knowledge/__integration__/dormant-processing-recovery.integration.ts +++ b/apps/sim/lib/knowledge/__integration__/dormant-processing-recovery.integration.ts @@ -18,6 +18,7 @@ import { user, workspace, } from '@sim/db/schema' +import { deleteWorkspaceFixture } from '@sim/db/testing/workspace-fixtures' import { generateId } from '@sim/utils/id' import { and, eq, inArray, sql } from 'drizzle-orm' import { afterAll, beforeAll, describe, expect, it, vi } from 'vitest' @@ -102,7 +103,7 @@ afterAll(async () => { .delete(outboxEvent) .where(sql`${outboxEvent.payload}->>'knowledgeBaseId' = ${ids.knowledgeBaseId}`) await db.delete(knowledgeBase).where(eq(knowledgeBase.id, ids.knowledgeBaseId)) - await db.delete(workspace).where(eq(workspace.id, ids.workspaceId)) + await deleteWorkspaceFixture(db, eq(workspace.id, ids.workspaceId)) await db.delete(organization).where(eq(organization.id, ids.organizationId)) await db.delete(user).where(inArray(user.id, [ids.aliceId, ids.bobId])) } diff --git a/apps/sim/lib/knowledge/__integration__/dormant-search-processing.integration.ts b/apps/sim/lib/knowledge/__integration__/dormant-search-processing.integration.ts index 0ed419014ed..2162bd369b8 100644 --- a/apps/sim/lib/knowledge/__integration__/dormant-search-processing.integration.ts +++ b/apps/sim/lib/knowledge/__integration__/dormant-search-processing.integration.ts @@ -1,5 +1,6 @@ import { db } from '@sim/db' import { document, knowledgeBase, organization, user, workspace } from '@sim/db/schema' +import { deleteWorkspaceFixture } from '@sim/db/testing/workspace-fixtures' import { generateId } from '@sim/utils/id' import { eq, inArray } from 'drizzle-orm' import { afterAll, beforeAll, describe, expect, it } from 'vitest' @@ -38,7 +39,7 @@ describe('dormant Search document processing', () => { afterAll(async () => { await db.delete(knowledgeBase).where(eq(knowledgeBase.id, ids.knowledgeBaseId)) - await db.delete(workspace).where(eq(workspace.id, ids.workspaceId)) + await deleteWorkspaceFixture(db, eq(workspace.id, ids.workspaceId)) await db.delete(organization).where(eq(organization.id, ids.organizationId)) await db.delete(user).where(inArray(user.id, [ids.aliceId, ids.bobId])) }) diff --git a/apps/sim/lib/knowledge/__integration__/embedding-insert-batches.integration.ts b/apps/sim/lib/knowledge/__integration__/embedding-insert-batches.integration.ts index 7c47768857e..b73c367f945 100644 --- a/apps/sim/lib/knowledge/__integration__/embedding-insert-batches.integration.ts +++ b/apps/sim/lib/knowledge/__integration__/embedding-insert-batches.integration.ts @@ -13,6 +13,7 @@ import { user, workspace, } from '@sim/db/schema' +import { deleteWorkspaceFixture } from '@sim/db/testing/workspace-fixtures' import { generateId } from '@sim/utils/id' import { eq, inArray } from 'drizzle-orm' import { afterAll, beforeAll, describe, expect, it, vi } from 'vitest' @@ -57,7 +58,7 @@ describe('bounded embedding insert transactions', () => { await db.$client.unsafe(`DROP TRIGGER IF EXISTS ${triggerName} ON embedding`) await db.$client.unsafe(`DROP FUNCTION IF EXISTS ${triggerName}()`) await db.delete(knowledgeBase).where(eq(knowledgeBase.id, ids.knowledgeBaseId)) - await db.delete(workspace).where(eq(workspace.id, ids.workspaceId)) + await deleteWorkspaceFixture(db, eq(workspace.id, ids.workspaceId)) await db.delete(organization).where(eq(organization.id, ids.organizationId)) await db.delete(user).where(inArray(user.id, [ids.aliceId, ids.bobId])) await rm(fixtures.root, { recursive: true, force: true }) diff --git a/apps/sim/lib/knowledge/__integration__/embedding-processing-recovery.integration.ts b/apps/sim/lib/knowledge/__integration__/embedding-processing-recovery.integration.ts index 777bf13656c..bdc92a30ea3 100644 --- a/apps/sim/lib/knowledge/__integration__/embedding-processing-recovery.integration.ts +++ b/apps/sim/lib/knowledge/__integration__/embedding-processing-recovery.integration.ts @@ -14,6 +14,7 @@ import { user, workspace, } from '@sim/db/schema' +import { deleteWorkspaceFixture } from '@sim/db/testing/workspace-fixtures' import { generateId } from '@sim/utils/id' import { and, eq, inArray } from 'drizzle-orm' import { afterAll, beforeAll, describe, expect, it, vi } from 'vitest' @@ -72,7 +73,7 @@ describe('embedding progress survives a processing slice', () => { Object.assign(env, previous) if (events.length) await db.delete(outboxEvent).where(inArray(outboxEvent.id, events)) await db.delete(knowledgeBase).where(eq(knowledgeBase.id, ids.knowledgeBaseId)) - await db.delete(workspace).where(eq(workspace.id, ids.workspaceId)) + await deleteWorkspaceFixture(db, eq(workspace.id, ids.workspaceId)) await db.delete(organization).where(eq(organization.id, ids.organizationId)) await db.delete(user).where(inArray(user.id, [ids.aliceId, ids.bobId])) await rm(fixtureStorage.root, { recursive: true, force: true }) diff --git a/apps/sim/lib/knowledge/__integration__/excluded-member-documents.integration.ts b/apps/sim/lib/knowledge/__integration__/excluded-member-documents.integration.ts index 79e4b2a111e..2082c8467da 100644 --- a/apps/sim/lib/knowledge/__integration__/excluded-member-documents.integration.ts +++ b/apps/sim/lib/knowledge/__integration__/excluded-member-documents.integration.ts @@ -11,6 +11,7 @@ import { user, workspace, } from '@sim/db/schema' +import { deleteWorkspaceFixture } from '@sim/db/testing/workspace-fixtures' import { generateId } from '@sim/utils/id' import { eq, inArray } from 'drizzle-orm' import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, it, vi } from 'vitest' @@ -183,7 +184,7 @@ describe('excluded member documents retain current source authorization', () => }) afterEach(async () => { - await db.delete(workspace).where(eq(workspace.id, ids.workspaceId)) + await deleteWorkspaceFixture(db, eq(workspace.id, ids.workspaceId)) await db.delete(organization).where(eq(organization.id, ids.organizationId)) await db.delete(user).where(inArray(user.id, [ids.aliceId, ids.bobId])) }) diff --git a/apps/sim/lib/knowledge/__integration__/execution-archive-provenance.integration.ts b/apps/sim/lib/knowledge/__integration__/execution-archive-provenance.integration.ts index 3757c597836..3f0fd094a10 100644 --- a/apps/sim/lib/knowledge/__integration__/execution-archive-provenance.integration.ts +++ b/apps/sim/lib/knowledge/__integration__/execution-archive-provenance.integration.ts @@ -16,6 +16,7 @@ import { workspace, workspaceFiles, } from '@sim/db/schema' +import { deleteWorkspaceFixture } from '@sim/db/testing/workspace-fixtures' import { remoteSandboxMock, remoteSandboxMockFns } from '@sim/testing/mocks/remote-sandbox.mock' import { generateId } from '@sim/utils/id' import { eq, inArray, sql } from 'drizzle-orm' @@ -239,7 +240,7 @@ afterAll(async () => { for (const ids of fixtures) { await db.delete(auditLog).where(eq(auditLog.workspaceId, ids.workspaceId)) await db.delete(knowledgeBase).where(eq(knowledgeBase.id, ids.knowledgeBaseId)) - await db.delete(workspace).where(eq(workspace.id, ids.workspaceId)) + await deleteWorkspaceFixture(db, eq(workspace.id, ids.workspaceId)) await db.delete(organization).where(eq(organization.id, ids.organizationId)) await db.delete(user).where(inArray(user.id, [ids.aliceId, ids.bobId])) } diff --git a/apps/sim/lib/knowledge/__integration__/external-file-provenance.integration.ts b/apps/sim/lib/knowledge/__integration__/external-file-provenance.integration.ts index 39c7192f9d2..5eff2d72592 100644 --- a/apps/sim/lib/knowledge/__integration__/external-file-provenance.integration.ts +++ b/apps/sim/lib/knowledge/__integration__/external-file-provenance.integration.ts @@ -5,6 +5,7 @@ import { tmpdir } from 'node:os' import path from 'node:path' import { db } from '@sim/db' import { knowledgeBase, organization, user, workspace, workspaceFiles } from '@sim/db/schema' +import { deleteWorkspaceFixture } from '@sim/db/testing/workspace-fixtures' import { generateId } from '@sim/utils/id' import { isPlainRecord } from '@sim/utils/object' import { eq, inArray } from 'drizzle-orm' @@ -132,7 +133,7 @@ afterAll(async () => { validateUrlSpy.mockRestore() for (const ids of fixtures) { await db.delete(knowledgeBase).where(eq(knowledgeBase.id, ids.knowledgeBaseId)) - await db.delete(workspace).where(eq(workspace.id, ids.workspaceId)) + await deleteWorkspaceFixture(db, eq(workspace.id, ids.workspaceId)) await db.delete(organization).where(eq(organization.id, ids.organizationId)) await db.delete(user).where(inArray(user.id, [ids.aliceId, ids.bobId])) } diff --git a/apps/sim/lib/knowledge/__integration__/filtered-search.integration.ts b/apps/sim/lib/knowledge/__integration__/filtered-search.integration.ts index e08292154f5..e54b80a29c1 100644 --- a/apps/sim/lib/knowledge/__integration__/filtered-search.integration.ts +++ b/apps/sim/lib/knowledge/__integration__/filtered-search.integration.ts @@ -9,6 +9,7 @@ import { user, workspace, } from '@sim/db/schema' +import { deleteWorkspaceFixture } from '@sim/db/testing/workspace-fixtures' import { generateId } from '@sim/utils/id' import { and, eq } from 'drizzle-orm' import { afterAll, beforeAll, describe, expect, it } from 'vitest' @@ -123,7 +124,7 @@ describe.each([384, 768, 1024, 1536, 3072] as const)( }) afterAll(async () => { - await db.delete(workspace).where(eq(workspace.id, ids.workspaceId)) + await deleteWorkspaceFixture(db, eq(workspace.id, ids.workspaceId)) await db.delete(user).where(eq(user.id, ids.aliceId)) await db.delete(user).where(eq(user.id, ids.bobId)) }) diff --git a/apps/sim/lib/knowledge/__integration__/github-member.integration.ts b/apps/sim/lib/knowledge/__integration__/github-member.integration.ts index 7829bd571eb..03dcd80b13d 100644 --- a/apps/sim/lib/knowledge/__integration__/github-member.integration.ts +++ b/apps/sim/lib/knowledge/__integration__/github-member.integration.ts @@ -1,3 +1,4 @@ +import { deleteWorkspaceFixture } from '@sim/db/testing/workspace-fixtures' /** * Fixture-backed GitHub API with real PostgreSQL, credential policy/token resolution, * connector registry, member sync, storage, chunking, and application authorization. @@ -549,7 +550,7 @@ describe('fixture-backed GitHub member search in PostgreSQL', () => { ) ) ) - await db.delete(workspace).where(eq(workspace.id, ids.workspaceId)) + await deleteWorkspaceFixture(db, eq(workspace.id, ids.workspaceId)) await db.delete(organization).where(eq(organization.id, ids.organizationId)) await db.delete(user).where(inArray(user.id, [ids.aliceId, ids.bobId])) vi.restoreAllMocks() diff --git a/apps/sim/lib/knowledge/__integration__/gitlab-federated.integration.ts b/apps/sim/lib/knowledge/__integration__/gitlab-federated.integration.ts index ac33889af41..b217bc67d93 100644 --- a/apps/sim/lib/knowledge/__integration__/gitlab-federated.integration.ts +++ b/apps/sim/lib/knowledge/__integration__/gitlab-federated.integration.ts @@ -19,6 +19,7 @@ import { user, workspace, } from '@sim/db/schema' +import { deleteWorkspaceFixture } from '@sim/db/testing/workspace-fixtures' import { sleep } from '@sim/utils/helpers' import { generateId } from '@sim/utils/id' import { isPlainRecord } from '@sim/utils/object' @@ -284,7 +285,7 @@ describe.skipIf(!fixtureFile)('federated self-hosted GitLab Search', () => { })(), (async () => { if (!ids) return - await db.delete(workspace).where(eq(workspace.id, ids.workspaceId)) + await deleteWorkspaceFixture(db, eq(workspace.id, ids.workspaceId)) await db.delete(organization).where(eq(organization.id, ids.organizationId)) await db.delete(user).where(inArray(user.id, [ids.aliceId, ids.bobId, ...simUserIds])) })(), diff --git a/apps/sim/lib/knowledge/__integration__/gitlab-live.integration.ts b/apps/sim/lib/knowledge/__integration__/gitlab-live.integration.ts index 974768e2295..b1b18e3ffff 100644 --- a/apps/sim/lib/knowledge/__integration__/gitlab-live.integration.ts +++ b/apps/sim/lib/knowledge/__integration__/gitlab-live.integration.ts @@ -1,3 +1,4 @@ +import { deleteWorkspaceFixture } from '@sim/db/testing/workspace-fixtures' /** * Opt-in self-hosted GitLab test. GITLAB_LIVE_FIXTURE_FILE contains {url, token, * auditorToken?} for a disposable localhost HTTPS instance. The administrator @@ -430,7 +431,7 @@ describe.skipIf(!fixtureFile)('live self-hosted GitLab ingestion and permission throw error } } - await db.delete(workspace).where(eq(workspace.id, ids.workspaceId)) + await deleteWorkspaceFixture(db, eq(workspace.id, ids.workspaceId)) await db.delete(organization).where(eq(organization.id, ids.organizationId)) await db.delete(user).where(inArray(user.id, [ids.aliceId, ids.bobId, ...extraSimIds])) } diff --git a/apps/sim/lib/knowledge/__integration__/gitlab-workspace.integration.ts b/apps/sim/lib/knowledge/__integration__/gitlab-workspace.integration.ts index 2774744beb5..ab90381eafe 100644 --- a/apps/sim/lib/knowledge/__integration__/gitlab-workspace.integration.ts +++ b/apps/sim/lib/knowledge/__integration__/gitlab-workspace.integration.ts @@ -15,6 +15,7 @@ import { user, workspace, } from '@sim/db/schema' +import { deleteWorkspaceFixture } from '@sim/db/testing/workspace-fixtures' import { generateId } from '@sim/utils/id' import { and, eq, inArray, isNull } from 'drizzle-orm' import { afterAll, beforeAll, expect, it, vi } from 'vitest' @@ -112,7 +113,7 @@ beforeAll(async () => { afterAll(async () => { vi.restoreAllMocks() - await db.delete(workspace).where(eq(workspace.id, ids.workspaceId)) + await deleteWorkspaceFixture(db, eq(workspace.id, ids.workspaceId)) await db.delete(organization).where(eq(organization.id, ids.organizationId)) await db.delete(user).where(inArray(user.id, [ids.aliceId, ids.bobId])) await rm(fixture.root, { recursive: true, force: true }) diff --git a/apps/sim/lib/knowledge/__integration__/gmail-member.integration.ts b/apps/sim/lib/knowledge/__integration__/gmail-member.integration.ts index af955d4df2b..cc07d015cda 100644 --- a/apps/sim/lib/knowledge/__integration__/gmail-member.integration.ts +++ b/apps/sim/lib/knowledge/__integration__/gmail-member.integration.ts @@ -18,6 +18,7 @@ import { user, workspace, } from '@sim/db/schema' +import { deleteWorkspaceFixture } from '@sim/db/testing/workspace-fixtures' import { generateId } from '@sim/utils/id' import { eq, inArray } from 'drizzle-orm' import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, it, vi } from 'vitest' @@ -307,7 +308,7 @@ describe('Gmail member ingestion and ACLs in PostgreSQL (provider fixtures)', () } } finally { storageKeys.clear() - await db.delete(workspace).where(eq(workspace.id, ids.workspaceId)) + await deleteWorkspaceFixture(db, eq(workspace.id, ids.workspaceId)) await db.delete(user).where(inArray(user.id, [ids.aliceId, ids.bobId])) } }) diff --git a/apps/sim/lib/knowledge/__integration__/google-calendar-member.integration.ts b/apps/sim/lib/knowledge/__integration__/google-calendar-member.integration.ts index 4d406455565..574c67c19a3 100644 --- a/apps/sim/lib/knowledge/__integration__/google-calendar-member.integration.ts +++ b/apps/sim/lib/knowledge/__integration__/google-calendar-member.integration.ts @@ -19,6 +19,7 @@ import { user, workspace, } from '@sim/db/schema' +import { deleteWorkspaceFixture } from '@sim/db/testing/workspace-fixtures' import { generateId } from '@sim/utils/id' import { and, eq, inArray, isNull } from 'drizzle-orm' import { afterAll, beforeAll, beforeEach, describe, expect, it, vi } from 'vitest' @@ -339,7 +340,7 @@ describe('Google Calendar member indexing and authorization in PostgreSQL', () = if (!(error instanceof Error && 'code' in error && error.code === 'ENOENT')) throw error }) } finally { - await db.delete(workspace).where(eq(workspace.id, ids.workspaceId)) + await deleteWorkspaceFixture(db, eq(workspace.id, ids.workspaceId)) await db.delete(user).where( inArray( user.id, diff --git a/apps/sim/lib/knowledge/__integration__/google-drive-live.integration.ts b/apps/sim/lib/knowledge/__integration__/google-drive-live.integration.ts index 84436ace304..068c629441f 100644 --- a/apps/sim/lib/knowledge/__integration__/google-drive-live.integration.ts +++ b/apps/sim/lib/knowledge/__integration__/google-drive-live.integration.ts @@ -22,6 +22,7 @@ import { workspace, workspaceFiles, } from '@sim/db/schema' +import { deleteWorkspaceFixture } from '@sim/db/testing/workspace-fixtures' import { generateId } from '@sim/utils/id' import { and, eq } from 'drizzle-orm' import { afterAll, beforeAll, describe, expect, it, vi } from 'vitest' @@ -251,7 +252,7 @@ describe.skipIf(!keyPath || !folderId || !ownerEmail)( } }) } - await db.delete(workspace).where(eq(workspace.id, ids.workspaceId)) + await deleteWorkspaceFixture(db, eq(workspace.id, ids.workspaceId)) await db.delete(user).where(eq(user.id, ids.aliceId)) await db.delete(user).where(eq(user.id, ids.bobId)) } diff --git a/apps/sim/lib/knowledge/__integration__/google-drive-shortcuts.integration.ts b/apps/sim/lib/knowledge/__integration__/google-drive-shortcuts.integration.ts index 80b569631bf..eeffefbad5b 100644 --- a/apps/sim/lib/knowledge/__integration__/google-drive-shortcuts.integration.ts +++ b/apps/sim/lib/knowledge/__integration__/google-drive-shortcuts.integration.ts @@ -13,6 +13,7 @@ import { user, workspace, } from '@sim/db/schema' +import { deleteWorkspaceFixture } from '@sim/db/testing/workspace-fixtures' import { generateId } from '@sim/utils/id' import { and, eq, inArray } from 'drizzle-orm' import { PDFDocument, StandardFonts } from 'pdf-lib' @@ -216,7 +217,7 @@ describe('Drive shortcuts through indexing and search', () => { vi.stubGlobal('fetch', providerFetch) }) afterAll(async () => { - await db.delete(workspace).where(eq(workspace.id, ids.workspaceId)) + await deleteWorkspaceFixture(db, eq(workspace.id, ids.workspaceId)) await db.delete(organization).where(eq(organization.id, ids.organizationId)) await db.delete(user).where(inArray(user.id, [ids.aliceId, ids.bobId])) await rm(fixture.storageRoot, { recursive: true, force: true }) diff --git a/apps/sim/lib/knowledge/__integration__/jira-member.integration.ts b/apps/sim/lib/knowledge/__integration__/jira-member.integration.ts index b3ae46cd07f..8672778309b 100644 --- a/apps/sim/lib/knowledge/__integration__/jira-member.integration.ts +++ b/apps/sim/lib/knowledge/__integration__/jira-member.integration.ts @@ -20,6 +20,7 @@ import { user, workspace, } from '@sim/db/schema' +import { deleteWorkspaceFixture } from '@sim/db/testing/workspace-fixtures' import { generateId } from '@sim/utils/id' import { and, eq, inArray, isNull } from 'drizzle-orm' import { afterAll, beforeAll, beforeEach, describe, expect, it, vi } from 'vitest' @@ -308,7 +309,7 @@ describe('Jira member indexing and authorization in PostgreSQL', () => { if (!(error instanceof Error && 'code' in error && error.code === 'ENOENT')) throw error }) } finally { - await db.delete(workspace).where(eq(workspace.id, ids.workspaceId)) + await deleteWorkspaceFixture(db, eq(workspace.id, ids.workspaceId)) await db.delete(user).where( inArray( user.id, diff --git a/apps/sim/lib/knowledge/__integration__/kb-block-search.integration.ts b/apps/sim/lib/knowledge/__integration__/kb-block-search.integration.ts index 7ebef980cfb..294ee0ff1c4 100644 --- a/apps/sim/lib/knowledge/__integration__/kb-block-search.integration.ts +++ b/apps/sim/lib/knowledge/__integration__/kb-block-search.integration.ts @@ -2,6 +2,7 @@ import type { Principal } from '@sim/auth/principal' import { db } from '@sim/db' import { document, embedding, knowledgeBase, organization, user, workspace } from '@sim/db/schema' +import { deleteWorkspaceFixture } from '@sim/db/testing/workspace-fixtures' import { generateId } from '@sim/utils/id' import { eq, inArray } from 'drizzle-orm' import { afterAll, beforeAll, describe, expect, it } from 'vitest' @@ -79,7 +80,7 @@ describe('API-key KB block fan-out', () => { }) afterAll(async () => { - await db.delete(workspace).where(eq(workspace.id, ids.workspaceId)) + await deleteWorkspaceFixture(db, eq(workspace.id, ids.workspaceId)) await db.delete(organization).where(eq(organization.id, ids.organizationId)) await db.delete(user).where(inArray(user.id, [ids.aliceId, ids.bobId])) await db.$client.end() diff --git a/apps/sim/lib/knowledge/__integration__/knowledge-base-deletion.integration.ts b/apps/sim/lib/knowledge/__integration__/knowledge-base-deletion.integration.ts index 07315c48fc6..74ffcefd1e8 100644 --- a/apps/sim/lib/knowledge/__integration__/knowledge-base-deletion.integration.ts +++ b/apps/sim/lib/knowledge/__integration__/knowledge-base-deletion.integration.ts @@ -9,6 +9,7 @@ import { user, workspace, } from '@sim/db/schema' +import { deleteWorkspaceFixture } from '@sim/db/testing/workspace-fixtures' import { generateId } from '@sim/utils/id' import { and, eq, inArray, isNull, sql } from 'drizzle-orm' import { afterEach, describe, expect, it } from 'vitest' @@ -59,7 +60,7 @@ describe('knowledge base deletion in PostgreSQL', () => { afterEach(async () => { for (const ids of fixtures.splice(0)) { - await db.delete(workspace).where(eq(workspace.id, ids.workspaceId)) + await deleteWorkspaceFixture(db, eq(workspace.id, ids.workspaceId)) await db.delete(organization).where(eq(organization.id, ids.organizationId)) await db.delete(user).where(inArray(user.id, [ids.aliceId, ids.bobId])) } diff --git a/apps/sim/lib/knowledge/__integration__/knowledge-base-list.integration.ts b/apps/sim/lib/knowledge/__integration__/knowledge-base-list.integration.ts index f2e61a91623..d9d4c674adc 100644 --- a/apps/sim/lib/knowledge/__integration__/knowledge-base-list.integration.ts +++ b/apps/sim/lib/knowledge/__integration__/knowledge-base-list.integration.ts @@ -1,6 +1,7 @@ import { writeFileSync } from 'node:fs' import { db } from '@sim/db' import { document, knowledgeBase, organization, user, workspace } from '@sim/db/schema' +import { deleteWorkspaceFixture } from '@sim/db/testing/workspace-fixtures' import { generateId } from '@sim/utils/id' import { eq, inArray, sql } from 'drizzle-orm' import { afterAll, beforeAll, describe, expect, it } from 'vitest' @@ -126,7 +127,7 @@ afterAll(async () => { if (reportPath) writeFileSync(reportPath, JSON.stringify(reports, null, 2)) try { for (const fixture of [ids, foreign, manyBases]) { - await db.delete(workspace).where(eq(workspace.id, fixture.workspaceId)) + await deleteWorkspaceFixture(db, eq(workspace.id, fixture.workspaceId)) await db.delete(organization).where(eq(organization.id, fixture.organizationId)) await db.delete(user).where(inArray(user.id, [fixture.aliceId, fixture.bobId])) } diff --git a/apps/sim/lib/knowledge/__integration__/knowledge-projection.integration.ts b/apps/sim/lib/knowledge/__integration__/knowledge-projection.integration.ts index 794e3f512e4..518a7fa8d5b 100644 --- a/apps/sim/lib/knowledge/__integration__/knowledge-projection.integration.ts +++ b/apps/sim/lib/knowledge/__integration__/knowledge-projection.integration.ts @@ -16,6 +16,7 @@ import { user, workspace, } from '@sim/db/schema' +import { deleteWorkspaceFixture } from '@sim/db/testing/workspace-fixtures' import { generateId } from '@sim/utils/id' import { eq, inArray, sql } from 'drizzle-orm' import postgres from 'postgres' @@ -115,7 +116,7 @@ beforeEach(async () => { }) afterAll(async () => { - await db.delete(workspace).where(eq(workspace.id, ids.workspaceId)) + await deleteWorkspaceFixture(db, eq(workspace.id, ids.workspaceId)) await db.delete(organization).where(eq(organization.id, ids.organizationId)) await db.delete(user).where(inArray(user.id, [ids.aliceId, ids.bobId])) await projector?.end() diff --git a/apps/sim/lib/knowledge/__integration__/listing-continuation.integration.ts b/apps/sim/lib/knowledge/__integration__/listing-continuation.integration.ts index 70ef7114fc7..78a4182d587 100644 --- a/apps/sim/lib/knowledge/__integration__/listing-continuation.integration.ts +++ b/apps/sim/lib/knowledge/__integration__/listing-continuation.integration.ts @@ -19,6 +19,7 @@ import { user, workspace, } from '@sim/db/schema' +import { deleteWorkspaceFixture } from '@sim/db/testing/workspace-fixtures' import { toNumberOrNull } from '@sim/utils/coerce' import { generateId } from '@sim/utils/id' import { toArray, toRecord } from '@sim/utils/object' @@ -150,7 +151,7 @@ describe('durable source and member cycles in PostgreSQL', () => { })) }) afterAll(async () => { - await db.delete(workspace).where(eq(workspace.id, ids.workspaceId)) + await deleteWorkspaceFixture(db, eq(workspace.id, ids.workspaceId)) await db.delete(user).where(eq(user.id, ids.aliceId)) await db.delete(user).where(eq(user.id, ids.bobId)) await rm(fixture.storageRoot, { recursive: true, force: true }) diff --git a/apps/sim/lib/knowledge/__integration__/member-document-lifecycle.integration.ts b/apps/sim/lib/knowledge/__integration__/member-document-lifecycle.integration.ts index 801274d195a..f0873f9196a 100644 --- a/apps/sim/lib/knowledge/__integration__/member-document-lifecycle.integration.ts +++ b/apps/sim/lib/knowledge/__integration__/member-document-lifecycle.integration.ts @@ -9,6 +9,7 @@ import { user, workspace, } from '@sim/db/schema' +import { deleteWorkspaceFixture } from '@sim/db/testing/workspace-fixtures' import { generateId } from '@sim/utils/id' import { and, eq, inArray, isNotNull, sql } from 'drizzle-orm' import { @@ -63,7 +64,7 @@ describe('member document lifecycle in PostgreSQL', () => { }) afterEach(async () => { - await db.delete(workspace).where(eq(workspace.id, ids.workspaceId)) + await deleteWorkspaceFixture(db, eq(workspace.id, ids.workspaceId)) await db.delete(organization).where(eq(organization.id, ids.organizationId)) await db.delete(user).where(inArray(user.id, [ids.aliceId, ids.bobId])) }) diff --git a/apps/sim/lib/knowledge/__integration__/member-scope-renewal.integration.ts b/apps/sim/lib/knowledge/__integration__/member-scope-renewal.integration.ts index 0a35cf64cbe..72c4492919e 100644 --- a/apps/sim/lib/knowledge/__integration__/member-scope-renewal.integration.ts +++ b/apps/sim/lib/knowledge/__integration__/member-scope-renewal.integration.ts @@ -7,6 +7,7 @@ import { user, workspace, } from '@sim/db/schema' +import { deleteWorkspaceFixture } from '@sim/db/testing/workspace-fixtures' import { generateId } from '@sim/utils/id' import { and, eq, inArray, lt, sql } from 'drizzle-orm' import { afterAll, afterEach, beforeEach, describe, expect, it } from 'vitest' @@ -31,7 +32,7 @@ describe('member observation renewal by access scope in PostgreSQL', () => { }) afterEach(async () => { - await db.delete(workspace).where(eq(workspace.id, ids.workspaceId)) + await deleteWorkspaceFixture(db, eq(workspace.id, ids.workspaceId)) await db.delete(organization).where(eq(organization.id, ids.organizationId)) await db.delete(user).where(inArray(user.id, [ids.aliceId, ids.bobId])) }) diff --git a/apps/sim/lib/knowledge/__integration__/ocr-input-failures.integration.ts b/apps/sim/lib/knowledge/__integration__/ocr-input-failures.integration.ts index 3dd20af7932..476bc08859c 100644 --- a/apps/sim/lib/knowledge/__integration__/ocr-input-failures.integration.ts +++ b/apps/sim/lib/knowledge/__integration__/ocr-input-failures.integration.ts @@ -14,6 +14,7 @@ import { user, workspace, } from '@sim/db/schema' +import { deleteWorkspaceFixture } from '@sim/db/testing/workspace-fixtures' import { sha256Hex } from '@sim/security/hash' import { generateId } from '@sim/utils/id' import { eq, inArray, sql } from 'drizzle-orm' @@ -83,7 +84,7 @@ describe('OCR input failures stop without partial indexing or futile retries', ( await db.delete(rateLimitBucket).where(inArray(rateLimitBucket.key, capacityKeys)) for (const ids of seeded) { await db.delete(knowledgeBase).where(eq(knowledgeBase.id, ids.knowledgeBaseId)) - await db.delete(workspace).where(eq(workspace.id, ids.workspaceId)) + await deleteWorkspaceFixture(db, eq(workspace.id, ids.workspaceId)) await db.delete(organization).where(eq(organization.id, ids.organizationId)) await db.delete(user).where(inArray(user.id, [ids.aliceId, ids.bobId])) } diff --git a/apps/sim/lib/knowledge/__integration__/processing-lock-scope.integration.ts b/apps/sim/lib/knowledge/__integration__/processing-lock-scope.integration.ts index fb793b32330..b05608e1a9a 100644 --- a/apps/sim/lib/knowledge/__integration__/processing-lock-scope.integration.ts +++ b/apps/sim/lib/knowledge/__integration__/processing-lock-scope.integration.ts @@ -8,6 +8,7 @@ import { tmpdir } from 'node:os' import path from 'node:path' import { db } from '@sim/db' import { document, embedding, knowledgeBase, organization, user, workspace } from '@sim/db/schema' +import { deleteWorkspaceFixture } from '@sim/db/testing/workspace-fixtures' import { generateId } from '@sim/utils/id' import { eq, inArray } from 'drizzle-orm' import postgres from 'postgres' @@ -70,7 +71,7 @@ describe('document processing commit lock scope', () => { afterAll(async () => { vi.restoreAllMocks() await db.delete(knowledgeBase).where(eq(knowledgeBase.id, ids.knowledgeBaseId)) - await db.delete(workspace).where(eq(workspace.id, ids.workspaceId)) + await deleteWorkspaceFixture(db, eq(workspace.id, ids.workspaceId)) await db.delete(organization).where(eq(organization.id, ids.organizationId)) await db.delete(user).where(inArray(user.id, [ids.aliceId, ids.bobId])) await rm(fixtures.root, { recursive: true, force: true }) diff --git a/apps/sim/lib/knowledge/__integration__/provider-processing-recovery.integration.ts b/apps/sim/lib/knowledge/__integration__/provider-processing-recovery.integration.ts index eb1cb08c663..7e19e45a1fc 100644 --- a/apps/sim/lib/knowledge/__integration__/provider-processing-recovery.integration.ts +++ b/apps/sim/lib/knowledge/__integration__/provider-processing-recovery.integration.ts @@ -18,6 +18,7 @@ import { user, workspace, } from '@sim/db/schema' +import { deleteWorkspaceFixture } from '@sim/db/testing/workspace-fixtures' import { sha256Hex } from '@sim/security/hash' import { generateId } from '@sim/utils/id' import { and, eq, inArray, sql } from 'drizzle-orm' @@ -95,7 +96,7 @@ describe('provider throttling resumes the shared indexing pipeline', () => { await db.delete(rateLimitBucket).where(inArray(rateLimitBucket.key, capacityKeys)) for (const ids of seeded) { await db.delete(knowledgeBase).where(eq(knowledgeBase.id, ids.knowledgeBaseId)) - await db.delete(workspace).where(eq(workspace.id, ids.workspaceId)) + await deleteWorkspaceFixture(db, eq(workspace.id, ids.workspaceId)) await db.delete(organization).where(eq(organization.id, ids.organizationId)) await db.delete(user).where(inArray(user.id, [ids.aliceId, ids.bobId])) } diff --git a/apps/sim/lib/knowledge/__integration__/providers-live.integration.ts b/apps/sim/lib/knowledge/__integration__/providers-live.integration.ts index c65f83a8fdc..d85537c177d 100644 --- a/apps/sim/lib/knowledge/__integration__/providers-live.integration.ts +++ b/apps/sim/lib/knowledge/__integration__/providers-live.integration.ts @@ -18,6 +18,7 @@ import { workspaceBYOKKeys, workspaceFiles, } from '@sim/db/schema' +import { deleteWorkspaceFixture } from '@sim/db/testing/workspace-fixtures' import { createLogger, LogLevel } from '@sim/logger' import { generateId } from '@sim/utils/id' import { eq, inArray } from 'drizzle-orm' @@ -107,7 +108,7 @@ describe.skipIf(!credentialsFile)('real embedding and scanned PDF providers', () .from(workspaceFiles) .where(eq(workspaceFiles.workspaceId, ids.workspaceId)) for (const file of files) await deleteFile({ key: file.key, context: 'knowledge-base' }) - await db.delete(workspace).where(eq(workspace.id, ids.workspaceId)) + await deleteWorkspaceFixture(db, eq(workspace.id, ids.workspaceId)) await db.delete(user).where(inArray(user.id, [ids.aliceId, ids.bobId])) } if (fixtureStorage.root) await rm(fixtureStorage.root, { recursive: true, force: true }) diff --git a/apps/sim/lib/knowledge/__integration__/purged-detach-reservation.integration.ts b/apps/sim/lib/knowledge/__integration__/purged-detach-reservation.integration.ts index ad5fe38601e..0845e185ece 100644 --- a/apps/sim/lib/knowledge/__integration__/purged-detach-reservation.integration.ts +++ b/apps/sim/lib/knowledge/__integration__/purged-detach-reservation.integration.ts @@ -14,6 +14,7 @@ import { user, workspace, } from '@sim/db/schema' +import { deleteWorkspaceFixture } from '@sim/db/testing/workspace-fixtures' import { generateId } from '@sim/utils/id' import { and, eq, inArray, isNull, sql } from 'drizzle-orm' import { afterAll, describe, expect, it } from 'vitest' @@ -136,7 +137,7 @@ afterAll(async () => { ) ) await db.delete(knowledgeBase).where(eq(knowledgeBase.id, ids.knowledgeBaseId)) - await db.delete(workspace).where(eq(workspace.id, ids.workspaceId)) + await deleteWorkspaceFixture(db, eq(workspace.id, ids.workspaceId)) await db.delete(organization).where(eq(organization.id, ids.organizationId)) await db.delete(user).where(inArray(user.id, [ids.aliceId, ids.bobId])) } diff --git a/apps/sim/lib/knowledge/__integration__/scale.integration.ts b/apps/sim/lib/knowledge/__integration__/scale.integration.ts index a9bebc84d4b..31d2d2a3d04 100644 --- a/apps/sim/lib/knowledge/__integration__/scale.integration.ts +++ b/apps/sim/lib/knowledge/__integration__/scale.integration.ts @@ -1,6 +1,7 @@ import { readFileSync, statSync, writeFileSync } from 'node:fs' import { db } from '@sim/db' import { document, embedding, knowledgeConnector, user, workspace } from '@sim/db/schema' +import { deleteWorkspaceFixture } from '@sim/db/testing/workspace-fixtures' import { createLogger } from '@sim/logger' import { toStringOrNull } from '@sim/utils/coerce' import { toArray, toRecord } from '@sim/utils/object' @@ -259,7 +260,7 @@ describe.skipIf(!enabled)('knowledge scale: isolated real PostgreSQL, no provide try { await snapshot('final') if (!keepDatabase) { - await db.delete(workspace).where(eq(workspace.id, ids.workspaceId)) + await deleteWorkspaceFixture(db, eq(workspace.id, ids.workspaceId)) await db.delete(user).where(inArray(user.id, [ids.aliceId, ids.bobId])) } } finally { diff --git a/apps/sim/lib/knowledge/__integration__/search-index-policy.integration.ts b/apps/sim/lib/knowledge/__integration__/search-index-policy.integration.ts index 75880eba264..0a286b3d43d 100644 --- a/apps/sim/lib/knowledge/__integration__/search-index-policy.integration.ts +++ b/apps/sim/lib/knowledge/__integration__/search-index-policy.integration.ts @@ -2,6 +2,7 @@ import type { Principal } from '@sim/auth/principal' import { db } from '@sim/db' import { document, folder, knowledgeBase, permissions, user, workspace } from '@sim/db/schema' +import { deleteWorkspaceFixture } from '@sim/db/testing/workspace-fixtures' import { generateId } from '@sim/utils/id' import { and, eq } from 'drizzle-orm' import { afterAll, beforeAll, describe, expect, it, vi } from 'vitest' @@ -107,7 +108,7 @@ describe('canonical search knowledge-base policy', () => { }) afterAll(async () => { - await db.delete(workspace).where(eq(workspace.id, ids.workspaceId)) + await deleteWorkspaceFixture(db, eq(workspace.id, ids.workspaceId)) await db.delete(user).where(eq(user.id, ids.aliceId)) await db.delete(user).where(eq(user.id, ids.bobId)) vi.unstubAllGlobals() diff --git a/apps/sim/lib/knowledge/__integration__/search-latency.integration.ts b/apps/sim/lib/knowledge/__integration__/search-latency.integration.ts index 41e4687fe58..8d19f607c59 100644 --- a/apps/sim/lib/knowledge/__integration__/search-latency.integration.ts +++ b/apps/sim/lib/knowledge/__integration__/search-latency.integration.ts @@ -14,6 +14,7 @@ import { user, workspace, } from '@sim/db/schema' +import { deleteWorkspaceFixture } from '@sim/db/testing/workspace-fixtures' import { createLogger, Logger } from '@sim/logger' import { generateId } from '@sim/utils/id' import { eq, inArray, sql } from 'drizzle-orm' @@ -711,7 +712,7 @@ describe.skipIf(!enabled)('Knowledge search latency on a realistic indexed corpu for (const fixture of process.env.KNOWLEDGE_SEARCH_PERFORMANCE_KEEP_DATABASE === 'true' ? [] : [ids, unrelated, fullWidthFixture]) { - await db.delete(workspace).where(eq(workspace.id, fixture.workspaceId)) + await deleteWorkspaceFixture(db, eq(workspace.id, fixture.workspaceId)) await db.delete(organization).where(eq(organization.id, fixture.organizationId)) await db.delete(user).where(eq(user.id, fixture.aliceId)) await db.delete(user).where(eq(user.id, fixture.bobId)) diff --git a/apps/sim/lib/knowledge/__integration__/search-reference-batching.integration.ts b/apps/sim/lib/knowledge/__integration__/search-reference-batching.integration.ts index a08d83b195a..a7b0e91f100 100644 --- a/apps/sim/lib/knowledge/__integration__/search-reference-batching.integration.ts +++ b/apps/sim/lib/knowledge/__integration__/search-reference-batching.integration.ts @@ -6,6 +6,7 @@ import { user, workspace, } from '@sim/db/schema' +import { deleteWorkspaceFixture } from '@sim/db/testing/workspace-fixtures' import { generateId } from '@sim/utils/id' import { and, eq, inArray } from 'drizzle-orm' import { afterAll, beforeAll, describe, expect, it, vi } from 'vitest' @@ -53,7 +54,7 @@ describe('batched search reference reads', () => { afterAll(async () => { vi.restoreAllMocks() - await db.delete(workspace).where(eq(workspace.id, fixture.workspaceId)) + await deleteWorkspaceFixture(db, eq(workspace.id, fixture.workspaceId)) await db.delete(organization).where(eq(organization.id, fixture.organizationId)) await db.delete(user).where(inArray(user.id, [fixture.aliceId, fixture.bobId])) await db.$client.end() diff --git a/apps/sim/lib/knowledge/__integration__/search-source-pagination.integration.ts b/apps/sim/lib/knowledge/__integration__/search-source-pagination.integration.ts index 9dc7e6a60c9..aa79c41f71f 100644 --- a/apps/sim/lib/knowledge/__integration__/search-source-pagination.integration.ts +++ b/apps/sim/lib/knowledge/__integration__/search-source-pagination.integration.ts @@ -8,6 +8,7 @@ import { user, workspace, } from '@sim/db/schema' +import { deleteWorkspaceFixture } from '@sim/db/testing/workspace-fixtures' import { generateId } from '@sim/utils/id' import { eq, inArray, sql } from 'drizzle-orm' import { afterAll, beforeAll, describe, expect, it } from 'vitest' @@ -62,7 +63,7 @@ beforeAll(async () => { }) afterAll(async () => { - await db.delete(workspace).where(eq(workspace.id, ids.workspaceId)) + await deleteWorkspaceFixture(db, eq(workspace.id, ids.workspaceId)) await db.delete(organization).where(eq(organization.id, ids.organizationId)) await db.delete(user).where(eq(user.id, ids.aliceId)) await db.delete(user).where(eq(user.id, ids.bobId)) diff --git a/apps/sim/lib/knowledge/__integration__/search-source-progress.integration.ts b/apps/sim/lib/knowledge/__integration__/search-source-progress.integration.ts index bf1a715cc08..a173f79707e 100644 --- a/apps/sim/lib/knowledge/__integration__/search-source-progress.integration.ts +++ b/apps/sim/lib/knowledge/__integration__/search-source-progress.integration.ts @@ -9,6 +9,7 @@ import { user, workspace, } from '@sim/db/schema' +import { deleteWorkspaceFixture } from '@sim/db/testing/workspace-fixtures' import { generateId } from '@sim/utils/id' import { and, eq, inArray } from 'drizzle-orm' import { afterAll, beforeAll, describe, expect, it, vi } from 'vitest' @@ -69,7 +70,7 @@ beforeAll(async () => { ) }) afterAll(async () => { - await db.delete(workspace).where(eq(workspace.id, ids.workspaceId)) + await deleteWorkspaceFixture(db, eq(workspace.id, ids.workspaceId)) await db.delete(organization).where(eq(organization.id, ids.organizationId)) await db.delete(user).where(eq(user.id, ids.aliceId)) await db.delete(user).where(eq(user.id, ids.bobId)) @@ -121,7 +122,7 @@ describe('retired Search document admission', () => { }) }) afterAll(async () => { - await db.delete(workspace).where(eq(workspace.id, fixture.workspaceId)) + await deleteWorkspaceFixture(db, eq(workspace.id, fixture.workspaceId)) await db.delete(organization).where(eq(organization.id, fixture.organizationId)) await db.delete(user).where(inArray(user.id, [fixture.aliceId, fixture.bobId])) }) @@ -245,7 +246,7 @@ describe('connector document filename search and document sets', () => { }) afterAll(async () => { - await db.delete(workspace).where(eq(workspace.id, fixture.workspaceId)) + await deleteWorkspaceFixture(db, eq(workspace.id, fixture.workspaceId)) await db.delete(organization).where(eq(organization.id, fixture.organizationId)) await db.delete(user).where(eq(user.id, fixture.aliceId)) await db.delete(user).where(eq(user.id, fixture.bobId)) @@ -445,7 +446,7 @@ describe('intentional skips and genuine failures across document reads', () => { }) afterAll(async () => { - await db.delete(workspace).where(eq(workspace.id, fixture.workspaceId)) + await deleteWorkspaceFixture(db, eq(workspace.id, fixture.workspaceId)) await db.delete(organization).where(eq(organization.id, fixture.organizationId)) await db.delete(user).where(eq(user.id, fixture.aliceId)) await db.delete(user).where(eq(user.id, fixture.bobId)) diff --git a/apps/sim/lib/knowledge/__integration__/search-source-setup.integration.ts b/apps/sim/lib/knowledge/__integration__/search-source-setup.integration.ts index 3d99262cb32..45fd0ad3615 100644 --- a/apps/sim/lib/knowledge/__integration__/search-source-setup.integration.ts +++ b/apps/sim/lib/knowledge/__integration__/search-source-setup.integration.ts @@ -12,7 +12,7 @@ import { workspace, } from '@sim/db/schema' import { generateId } from '@sim/utils/id' -import { and, eq, isNull } from 'drizzle-orm' +import { and, eq, inArray, isNull } from 'drizzle-orm' import { afterAll, beforeAll, describe, expect, it, vi } from 'vitest' const fixture = vi.hoisted(() => ({ dispatch: vi.fn() })) @@ -94,12 +94,12 @@ describe('Search source identity and concurrent creation', () => { }) afterAll(async () => { - await db.delete(workspace).where(eq(workspace.id, ids.workspaceId)) - await db.delete(workspace).where(eq(workspace.id, other.workspaceId)) - for (const id of [ids.aliceId, ids.bobId, other.aliceId, other.bobId]) { - await db.delete(project).where(eq(project.ownerId, id)) - await db.delete(user).where(eq(user.id, id)) - } + const userIds = [ids.aliceId, ids.bobId, other.aliceId, other.bobId] + await db.transaction(async (tx) => { + await tx.delete(workspace).where(inArray(workspace.ownerId, userIds)) + await tx.delete(project).where(inArray(project.ownerId, userIds)) + await tx.delete(user).where(inArray(user.id, userIds)) + }) }) async function createSource(folderId: string) { @@ -178,8 +178,18 @@ describe('Search source identity and concurrent creation', () => { .where(eq(resourcePolicy.workspaceId, created.id)) expect(policies).toHaveLength(0) } finally { - await db.delete(permissions).where(eq(permissions.entityId, created.id)) - await db.delete(workspace).where(eq(workspace.id, created.id)) + await db.transaction(async (tx) => { + await tx.delete(permissions).where(eq(permissions.entityId, created.id)) + const [removed] = await tx + .delete(workspace) + .where(eq(workspace.id, created.id)) + .returning({ projectId: workspace.projectId }) + if (removed) { + await tx + .delete(project) + .where(and(eq(project.id, removed.projectId), eq(project.ownerId, ids.aliceId))) + } + }) } }) diff --git a/apps/sim/lib/knowledge/__integration__/seed-source-access-fixture.ts b/apps/sim/lib/knowledge/__integration__/seed-source-access-fixture.ts index 66ee5364efc..28441370e74 100644 --- a/apps/sim/lib/knowledge/__integration__/seed-source-access-fixture.ts +++ b/apps/sim/lib/knowledge/__integration__/seed-source-access-fixture.ts @@ -13,9 +13,9 @@ import { organization, permissions, user, - workspace, } from '@sim/db/schema' import { assertDisposableTestDatabaseUrl } from '@sim/db/testing/test-infrastructure' +import { insertWorkspaceFixture } from '@sim/db/testing/workspace-fixtures' import { generateId } from '@sim/utils/id' import { and, eq } from 'drizzle-orm' @@ -79,7 +79,7 @@ export async function seedKnowledgeAclFixture( name: 'ACL integration organization', slug: ids.organizationId, }) - await db.insert(workspace).values({ + await insertWorkspaceFixture(db, { id: workspaceId, organizationId: ids.organizationId, name: 'ACL integration fixture', diff --git a/apps/sim/lib/knowledge/__integration__/slack-empty-threads.integration.ts b/apps/sim/lib/knowledge/__integration__/slack-empty-threads.integration.ts index a27f136fb84..a0da7d35c56 100644 --- a/apps/sim/lib/knowledge/__integration__/slack-empty-threads.integration.ts +++ b/apps/sim/lib/knowledge/__integration__/slack-empty-threads.integration.ts @@ -12,6 +12,7 @@ import { user, workspace, } from '@sim/db/schema' +import { deleteWorkspaceFixture } from '@sim/db/testing/workspace-fixtures' import { generateId } from '@sim/utils/id' import { and, eq, inArray } from 'drizzle-orm' import { afterAll, beforeAll, describe, expect, it, vi } from 'vitest' @@ -174,7 +175,7 @@ describe('Slack empty threads through sync jobs, indexing and search', () => { }) }) afterAll(async () => { - await db.delete(workspace).where(eq(workspace.id, ids.workspaceId)) + await deleteWorkspaceFixture(db, eq(workspace.id, ids.workspaceId)) await db.delete(organization).where(eq(organization.id, ids.organizationId)) await db.delete(user).where(inArray(user.id, [ids.aliceId, ids.bobId])) await rm(fixture.storageRoot, { recursive: true, force: true }) diff --git a/apps/sim/lib/knowledge/__integration__/storage-accounting.integration.ts b/apps/sim/lib/knowledge/__integration__/storage-accounting.integration.ts index 7cb1651e63c..d10f1c2ffa3 100644 --- a/apps/sim/lib/knowledge/__integration__/storage-accounting.integration.ts +++ b/apps/sim/lib/knowledge/__integration__/storage-accounting.integration.ts @@ -1,3 +1,4 @@ +import { deleteWorkspaceFixture } from '@sim/db/testing/workspace-fixtures' /** Real PostgreSQL coverage for storage ownership changes and document lifecycle accounting. */ import { execFile } from 'node:child_process' @@ -130,7 +131,7 @@ afterAll(async () => { ) ) await db.delete(knowledgeBase).where(eq(knowledgeBase.id, ids.knowledgeBaseId)) - await db.delete(workspace).where(eq(workspace.id, ids.workspaceId)) + await deleteWorkspaceFixture(db, eq(workspace.id, ids.workspaceId)) await db.delete(organization).where(eq(organization.id, ids.organizationId)) await db.delete(user).where(inArray(user.id, [ids.aliceId, ids.bobId])) } diff --git a/apps/sim/lib/knowledge/__integration__/storage-cleanup.integration.ts b/apps/sim/lib/knowledge/__integration__/storage-cleanup.integration.ts index 4351f9c0f2e..64f925f85c0 100644 --- a/apps/sim/lib/knowledge/__integration__/storage-cleanup.integration.ts +++ b/apps/sim/lib/knowledge/__integration__/storage-cleanup.integration.ts @@ -13,6 +13,7 @@ import { workspace, workspaceFiles, } from '@sim/db/schema' +import { deleteWorkspaceFixture } from '@sim/db/testing/workspace-fixtures' import { sleep } from '@sim/utils/helpers' import { generateId } from '@sim/utils/id' import { desc, eq, inArray, sql } from 'drizzle-orm' @@ -57,7 +58,7 @@ describe('knowledge backing storage cleanup in PostgreSQL', () => { if (events.length) await db.delete(outboxEvent).where(inArray(outboxEvent.id, events)) for (const ids of fixtures) { await db.delete(knowledgeBase).where(eq(knowledgeBase.id, ids.knowledgeBaseId)) - await db.delete(workspace).where(eq(workspace.id, ids.workspaceId)) + await deleteWorkspaceFixture(db, eq(workspace.id, ids.workspaceId)) await db.delete(organization).where(eq(organization.id, ids.organizationId)) await db.delete(user).where(inArray(user.id, [ids.aliceId, ids.bobId])) } diff --git a/apps/sim/lib/knowledge/__integration__/stored-document-recovery.integration.ts b/apps/sim/lib/knowledge/__integration__/stored-document-recovery.integration.ts index 9dc36a3698a..9eab286d844 100644 --- a/apps/sim/lib/knowledge/__integration__/stored-document-recovery.integration.ts +++ b/apps/sim/lib/knowledge/__integration__/stored-document-recovery.integration.ts @@ -14,6 +14,7 @@ import { user, workspace, } from '@sim/db/schema' +import { deleteWorkspaceFixture } from '@sim/db/testing/workspace-fixtures' import { generateId } from '@sim/utils/id' import { and, eq, inArray, sql } from 'drizzle-orm' import { afterAll, afterEach, beforeAll, describe, expect, it, vi } from 'vitest' @@ -172,7 +173,7 @@ afterAll(async () => { .delete(outboxEvent) .where(sql`${outboxEvent.payload}->>'knowledgeBaseId' = ${ids.knowledgeBaseId}`) await db.delete(knowledgeBase).where(eq(knowledgeBase.id, ids.knowledgeBaseId)) - await db.delete(workspace).where(eq(workspace.id, ids.workspaceId)) + await deleteWorkspaceFixture(db, eq(workspace.id, ids.workspaceId)) await db.delete(organization).where(eq(organization.id, ids.organizationId)) await db.delete(user).where(inArray(user.id, [ids.aliceId, ids.bobId])) } diff --git a/apps/sim/lib/knowledge/__integration__/upload-read-provenance.integration.ts b/apps/sim/lib/knowledge/__integration__/upload-read-provenance.integration.ts index 16733d0e868..72b0687f2cb 100644 --- a/apps/sim/lib/knowledge/__integration__/upload-read-provenance.integration.ts +++ b/apps/sim/lib/knowledge/__integration__/upload-read-provenance.integration.ts @@ -15,6 +15,7 @@ import { workspaceFileSecretProvenance, workspaceFiles, } from '@sim/db/schema' +import { deleteWorkspaceFixture } from '@sim/db/testing/workspace-fixtures' import { generateId } from '@sim/utils/id' import { and, eq, inArray } from 'drizzle-orm' import { afterAll, beforeAll, describe, expect, it, vi } from 'vitest' @@ -130,7 +131,7 @@ afterAll(async () => { for (const ids of fixtures) { await db.delete(auditLog).where(eq(auditLog.workspaceId, ids.workspaceId)) await db.delete(knowledgeBase).where(eq(knowledgeBase.id, ids.knowledgeBaseId)) - await db.delete(workspace).where(eq(workspace.id, ids.workspaceId)) + await deleteWorkspaceFixture(db, eq(workspace.id, ids.workspaceId)) await db.delete(organization).where(eq(organization.id, ids.organizationId)) await db.delete(user).where(inArray(user.id, [ids.aliceId, ids.bobId])) } diff --git a/apps/sim/lib/knowledge/__integration__/user-document-visibility.integration.ts b/apps/sim/lib/knowledge/__integration__/user-document-visibility.integration.ts index 4e551f06cfb..541c17950c2 100644 --- a/apps/sim/lib/knowledge/__integration__/user-document-visibility.integration.ts +++ b/apps/sim/lib/knowledge/__integration__/user-document-visibility.integration.ts @@ -1,6 +1,7 @@ /** Real PostgreSQL coverage for the probe that keeps a user's visible documents from being skipped. */ import { db } from '@sim/db' import { document, knowledgeConnector, organization, user, workspace } from '@sim/db/schema' +import { deleteWorkspaceFixture } from '@sim/db/testing/workspace-fixtures' import { generateId } from '@sim/utils/id' import { eq, inArray } from 'drizzle-orm' import { afterAll, afterEach, beforeEach, describe, expect, it } from 'vitest' @@ -28,7 +29,7 @@ describe('Visible user documents in PostgreSQL', () => { }) }) afterEach(async () => { - await db.delete(workspace).where(eq(workspace.id, owner.workspaceId)) + await deleteWorkspaceFixture(db, eq(workspace.id, owner.workspaceId)) await db.delete(organization).where(eq(organization.id, owner.organizationId)) await db.delete(user).where(inArray(user.id, [owner.aliceId, owner.bobId])) }) diff --git a/apps/sim/lib/knowledge/__integration__/workspace-import.integration.ts b/apps/sim/lib/knowledge/__integration__/workspace-import.integration.ts index c27bdf44438..08ca23b0b30 100644 --- a/apps/sim/lib/knowledge/__integration__/workspace-import.integration.ts +++ b/apps/sim/lib/knowledge/__integration__/workspace-import.integration.ts @@ -17,6 +17,7 @@ import { workspace, workspaceFiles, } from '@sim/db/schema' +import { deleteWorkspaceFixture } from '@sim/db/testing/workspace-fixtures' import { generateId } from '@sim/utils/id' import { and, eq, inArray, sql } from 'drizzle-orm' import { afterAll, beforeAll, describe, expect, it, vi } from 'vitest' @@ -130,7 +131,7 @@ afterAll(async () => { for (const ids of fixtures) { await db.delete(auditLog).where(eq(auditLog.workspaceId, ids.workspaceId)) await db.delete(knowledgeBase).where(eq(knowledgeBase.id, ids.knowledgeBaseId)) - await db.delete(workspace).where(eq(workspace.id, ids.workspaceId)) + await deleteWorkspaceFixture(db, eq(workspace.id, ids.workspaceId)) await db.delete(organization).where(eq(organization.id, ids.organizationId)) await db.delete(user).where(inArray(user.id, [ids.aliceId, ids.bobId])) } diff --git a/apps/sim/lib/knowledge/__integration__/workspace-kb-document-access.integration.ts b/apps/sim/lib/knowledge/__integration__/workspace-kb-document-access.integration.ts index af42f093157..6b42a80d071 100644 --- a/apps/sim/lib/knowledge/__integration__/workspace-kb-document-access.integration.ts +++ b/apps/sim/lib/knowledge/__integration__/workspace-kb-document-access.integration.ts @@ -26,6 +26,7 @@ import { user, workspace, } from '@sim/db/schema' +import { deleteWorkspaceFixture } from '@sim/db/testing/workspace-fixtures' import { generateId } from '@sim/utils/id' import { eq, inArray } from 'drizzle-orm' import { afterAll, beforeAll, describe, expect, it } from 'vitest' @@ -137,7 +138,7 @@ describe.each([ }) afterAll(async () => { - await db.delete(workspace).where(eq(workspace.id, ids.workspaceId)) + await deleteWorkspaceFixture(db, eq(workspace.id, ids.workspaceId)) await db.delete(organization).where(eq(organization.id, ids.organizationId)) await db.delete(user).where(inArray(user.id, [ids.aliceId, ids.bobId])) }) @@ -352,7 +353,7 @@ describe('a workspace source whose reader must be proven live', () => { }) afterAll(async () => { - await db.delete(workspace).where(eq(workspace.id, ids.workspaceId)) + await deleteWorkspaceFixture(db, eq(workspace.id, ids.workspaceId)) await db.delete(credentialGroup).where(eq(credentialGroup.id, groupId)) await db.delete(organization).where(eq(organization.id, ids.organizationId)) await db.delete(user).where(inArray(user.id, [ids.aliceId, ids.bobId])) diff --git a/apps/sim/lib/knowledge/__integration__/workspace-lifecycle.integration.ts b/apps/sim/lib/knowledge/__integration__/workspace-lifecycle.integration.ts index 5317a7da3da..73b9e563dc7 100644 --- a/apps/sim/lib/knowledge/__integration__/workspace-lifecycle.integration.ts +++ b/apps/sim/lib/knowledge/__integration__/workspace-lifecycle.integration.ts @@ -8,6 +8,7 @@ import { user, workspace, } from '@sim/db/schema' +import { deleteWorkspaceFixture, insertWorkspaceFixture } from '@sim/db/testing/workspace-fixtures' import { generateId } from '@sim/utils/id' import { eq, inArray } from 'drizzle-orm' import { afterEach, describe, expect, it, vi } from 'vitest' @@ -30,6 +31,7 @@ import { archiveWorkspace } from '@/lib/workspaces/lifecycle' describe('workspace knowledge lifecycle in PostgreSQL', () => { const fixtures: ReturnType[] = [] + const retainedEnvironments = new Map() async function seed() { const ids = createKnowledgeAclFixtureIds() @@ -50,7 +52,12 @@ describe('workspace knowledge lifecycle in PostgreSQL', () => { afterEach(async () => { for (const ids of fixtures.splice(0)) { - await db.delete(workspace).where(eq(workspace.id, ids.workspaceId)) + const retained = retainedEnvironments.get(ids.workspaceId) + await deleteWorkspaceFixture( + db, + inArray(workspace.id, retained ? [ids.workspaceId, retained] : [ids.workspaceId]) + ) + retainedEnvironments.delete(ids.workspaceId) await db.delete(organization).where(eq(organization.id, ids.organizationId)) await db.delete(user).where(inArray(user.id, [ids.aliceId, ids.bobId])) } @@ -61,6 +68,16 @@ describe('workspace knowledge lifecycle in PostgreSQL', () => { async (alreadyArchived) => { const ids = await seed() const other = await seed() + const retainedId = generateId() + retainedEnvironments.set(ids.workspaceId, retainedId) + await insertWorkspaceFixture(db, { + id: retainedId, + name: 'Retained active environment', + ownerId: ids.aliceId, + billedAccountUserId: ids.aliceId, + organizationId: ids.organizationId, + forkedFromWorkspaceId: ids.workspaceId, + }) const previousArchive = new Date('2025-01-01T00:00:00.000Z') if (alreadyArchived) { await db @@ -115,7 +132,7 @@ describe('workspace knowledge lifecycle in PostgreSQL', () => { it('hard deletion cascades to KBs, documents, and connectors', async () => { const ids = await seed() - await db.delete(workspace).where(eq(workspace.id, ids.workspaceId)) + await deleteWorkspaceFixture(db, eq(workspace.id, ids.workspaceId)) expect( await db diff --git a/apps/sim/lib/logs/execution/completion-ledger-order.integration.ts b/apps/sim/lib/logs/execution/completion-ledger-order.integration.ts index 111e618a82d..f1c41610836 100644 --- a/apps/sim/lib/logs/execution/completion-ledger-order.integration.ts +++ b/apps/sim/lib/logs/execution/completion-ledger-order.integration.ts @@ -11,6 +11,7 @@ import { workflowExecutionSnapshots, workspace, } from '@sim/db/schema' +import { deleteWorkspaceFixture, insertWorkspaceFixture } from '@sim/db/testing/workspace-fixtures' import { createDeferred } from '@sim/testing' import { generateId } from '@sim/utils/id' import { eq, sql } from 'drizzle-orm' @@ -84,7 +85,7 @@ beforeAll(async () => { createdAt: now, updatedAt: now, }) - await db.insert(workspace).values({ + await insertWorkspaceFixture(db, { id: ids.workspace, name: 'Ledger Order', ownerId: ids.owner, @@ -107,7 +108,7 @@ afterAll(async () => { await db .delete(workflowExecutionSnapshots) .where(eq(workflowExecutionSnapshots.workflowId, ids.workflow)) - await db.delete(workspace).where(eq(workspace.id, ids.workspace)) + await deleteWorkspaceFixture(db, eq(workspace.id, ids.workspace)) await db.delete(user).where(eq(user.id, ids.owner)) }) diff --git a/apps/sim/lib/logs/execution/start-execution.integration.ts b/apps/sim/lib/logs/execution/start-execution.integration.ts index ea835d3d966..0db51eefcd7 100644 --- a/apps/sim/lib/logs/execution/start-execution.integration.ts +++ b/apps/sim/lib/logs/execution/start-execution.integration.ts @@ -10,6 +10,7 @@ import { workflowExecutionSnapshots, workspace, } from '@sim/db/schema' +import { deleteWorkspaceFixture, insertWorkspaceFixture } from '@sim/db/testing/workspace-fixtures' import { generateId } from '@sim/utils/id' import { eq, inArray } from 'drizzle-orm' import { afterAll, beforeAll, describe, expect, it } from 'vitest' @@ -84,7 +85,7 @@ beforeAll(async () => { createdAt: now, updatedAt: now, }) - await db.insert(workspace).values({ + await insertWorkspaceFixture(db, { id: ids.workspace, name: 'Start Log', ownerId: ids.owner, @@ -107,7 +108,7 @@ afterAll(async () => { await db .delete(workflowExecutionSnapshots) .where(eq(workflowExecutionSnapshots.workflowId, ids.workflow)) - await db.delete(workspace).where(eq(workspace.id, ids.workspace)) + await deleteWorkspaceFixture(db, eq(workspace.id, ids.workspace)) await db.delete(user).where(eq(user.id, ids.owner)) }) diff --git a/apps/sim/lib/mothership/agent-cli/workbench-file-provenance.integration.ts b/apps/sim/lib/mothership/agent-cli/workbench-file-provenance.integration.ts index 733c078aada..2fbc47f2f47 100644 --- a/apps/sim/lib/mothership/agent-cli/workbench-file-provenance.integration.ts +++ b/apps/sim/lib/mothership/agent-cli/workbench-file-provenance.integration.ts @@ -4,6 +4,7 @@ import { AuditAction } from '@sim/audit' import { db } from '@sim/db' import { auditLog, organization, user, workspace } from '@sim/db/schema' import { readTestRedisUrl } from '@sim/db/testing/test-infrastructure' +import { deleteWorkspaceFixture, insertWorkspaceFixture } from '@sim/db/testing/workspace-fixtures' import { flushMacrotask } from '@sim/testing/helpers/async' import { redisConfigMock, redisConfigMockFns } from '@sim/testing/mocks/redis-config.mock' import { generateShortId } from '@sim/utils/id' @@ -101,7 +102,7 @@ beforeAll(async () => { createdAt: new Date(), updatedAt: new Date(), }) - await db.insert(workspace).values({ + await insertWorkspaceFixture(db, { id: fixtureWorkspaceId, name: 'Receipt test workspace', ownerId: fixtureUserId, @@ -143,7 +144,7 @@ afterAll(async () => { } if (historyKeys.size > 0) await redis.del(...historyKeys) await db.delete(auditLog).where(eq(auditLog.workspaceId, fixtureWorkspaceId)) - await db.delete(workspace).where(eq(workspace.id, fixtureWorkspaceId)) + await deleteWorkspaceFixture(db, eq(workspace.id, fixtureWorkspaceId)) await db.delete(user).where(eq(user.id, fixtureUserId)) } finally { restoreAuditObservation?.() diff --git a/apps/sim/lib/mothership/async-runs/orphaned-runs.integration.ts b/apps/sim/lib/mothership/async-runs/orphaned-runs.integration.ts index 6fee3706e5a..653dce5adb4 100644 --- a/apps/sim/lib/mothership/async-runs/orphaned-runs.integration.ts +++ b/apps/sim/lib/mothership/async-runs/orphaned-runs.integration.ts @@ -1,3 +1,4 @@ +import { deleteWorkspaceFixture, insertWorkspaceFixture } from '@sim/db/testing/workspace-fixtures' /** * Settlement of Chat runs that no controller owns, against real PostgreSQL and Redis: * the chat stream lock, the replay buffer keys, the run and chat rows, and the Stop @@ -111,7 +112,7 @@ describe.runIf(Boolean(redisUrl))('Chat runs no controller owns', () => { createdAt: now, updatedAt: now, }) - await db.insert(workspace).values({ + await insertWorkspaceFixture(db, { id: workspaceId, name: 'Orphaned run fixture', ownerId: userId, @@ -129,7 +130,7 @@ describe.runIf(Boolean(redisUrl))('Chat runs no controller owns', () => { afterAll(async () => { if (chatIds.length) await db.delete(copilotChats).where(inArray(copilotChats.id, chatIds)) await db.delete(permissions).where(eq(permissions.userId, userId)) - await db.delete(workspace).where(eq(workspace.id, workspaceId)) + await deleteWorkspaceFixture(db, eq(workspace.id, workspaceId)) await db.delete(user).where(eq(user.id, userId)) }) diff --git a/apps/sim/lib/mothership/chat/application/set-effort.integration.ts b/apps/sim/lib/mothership/chat/application/set-effort.integration.ts index f3122ec23b2..aff041f6522 100644 --- a/apps/sim/lib/mothership/chat/application/set-effort.integration.ts +++ b/apps/sim/lib/mothership/chat/application/set-effort.integration.ts @@ -1,6 +1,7 @@ /** Exercises a chat's explicit effort choice against real PostgreSQL rows. */ import { db } from '@sim/db' import { copilotChats, permissions, user, workspace } from '@sim/db/schema' +import { deleteWorkspaceFixture, insertWorkspaceFixture } from '@sim/db/testing/workspace-fixtures' import { createSessionPrincipal } from '@sim/testing/factories/principal.factory' import { generateId } from '@sim/utils/id' import { eq, inArray } from 'drizzle-orm' @@ -46,7 +47,7 @@ beforeAll(async () => { updatedAt: now, })) ) - await db.insert(workspace).values({ + await insertWorkspaceFixture(db, { id: workspaceId, name: 'Chat effort fixture', ownerId, @@ -65,7 +66,7 @@ beforeAll(async () => { afterAll(async () => { await db.delete(copilotChats).where(eq(copilotChats.workspaceId, workspaceId)) - await db.delete(workspace).where(eq(workspace.id, workspaceId)) + await deleteWorkspaceFixture(db, eq(workspace.id, workspaceId)) await db.delete(user).where(inArray(user.id, [ownerId, outsiderId])) await db.$client.end() }) diff --git a/apps/sim/lib/mothership/request/session/recovery-storm.integration.ts b/apps/sim/lib/mothership/request/session/recovery-storm.integration.ts index bd7f34718cf..2319dad203a 100644 --- a/apps/sim/lib/mothership/request/session/recovery-storm.integration.ts +++ b/apps/sim/lib/mothership/request/session/recovery-storm.integration.ts @@ -1,3 +1,4 @@ +import { deleteWorkspaceFixture, insertWorkspaceFixture } from '@sim/db/testing/workspace-fixtures' /** * How often reconnecting to an orphaned Chat run re-POSTs it to the worker, against real * Redis and PostgreSQL. The production reconnect route, stream recovery, chat lifecycle @@ -297,7 +298,7 @@ describe.runIf(Boolean(redisUrl))('reconnecting to an orphaned Chat run', () => createdAt: now, updatedAt: now, }) - await db.insert(workspace).values({ + await insertWorkspaceFixture(db, { id: workspaceId, name: 'Recovery storm fixture', ownerId: userId, @@ -323,7 +324,7 @@ describe.runIf(Boolean(redisUrl))('reconnecting to an orphaned Chat run', () => await db.delete(copilotChats).where(eq(copilotChats.id, chatId)) } await db.delete(permissions).where(eq(permissions.userId, userId)) - await db.delete(workspace).where(eq(workspace.id, workspaceId)) + await deleteWorkspaceFixture(db, eq(workspace.id, workspaceId)) await db.delete(user).where(eq(user.id, userId)) await closeRedisConnection() }) diff --git a/apps/sim/lib/mothership/request/session/replay-budget.integration.ts b/apps/sim/lib/mothership/request/session/replay-budget.integration.ts index 6eab2cdd23b..8735d6b78aa 100644 --- a/apps/sim/lib/mothership/request/session/replay-budget.integration.ts +++ b/apps/sim/lib/mothership/request/session/replay-budget.integration.ts @@ -1,3 +1,4 @@ +import { deleteWorkspaceFixture, insertWorkspaceFixture } from '@sim/db/testing/workspace-fixtures' /** * The leased Chat stream writer and its replay-budget failure path against real * Redis and PostgreSQL. A local HTTP server stands in for the worker's abort @@ -479,7 +480,7 @@ describe.runIf(Boolean(redisUrl))('a turn whose stream exhausts its replay budge createdAt: now, updatedAt: now, }) - await db.insert(workspace).values({ + await insertWorkspaceFixture(db, { id: workspaceId, name: 'Replay budget fixture', ownerId: userId, @@ -502,7 +503,7 @@ describe.runIf(Boolean(redisUrl))('a turn whose stream exhausts its replay budge afterAll(async () => { await db.delete(copilotChats).where(eq(copilotChats.id, chatId)) await db.delete(permissions).where(eq(permissions.userId, userId)) - await db.delete(workspace).where(eq(workspace.id, workspaceId)) + await deleteWorkspaceFixture(db, eq(workspace.id, workspaceId)) await db.delete(user).where(eq(user.id, userId)) }) diff --git a/apps/sim/lib/mothership/request/session/replay-gap.integration.ts b/apps/sim/lib/mothership/request/session/replay-gap.integration.ts index f9e8be62fe5..e9d23055b67 100644 --- a/apps/sim/lib/mothership/request/session/replay-gap.integration.ts +++ b/apps/sim/lib/mothership/request/session/replay-gap.integration.ts @@ -1,3 +1,4 @@ +import { deleteWorkspaceFixture, insertWorkspaceFixture } from '@sim/db/testing/workspace-fixtures' /** * Reconnects that Sim's replay ring can no longer serve, against real Redis and * PostgreSQL through the production reconnect route. A local HTTP server stands in for @@ -154,7 +155,7 @@ describe.runIf(Boolean(redisUrl))('reconnects past the replay ring', () => { createdAt: now, updatedAt: now, }) - await db.insert(workspace).values({ + await insertWorkspaceFixture(db, { id: workspaceId, name: 'Replay gap fixture', ownerId: userId, @@ -183,7 +184,7 @@ describe.runIf(Boolean(redisUrl))('reconnects past the replay ring', () => { await db.delete(copilotRuns).where(eq(copilotRuns.chatId, chatId)) await db.delete(copilotChats).where(eq(copilotChats.id, chatId)) await db.delete(permissions).where(eq(permissions.userId, userId)) - await db.delete(workspace).where(eq(workspace.id, workspaceId)) + await deleteWorkspaceFixture(db, eq(workspace.id, workspaceId)) await db.delete(user).where(eq(user.id, userId)) await closeRedisConnection() }) diff --git a/apps/sim/lib/mothership/request/session/stream-recovery.integration.ts b/apps/sim/lib/mothership/request/session/stream-recovery.integration.ts index 2626b6d804e..afd820d1c9c 100644 --- a/apps/sim/lib/mothership/request/session/stream-recovery.integration.ts +++ b/apps/sim/lib/mothership/request/session/stream-recovery.integration.ts @@ -1,3 +1,4 @@ +import { deleteWorkspaceFixture, insertWorkspaceFixture } from '@sim/db/testing/workspace-fixtures' /** * Recovery of a Chat run whose Sim controller died after its replay ring lost its * head, against real Redis and PostgreSQL through the production reconnect route and @@ -195,7 +196,7 @@ describe.runIf(Boolean(redisUrl))('recovering a run whose ring lost its head', ( createdAt: now, updatedAt: now, }) - await db.insert(workspace).values({ + await insertWorkspaceFixture(db, { id: workspaceId, name: 'Stream recovery fixture', ownerId: userId, @@ -221,7 +222,7 @@ describe.runIf(Boolean(redisUrl))('recovering a run whose ring lost its head', ( await db.delete(copilotChats).where(eq(copilotChats.id, chatId)) } await db.delete(permissions).where(eq(permissions.userId, userId)) - await db.delete(workspace).where(eq(workspace.id, workspaceId)) + await deleteWorkspaceFixture(db, eq(workspace.id, workspaceId)) await db.delete(user).where(eq(user.id, userId)) await closeRedisConnection() }) diff --git a/apps/sim/lib/mothership/request/tools/workflow-client-settlement.integration.ts b/apps/sim/lib/mothership/request/tools/workflow-client-settlement.integration.ts index a67cdf1e019..f4669a97022 100644 --- a/apps/sim/lib/mothership/request/tools/workflow-client-settlement.integration.ts +++ b/apps/sim/lib/mothership/request/tools/workflow-client-settlement.integration.ts @@ -1,3 +1,4 @@ +import { deleteWorkspaceFixture, insertWorkspaceFixture } from '@sim/db/testing/workspace-fixtures' /** * A browser claims a Chat workflow tool, the execute route runs it, and the browser may never * report back (tab closed, network lost, beacon dropped). Runs against real PostgreSQL and Redis: @@ -88,7 +89,7 @@ describe.runIf(Boolean(redisUrl))('settled client-claimed workflow tools', () => createdAt: now, updatedAt: now, }) - await db.insert(workspace).values({ + await insertWorkspaceFixture(db, { id: workspaceId, name: 'Workflow settlement fixture', ownerId: userId, @@ -131,7 +132,7 @@ describe.runIf(Boolean(redisUrl))('settled client-claimed workflow tools', () => .delete(workflowExecutionSnapshots) .where(inArray(workflowExecutionSnapshots.id, snapshotIds)) await db.delete(workflow).where(eq(workflow.id, workflowId)) - await db.delete(workspace).where(eq(workspace.id, workspaceId)) + await deleteWorkspaceFixture(db, eq(workspace.id, workspaceId)) await db.delete(user).where(eq(user.id, userId)) }) diff --git a/apps/sim/lib/mothership/tasks/application/prepare-wake.integration.ts b/apps/sim/lib/mothership/tasks/application/prepare-wake.integration.ts index 5cf7636e426..d86a9232014 100644 --- a/apps/sim/lib/mothership/tasks/application/prepare-wake.integration.ts +++ b/apps/sim/lib/mothership/tasks/application/prepare-wake.integration.ts @@ -1,3 +1,4 @@ +import { deleteWorkspaceFixture, insertWorkspaceFixture } from '@sim/db/testing/workspace-fixtures' /** * How sim answers the worker's retry of a task wake, against real PostgreSQL and Redis: the * wake route, the chat stream lock, and the run records are production code. Only `after` is @@ -67,7 +68,7 @@ describe.runIf(Boolean(redisUrl))('task wake retries', () => { createdAt: now, updatedAt: now, }) - await db.insert(workspace).values({ + await insertWorkspaceFixture(db, { id: workspaceId, name: 'Task wake fixture', ownerId: userId, @@ -88,7 +89,7 @@ describe.runIf(Boolean(redisUrl))('task wake retries', () => { await db.delete(copilotChats).where(inArray(copilotChats.id, chatIds)) } await db.delete(permissions).where(eq(permissions.userId, userId)) - await db.delete(workspace).where(eq(workspace.id, workspaceId)) + await deleteWorkspaceFixture(db, eq(workspace.id, workspaceId)) await db.delete(user).where(eq(user.id, userId)) }) diff --git a/apps/sim/lib/mothership/tools/client/desktop-tool-authorization.integration.ts b/apps/sim/lib/mothership/tools/client/desktop-tool-authorization.integration.ts index e04d8728f9f..3e1afd3576f 100644 --- a/apps/sim/lib/mothership/tools/client/desktop-tool-authorization.integration.ts +++ b/apps/sim/lib/mothership/tools/client/desktop-tool-authorization.integration.ts @@ -25,6 +25,7 @@ vi.mock('@/lib/auth', () => authMock) import { db } from '@sim/db' import { copilotAsyncToolCalls, copilotChats, copilotRuns, user, workspace } from '@sim/db/schema' +import { deleteWorkspaceFixture, insertWorkspaceFixture } from '@sim/db/testing/workspace-fixtures' import { generateId } from '@sim/utils/id' import { eq } from 'drizzle-orm' import { NextRequest } from 'next/server' @@ -203,7 +204,7 @@ describe.runIf(Boolean(redisUrl))('desktop tool calls the server no longer admit createdAt: now, updatedAt: now, }) - await db.insert(workspace).values({ + await insertWorkspaceFixture(db, { id: workspaceId, name: 'Desktop authorization fixture', ownerId: userId, @@ -224,7 +225,7 @@ describe.runIf(Boolean(redisUrl))('desktop tool calls the server no longer admit afterAll(async () => { await db.delete(copilotChats).where(eq(copilotChats.id, chatId)) - await db.delete(workspace).where(eq(workspace.id, workspaceId)) + await deleteWorkspaceFixture(db, eq(workspace.id, workspaceId)) await db.delete(user).where(eq(user.id, userId)) }) diff --git a/apps/sim/lib/mothership/tools/client/desktop-tool-chat-view-lease.integration.ts b/apps/sim/lib/mothership/tools/client/desktop-tool-chat-view-lease.integration.ts index a13598bcb6c..c1cd8f4b5db 100644 --- a/apps/sim/lib/mothership/tools/client/desktop-tool-chat-view-lease.integration.ts +++ b/apps/sim/lib/mothership/tools/client/desktop-tool-chat-view-lease.integration.ts @@ -5,6 +5,7 @@ * Runs against real PostgreSQL and Redis through the production pre-persist path, the authorize * route, the lease route and the confirm route. */ +import { deleteWorkspaceFixture, insertWorkspaceFixture } from '@sim/db/testing/workspace-fixtures' import { authMock, authMockFns } from '@sim/testing/mocks/auth.mock' import { afterAll, beforeAll, describe, expect, it, vi } from 'vitest' @@ -202,7 +203,7 @@ describe.runIf(Boolean(redisUrl))('a chat-view import kept alive by its lease', createdAt: now, updatedAt: now, }) - await db.insert(workspace).values({ + await insertWorkspaceFixture(db, { id: workspaceId, name: 'Chat-view lease fixture', ownerId: userId, @@ -226,7 +227,7 @@ describe.runIf(Boolean(redisUrl))('a chat-view import kept alive by its lease', afterAll(async () => { await db.delete(copilotChats).where(eq(copilotChats.id, chatId)) - await db.delete(workspace).where(eq(workspace.id, workspaceId)) + await deleteWorkspaceFixture(db, eq(workspace.id, workspaceId)) await db.delete(user).where(eq(user.id, userId)) await db.delete(user).where(eq(user.id, otherUserId)) }) diff --git a/apps/sim/lib/mothership/tools/client/desktop-tool-executor.integration.ts b/apps/sim/lib/mothership/tools/client/desktop-tool-executor.integration.ts index 813a3be4e0e..e78e0bdead8 100644 --- a/apps/sim/lib/mothership/tools/client/desktop-tool-executor.integration.ts +++ b/apps/sim/lib/mothership/tools/client/desktop-tool-executor.integration.ts @@ -22,6 +22,7 @@ vi.mock('@/lib/auth', () => authMock) import { db } from '@sim/db' import { copilotAsyncToolCalls, copilotChats, copilotRuns, user, workspace } from '@sim/db/schema' +import { deleteWorkspaceFixture, insertWorkspaceFixture } from '@sim/db/testing/workspace-fixtures' import { sleep } from '@sim/utils/helpers' import { generateId } from '@sim/utils/id' import { eq } from 'drizzle-orm' @@ -142,7 +143,7 @@ describe.runIf(Boolean(redisUrl))('a desktop tool call watched by a web tab', () createdAt: now, updatedAt: now, }) - await db.insert(workspace).values({ + await insertWorkspaceFixture(db, { id: workspaceId, name: 'Desktop executor fixture', ownerId: userId, @@ -177,7 +178,7 @@ describe.runIf(Boolean(redisUrl))('a desktop tool call watched by a web tab', () afterAll(async () => { await db.delete(copilotChats).where(eq(copilotChats.id, chatId)) - await db.delete(workspace).where(eq(workspace.id, workspaceId)) + await deleteWorkspaceFixture(db, eq(workspace.id, workspaceId)) await db.delete(user).where(eq(user.id, userId)) }) diff --git a/apps/sim/lib/mothership/tools/client/desktop-tool-pickup.integration.ts b/apps/sim/lib/mothership/tools/client/desktop-tool-pickup.integration.ts index 98a8b3c983b..0a9a5be1efa 100644 --- a/apps/sim/lib/mothership/tools/client/desktop-tool-pickup.integration.ts +++ b/apps/sim/lib/mothership/tools/client/desktop-tool-pickup.integration.ts @@ -6,6 +6,7 @@ * persisted and dispatched by the production stream handlers, the desktop claims it through the * authorize route and reports through the confirm route. */ +import { deleteWorkspaceFixture, insertWorkspaceFixture } from '@sim/db/testing/workspace-fixtures' import { authMock, authMockFns } from '@sim/testing/mocks/auth.mock' import { afterAll, beforeAll, describe, expect, it, vi } from 'vitest' @@ -141,7 +142,7 @@ describe.runIf(Boolean(redisUrl))('a desktop tool call nobody picks up', () => { createdAt: now, updatedAt: now, }) - await db.insert(workspace).values({ + await insertWorkspaceFixture(db, { id: workspaceId, name: 'Desktop pickup fixture', ownerId: userId, @@ -173,7 +174,7 @@ describe.runIf(Boolean(redisUrl))('a desktop tool call nobody picks up', () => { afterAll(async () => { await db.delete(copilotChats).where(eq(copilotChats.id, chatId)) - await db.delete(workspace).where(eq(workspace.id, workspaceId)) + await deleteWorkspaceFixture(db, eq(workspace.id, workspaceId)) await db.delete(user).where(eq(user.id, userId)) }) diff --git a/apps/sim/lib/oauth/credential-revocation.integration.ts b/apps/sim/lib/oauth/credential-revocation.integration.ts index 94b8d0f384e..9ec7b2f4c51 100644 --- a/apps/sim/lib/oauth/credential-revocation.integration.ts +++ b/apps/sim/lib/oauth/credential-revocation.integration.ts @@ -2,6 +2,7 @@ import { mkdir, writeFile } from 'node:fs/promises' import { dirname } from 'node:path' import { db } from '@sim/db' import { account, credential, user, workspace } from '@sim/db/schema' +import { deleteWorkspaceFixture, insertWorkspaceFixture } from '@sim/db/testing/workspace-fixtures' import { createDeferred, type Deferred } from '@sim/testing/helpers/deferred' import { generateId } from '@sim/utils/id' import { eq, inArray } from 'drizzle-orm' @@ -121,7 +122,7 @@ beforeAll(async () => { createdAt: new Date(), updatedAt: new Date(), }) - await db.insert(workspace).values({ + await insertWorkspaceFixture(db, { id: workspaceId, name: 'Revocation fixture', ownerId: userId, @@ -187,7 +188,7 @@ afterEach((context) => { afterAll(async () => { globalThis.fetch = originalFetch await expireRedisFlags() - await db.delete(workspace).where(eq(workspace.id, workspaceId)) + await deleteWorkspaceFixture(db, eq(workspace.id, workspaceId)) await db.delete(user).where(eq(user.id, userId)) await closeRedisConnection() const reportPath = diff --git a/apps/sim/lib/oauth/shopify.integration.ts b/apps/sim/lib/oauth/shopify.integration.ts index 0cd2360962b..8afe7b7b845 100644 --- a/apps/sim/lib/oauth/shopify.integration.ts +++ b/apps/sim/lib/oauth/shopify.integration.ts @@ -3,6 +3,7 @@ import { createServer, type Server } from 'node:http' import { dirname } from 'node:path' import { db } from '@sim/db' import { account, credential, pendingCredentialDraft, user, workspace } from '@sim/db/schema' +import { deleteWorkspaceFixture, insertWorkspaceFixture } from '@sim/db/testing/workspace-fixtures' import { createDeferred, type Deferred } from '@sim/testing/helpers/deferred' import { sleep } from '@sim/utils/helpers' import { generateId } from '@sim/utils/id' @@ -72,7 +73,7 @@ beforeAll(async () => { updatedAt: new Date(), })) ) - await db.insert(workspace).values({ + await insertWorkspaceFixture(db, { id: workspaceId, name: 'Shopify fixture', ownerId: userIds[0], @@ -212,7 +213,7 @@ afterEach((context) => { afterAll(async () => { globalThis.fetch = originalFetch - await db.delete(workspace).where(eq(workspace.id, workspaceId)) + await deleteWorkspaceFixture(db, eq(workspace.id, workspaceId)) await db.delete(user).where(inArray(user.id, userIds)) if (provider) { provider.closeAllConnections() diff --git a/apps/sim/lib/permission-groups/__integration__/copilot-permission-config.integration.ts b/apps/sim/lib/permission-groups/__integration__/copilot-permission-config.integration.ts index 6c69b851fc9..ad73f1909f9 100644 --- a/apps/sim/lib/permission-groups/__integration__/copilot-permission-config.integration.ts +++ b/apps/sim/lib/permission-groups/__integration__/copilot-permission-config.integration.ts @@ -11,6 +11,7 @@ import { user, workspace, } from '@sim/db/schema' +import { deleteWorkspaceFixture, insertWorkspaceFixture } from '@sim/db/testing/workspace-fixtures' import { envFlagsMock } from '@sim/testing/mocks/env-flags.mock' import { generateId } from '@sim/utils/id' import { eq, inArray } from 'drizzle-orm' @@ -73,7 +74,8 @@ describe('chat-delegated permission configuration', () => { createdAt: now, })) ) - await db.insert(workspace).values( + await insertWorkspaceFixture( + db, [workspaceId, otherWorkspaceId].map((id) => ({ id, name: 'Permission config fixture', @@ -131,7 +133,7 @@ describe('chat-delegated permission configuration', () => { }) afterAll(async () => { - await db.delete(workspace).where(inArray(workspace.id, [workspaceId, otherWorkspaceId])) + await deleteWorkspaceFixture(db, inArray(workspace.id, [workspaceId, otherWorkspaceId])) await db.delete(organization).where(eq(organization.id, organizationId)) await db.delete(user).where(inArray(user.id, [restrictedId, defaultMemberId, outsiderId])) }) diff --git a/apps/sim/lib/projects/README.md b/apps/sim/lib/projects/README.md index dc3deb8a198..df61b763da7 100644 --- a/apps/sim/lib/projects/README.md +++ b/apps/sim/lib/projects/README.md @@ -1,16 +1,47 @@ -# Project creation +# Projects A Project groups environments. An environment is an existing `workspace` record; there is no separate environment table. Every newly created Project starts with an environment. Project APIs return HTTP 503 until the `projects` feature flag is on (AppConfig on hosted deployments; the `PROJECT_API_ENABLED` secret elsewhere). The flag defaults off and gates only the Project APIs: workspace creation always assigns a Project atomically, and assigned Projects keep their lifecycle protections (fork inheritance, disconnect) either way. +## Storage and consistency + +Each workspace belongs to exactly one Project through the required `workspace.project_id` +foreign key. One Project can contain many workspaces, including disconnected roots; there +is no membership connector table. Referenced Projects cannot be deleted while their +workspaces remain. `project.updated_at` is Project-record metadata, not a workspace +activity counter or a concurrency version. + +Native composite foreign keys enforce two structural invariants: + +- Workspace and Project organizations agree, including personal (`NULL`) scope. Both + tables have a stored generated `organization_scope_key`: `personal` for NULL and + `organization:` followed by the organization ID otherwise. Writers cannot supply this + key independently. A unique Project `(id, organization_scope_key)` constraint supports + the workspace `(project_id, organization_scope_key)` foreign key. +- A connected fork and its parent share a Project. A unique workspace `(id, project_id)` + constraint supports `(forked_from_workspace_id, project_id)` referencing that pair. + The parent-ID foreign key uses `ON DELETE SET NULL`, clearing only the parent pointer. + The composite foreign key uses `NO ACTION`; Project membership never cascades automatically. + +Both composite foreign keys are `DEFERRABLE INITIALLY DEFERRED`, allowing atomic +organization transfers and subtree moves before validation. Drizzle records the columns, +indexes, and relationships; the shared schema finalizer sets deferred timing after a fresh +schema push because Drizzle does not represent that timing. + +Application transactions own creation with the first workspace, nonempty Projects, +archive lifecycle, workflow admission/restoration, and subtree disconnection. Their +Project, lineage, edge, and workspace locks protect these business operations. PostgreSQL +owns referential-integrity concurrency through the native constraints; no custom Project +integrity triggers or artificial Project row touches are required. + ## Choose the creation flow | Caller | Flow | Result | | --- | --- | --- | | New Project onboarding | `POST /api/projects` | Creates a Project and its first environment together, with independently supplied names. | | Existing workspace creation UI or caller | `POST /api/workspaces` | Creates a workspace and automatically creates its Project, preserving the existing workspace response. | -| Create another environment by forking | Existing workspace fork operation | Inherits the source workspace's Project; unassigned legacy families remain unassigned until backfilled. | +| Create another environment by forking | Existing workspace fork operation | Inherits the source workspace's Project. | Both POST endpoints are internal, session-authenticated APIs. `POST /api/projects` is not a public `/api/v2` endpoint and does not accept API-key principals. Existing workspace creation endpoints remain supported. diff --git a/apps/sim/lib/projects/__integration__/backfill-repair.integration.ts b/apps/sim/lib/projects/__integration__/backfill-repair.integration.ts new file mode 100644 index 00000000000..4bb72a9bbfb --- /dev/null +++ b/apps/sim/lib/projects/__integration__/backfill-repair.integration.ts @@ -0,0 +1,510 @@ +import { execFile } from 'node:child_process' +import { copyFile, mkdir, mkdtemp, readFile, rm, writeFile } from 'node:fs/promises' +import { createServer } from 'node:http' +import { tmpdir } from 'node:os' +import { join } from 'node:path' +import { promisify } from 'node:util' +import { switchProjectMembershipAuthority } from '@sim/db/maintenance/project-rollout' +import journal from '@sim/db/migrations/meta/_journal.json' +import { readTestDatabaseUrl, readTestRedisUrl } from '@sim/db/testing/test-infrastructure' +import { createDeferred } from '@sim/testing/helpers/deferred' +import { generateId } from '@sim/utils/id' +import Redis from 'ioredis' +import postgres from 'postgres' +import { afterAll, beforeAll, describe, expect, it } from 'vitest' + +const admin = postgres(readTestDatabaseUrl(), { max: 1 }) +const name = `project_repair_test_${generateId().replaceAll('-', '')}` +const url = new URL(readTestDatabaseUrl()) +url.pathname = `/${name}` +const client = postgres(url.toString(), { max: 1, onnotice: () => {} }) +let directory: string +let manifest: string +let report: string +const redisUrl = readTestRedisUrl() +const describeWithRedis = describe.runIf(Boolean(redisUrl)) +let cache: Redis +let subscriber: Redis +const mcpServerId = generateId() +const cacheKey = `mcp:tools:workspace:env:server:${mcpServerId}` +const published: unknown[] = [] +const releaseNotifications = createDeferred() +let tailNotified = false +const realtime = createServer(async (request, response) => { + const chunks: Buffer[] = [] + for await (const chunk of request) chunks.push(Buffer.from(chunk)) + const { workflowId } = JSON.parse(Buffer.concat(chunks).toString()) as { workflowId: string } + if (workflowId === 'tail') tailNotified = true + if (workflowId.startsWith('slow-')) await releaseNotifications.promise + response.writeHead(200, { 'content-type': 'application/json' }).end('{}') +}) +const environment = { + ...process.env, + DATABASE_URL: url.toString(), + MIGRATION_DATABASE_URL: url.toString(), + REDIS_URL: redisUrl, + SOCKET_SERVER_URL: process.env.SOCKET_SERVER_URL, +} +const execute = promisify(execFile) +const run = (command: string, artifacts = { manifest, report }) => + execute( + 'bun', + [ + '--no-env-file', + 'scripts/backfill-projects.ts', + command, + '--manifest', + artifacts.manifest, + ...(command === 'plan' ? [] : ['--report', artifacts.report, '--ack-release-drained']), + ], + { + cwd: new URL('../../../', import.meta.url), + env: environment, + timeout: 45000, + } + ) + +describeWithRedis('Operator archive repair against the full compatible schema', () => { + beforeAll(async () => { + if (!redisUrl) throw new Error('Archive repair integration requires TEST_REDIS_URL') + directory = await mkdtemp(join(tmpdir(), 'project-repair-test-')) + manifest = join(directory, 'manifest.json') + report = join(directory, 'report.json') + cache = new Redis(redisUrl) + subscriber = new Redis(redisUrl) + subscriber.on('message', (_channel, message) => published.push(JSON.parse(message))) + await new Promise((resolve) => realtime.listen(0, '127.0.0.1', resolve)) + const address = realtime.address() + if (!address || typeof address === 'string') throw new Error('Realtime fixture failed to bind') + environment.SOCKET_SERVER_URL = `http://127.0.0.1:${address.port}` + await subscriber.subscribe('mcp:workflow_tools_changed') + await admin.unsafe(`CREATE DATABASE "${name}"`) + await client.unsafe( + 'CREATE EXTENSION vector; CREATE EXTENSION btree_gin; CREATE EXTENSION pg_trgm' + ) + const folder = join(directory, 'migrations') + await mkdir(join(folder, 'meta'), { recursive: true }) + const expansion = journal.entries.find((entry) => entry.tag === '0406_workspace_project_column') + if (!expansion) throw new Error('Missing expansion migration') + const entries = journal.entries.filter((entry) => entry.when <= expansion.when) + const source = new URL('../../../../../packages/db/migrations/', import.meta.url) + for (const entry of entries) + await copyFile(new URL(`${entry.tag}.sql`, source), join(folder, `${entry.tag}.sql`)) + await writeFile(join(folder, 'meta/_journal.json'), JSON.stringify({ ...journal, entries })) + await execute( + 'bun', + [ + '--no-env-file', + '-e', + ` + import postgres from 'postgres'; + import { drizzle } from 'drizzle-orm/postgres-js'; + import { migrate } from 'drizzle-orm/postgres-js/migrator'; + import { runScriptMigrations, scriptMigrations } from '@sim/db/script-migrations/index'; + const sql = postgres(process.env.MIGRATION_DATABASE_URL,{max:1,onnotice:()=>{}}); + try { await migrate(drizzle(sql),{migrationsFolder:process.env.FIXTURE_MIGRATIONS}); await runScriptMigrations(sql, scriptMigrations.filter((item) => item.name !== '0031_project_membership')); } + finally { await sql.end(); } + `, + ], + { + cwd: new URL('../../../../../packages/db/', import.meta.url), + env: { ...environment, FIXTURE_MIGRATIONS: folder }, + timeout: 120000, + } + ) + await client`SELECT pg_advisory_lock(hashtextextended('sim:project-backfill-operator', 0))` + try { + await switchProjectMembershipAuthority(client) + } finally { + await client`SELECT pg_advisory_unlock(hashtextextended('sim:project-backfill-operator', 0))` + } + }, 120000) + + afterAll(async () => { + releaseNotifications.resolve() + await new Promise((resolve, reject) => { + realtime.close((error) => (error ? reject(error) : resolve())) + realtime.closeAllConnections() + }) + await cache.del(cacheKey) + await Promise.all([cache.quit(), subscriber.quit()]) + await client.end({ timeout: 2 }) + await admin.unsafe(`DROP DATABASE IF EXISTS "${name}" WITH (FORCE)`) + await admin.end() + await rm(directory, { recursive: true, force: true }) + }) + + it.each(['workspace', 'project', 'row'] as const)( + 'defers a busy %s lock, repairs unrelated environments and resumes without losing progress', + async (lock) => { + const ownerId = `owner-${lock}` + const busyId = `busy-${lock}` + const freeId = `free-${lock}` + const projectId = `project-${lock}` + const artifacts = { + manifest: join(directory, `${lock}-manifest.json`), + report: join(directory, `${lock}-report.json`), + } + const holder = postgres(url.toString(), { max: 1, onnotice: () => {} }) + try { + await client`INSERT INTO "user" (id,name,email,email_verified,created_at,updated_at) + VALUES (${ownerId},'Owner',${`${lock}@fixture.test`},true,now(),now())` + await client`INSERT INTO project (id,name,owner_id,archived_at) + VALUES (${projectId},'Stale active Project',${ownerId},NULL)` + await client`INSERT INTO workspace (id,name,owner_id,billed_account_user_id,project_id,archived_at) + VALUES (${busyId},'Busy',${ownerId},${ownerId},${projectId},'2026-05-01'), + (${freeId},'Free',${ownerId},${ownerId},NULL,'2026-05-01')` + await client`INSERT INTO workflow (id,name,user_id,workspace_id,last_synced,created_at,updated_at) + VALUES (${busyId},'Busy flow',${ownerId},${busyId},now(),now(),now()), + (${freeId},'Free flow',${ownerId},${freeId},now(),now(),now())` + await run('plan', artifacts) + await holder.begin(async (tx) => { + if (lock === 'row') await tx`SELECT id FROM workspace WHERE id = ${busyId} FOR SHARE` + else { + const key = lock === 'workspace' ? `project-backfill:${busyId}` : `project:${projectId}` + await tx`SELECT pg_advisory_xact_lock(hashtextextended(${key},0))` + } + await expect(run('repair', artifacts)).rejects.toMatchObject({ code: 2 }) + expect( + await client`SELECT workspace_id FROM project_backfill_archive_repairs WHERE workspace_id = ${busyId}` + ).toHaveLength(0) + expect(JSON.parse(await readFile(artifacts.report, 'utf8'))).toMatchObject({ + status: 'incomplete', + repairsCompleted: [freeId], + }) + expect(await client`SELECT workspace_id FROM workflow WHERE archived_at IS NULL`).toEqual( + [{ workspace_id: busyId }] + ) + }) + await expect(run('repair', artifacts)).rejects.toMatchObject({ code: 2 }) + expect(JSON.parse(await readFile(artifacts.report, 'utf8')).repairsCompleted).toEqual( + expect.arrayContaining([freeId, busyId]) + ) + expect(await client`SELECT archived_at::text FROM project WHERE id = ${projectId}`).toEqual( + [{ archived_at: '2026-05-01 00:00:00' }] + ) + await run('apply', artifacts) + await run('verify', artifacts) + } finally { + await holder.end({ timeout: 2 }) + await client`DELETE FROM workspace WHERE owner_id = ${ownerId}` + await client`DELETE FROM project WHERE owner_id = ${ownerId}` + await client`DELETE FROM "user" WHERE id = ${ownerId}` + } + }, + 60000 + ) + + it.each(['column', 'mixed'] as const)( + 'repairs an explicitly reviewed detached family with %s membership and replays a lost report', + async (membership) => { + const artifacts = { + manifest: join(directory, `detach-${membership}-manifest.json`), + report: join(directory, `detach-${membership}-report.json`), + } + try { + await client`INSERT INTO "user" (id,name,email,email_verified,created_at,updated_at) + VALUES ('detach-owner','Owner','detach@fixture.test',true,now(),now())` + await client`INSERT INTO project (id,name,owner_id) VALUES ('detach-project','Keep context','detach-owner')` + await client`INSERT INTO workspace (id,name,owner_id,billed_account_user_id,project_id) + VALUES ('retained-root','Retained','detach-owner','detach-owner','detach-project'), + ('detached-root','Detached','detach-owner','detach-owner','detach-project')` + await client`INSERT INTO workspace (id,name,owner_id,billed_account_user_id,project_id,forked_from_workspace_id) + VALUES ('detached-child','Child','detach-owner','detach-owner','detach-project','detached-root')` + if (membership === 'mixed') { + await client`INSERT INTO project_workspace (project_id,workspace_id) + VALUES ('detach-project','retained-root'),('detach-project','detached-child')` + await client`UPDATE workspace SET project_id=NULL WHERE id IN ('retained-root','detached-child')` + } + await expect(run('plan', artifacts)).rejects.toMatchObject({ code: 2 }) + const plan = JSON.parse(await readFile(artifacts.manifest, 'utf8')) + plan.groupings[0].decision = 'detach' + plan.groupings[0].detachRootId = 'detached-root' + await writeFile(artifacts.manifest, JSON.stringify(plan)) + const before = + await client`SELECT id,owner_id,organization_id,archived_at,forked_from_workspace_id FROM workspace ORDER BY id` + const holder = postgres(url.toString(), { max: 1 }) + try { + await holder.begin(async (tx) => { + await tx`SELECT pg_advisory_xact_lock(hashtextextended('project:detach-project',0))` + await expect(run('repair', artifacts)).rejects.toMatchObject({ code: 2 }) + expect(await client`SELECT id FROM project WHERE owner_id='detach-owner'`).toHaveLength( + 1 + ) + }) + if (membership === 'mixed') { + await client`INSERT INTO project (id,name,owner_id) VALUES ('changed-project','Changed','detach-owner')` + await holder.begin(async (tx) => { + await tx`UPDATE project_workspace SET project_id='changed-project' WHERE workspace_id='retained-root'` + await expect(run('repair', artifacts)).rejects.toMatchObject({ code: 2 }) + expect( + await client`SELECT project_id FROM workspace WHERE id='retained-root'` + ).toEqual([{ project_id: null }]) + expect( + await client`SELECT id FROM project WHERE id=${plan.groupings[0].destinationProjectId}` + ).toHaveLength(0) + }) + await expect(run('repair', artifacts)).rejects.toMatchObject({ code: 1 }) + expect(await client`SELECT project_id FROM workspace WHERE id='retained-root'`).toEqual( + [{ project_id: null }] + ) + await client`UPDATE project_workspace SET project_id='detach-project' WHERE workspace_id='retained-root'` + await client`DELETE FROM project WHERE id='changed-project'` + } + } finally { + await holder.end() + } + await client`UPDATE workspace SET archived_at='2026-05-01' WHERE id='detached-child'` + await expect(run('repair', artifacts)).rejects.toMatchObject({ code: 1 }) + expect(await client`SELECT id FROM project WHERE owner_id='detach-owner'`).toHaveLength(1) + await client`UPDATE workspace SET archived_at=NULL WHERE id='detached-child'` + await expect(run('repair', artifacts)).rejects.toMatchObject({ code: 2 }) + const destination = plan.groupings[0].destinationProjectId + expect( + await client`SELECT id FROM workspace WHERE project_id = ${destination} ORDER BY id` + ).toEqual([{ id: 'detached-child' }, { id: 'detached-root' }]) + expect(await client`SELECT id,name FROM project WHERE id = 'detach-project'`).toEqual([ + { id: 'detach-project', name: 'Keep context' }, + ]) + expect( + await client`SELECT id,owner_id,organization_id,archived_at,forked_from_workspace_id FROM workspace ORDER BY id` + ).toEqual(before) + await rm(artifacts.report) + await expect(run('repair', artifacts)).rejects.toMatchObject({ code: 2 }) + expect(await client`SELECT id FROM project WHERE owner_id = 'detach-owner'`).toHaveLength(2) + } finally { + await client`DELETE FROM workspace WHERE owner_id = 'detach-owner'` + await client`DELETE FROM project WHERE owner_id = 'detach-owner'` + await client`DELETE FROM "user" WHERE id = 'detach-owner'` + } + }, + 60000 + ) + + it('keeps a Project active when another active environment still uses legacy membership', async () => { + const artifacts = { + manifest: join(directory, 'legacy-active-manifest.json'), + report: join(directory, 'legacy-active-report.json'), + } + try { + await client`INSERT INTO "user" (id,name,email,email_verified,created_at,updated_at) + VALUES ('legacy-owner','Owner','legacy@fixture.test',true,now(),now())` + await client`INSERT INTO project (id,name,owner_id) VALUES ('legacy-project','Project','legacy-owner')` + await client`INSERT INTO workspace (id,name,owner_id,billed_account_user_id,project_id,archived_at) + VALUES ('legacy-archive','Archived','legacy-owner','legacy-owner','legacy-project','2026-05-01'), + ('legacy-live','Live','legacy-owner','legacy-owner',NULL,NULL)` + await client`INSERT INTO project_workspace (project_id,workspace_id) VALUES ('legacy-project','legacy-live'),('legacy-project','legacy-archive')` + await client`INSERT INTO workflow (id,name,user_id,workspace_id,last_synced,created_at,updated_at) + VALUES ('legacy-flow','Flow','legacy-owner','legacy-archive',now(),now(),now())` + await run('plan', artifacts) + await expect(run('repair', artifacts)).rejects.toMatchObject({ code: 2 }) + expect(await client`SELECT archived_at FROM project WHERE id='legacy-project'`).toEqual([ + { archived_at: null }, + ]) + } finally { + await client`DELETE FROM workspace WHERE owner_id='legacy-owner'` + await client`DELETE FROM project WHERE id='legacy-project'` + await client`DELETE FROM "user" WHERE id='legacy-owner'` + } + }, 60000) + + it('notifies healthy local MCP subscribers and completes cleanup after another subscriber throws', async () => { + const serverId = generateId() + try { + await client`INSERT INTO "user" (id,name,email,email_verified,created_at,updated_at) + VALUES ('local-owner','Owner','local@fixture.test',true,now(),now())` + await client`INSERT INTO project (id,name,owner_id) VALUES ('local-project','Project','local-owner')` + await client`INSERT INTO workspace (id,name,owner_id,billed_account_user_id,project_id,archived_at) + VALUES ('local-env','Archived','local-owner','local-owner','local-project','2026-05-01')` + await client`INSERT INTO workflow (id,name,user_id,workspace_id,last_synced,created_at,updated_at) + VALUES ('local-flow','Flow','local-owner','local-env',now(),now(),now())` + await client`INSERT INTO workflow_mcp_server (id,workspace_id,created_by,name,is_public) + VALUES (${serverId},'local-env','local-owner','Server',true)` + await client`INSERT INTO workflow_mcp_tool (id,server_id,workflow_id,tool_name) + VALUES ('local-tool',${serverId},'local-flow','fixture')` + await execute( + 'bun', + [ + '--no-env-file', + '-e', + ` + import { repairArchivedProjectEnvironment } from '@/lib/projects/backfill-repair'; + import { mcpPubSub } from '@/lib/mcp/pubsub'; + import { db, dbReplica } from '@sim/db'; + import assert from 'node:assert/strict'; + import postgres from 'postgres'; + const client = postgres(process.env.MIGRATION_DATABASE_URL, {max:1}); + const unsubscribe = mcpPubSub.onWorkflowToolsChanged(() => { throw new Error('fixture subscriber failed'); }); + const received = []; + const unsubscribeHealthy = mcpPubSub.onWorkflowToolsChanged((event) => received.push(event)); + try { + await repairArchivedProjectEnvironment(client, {workspaceId:'local-env', archivedAt:'2026-05-01 00:00:00', workflowIds:['local-flow']}, 'local-subscriber'); + assert.ok(received.some((event) => event.serverId === '${serverId}' && event.workspaceId === 'local-env'), 'Healthy subscriber missed the archive notification'); + } finally { + unsubscribe(); unsubscribeHealthy(); mcpPubSub.dispose(); await client.end(); + await Promise.all([...new Set([db.$client,dbReplica.$client])].map(client=>client.end())); + } + `, + ], + { + cwd: new URL('../../../', import.meta.url), + env: { ...environment, REDIS_URL: '' }, + timeout: 45000, + } + ) + expect( + await client`SELECT completed_at IS NOT NULL AS complete FROM public.project_backfill_archive_repairs WHERE workspace_id = 'local-env'` + ).toEqual([{ complete: true }]) + } finally { + await client`DELETE FROM workspace WHERE id = 'local-env'` + await client`DELETE FROM project WHERE id = 'local-project'` + await client`DELETE FROM "user" WHERE id = 'local-owner'` + } + }, 60000) + + it('commits the complete cascade, reports external failure and resumes cleanup from its original manifest', async () => { + await client`INSERT INTO "user" (id,name,email,email_verified,created_at,updated_at) VALUES ('owner','Owner','repair@fixture.test',true,now(),now())` + await client`INSERT INTO workspace (id,name,owner_id,billed_account_user_id,archived_at) VALUES ('env','Archived','owner','owner','2026-05-01')` + await client`INSERT INTO workflow (id,name,user_id,workspace_id,last_synced,created_at,updated_at,is_deployed,is_public_api) + VALUES ('flow','Flow','owner','env',now(),now(),now(),true,true)` + await client`INSERT INTO workflow (id,name,user_id,workspace_id,last_synced,created_at,updated_at) + SELECT 'slow-' || n,'Slow notification ' || n,'owner','env',now(),now(),now() FROM generate_series(1,7) n + UNION ALL SELECT 'tail','Last notification','owner','env',now(),now(),now()` + await client`INSERT INTO workflow_schedule (id,workflow_id,trigger_type,status,next_run_at,last_queued_at) VALUES ('schedule','flow','schedule','active',now(),now())` + await client`INSERT INTO webhook (id,workflow_id,provider,path,provider_config) VALUES ('hook','flow','gitlab','fixture-hook','{}')` + await client`INSERT INTO chat (id,workflow_id,user_id,identifier,title) VALUES ('chat','flow','owner','repair-chat','Chat')` + await client`INSERT INTO workflow_deployment_version (id,workflow_id,version,state,is_active) VALUES ('deployment','flow',1,'{}',true)` + await client`INSERT INTO workflow_mcp_server (id,workspace_id,created_by,name,is_public) VALUES (${mcpServerId},'env','owner','Server',true)` + await client`INSERT INTO workflow_mcp_tool (id,server_id,workflow_id,tool_name) VALUES ('tool',${mcpServerId},'flow','fixture')` + await client`INSERT INTO mcp_servers (id,workspace_id,created_by,name,transport) VALUES (${mcpServerId},'env','owner','Cached server','streamable-http')` + await cache.set(cacheKey, JSON.stringify({ tools: [], expiry: Date.now() + 60000 }), 'EX', 60) + await run('plan') + const repair = run('repair').then( + () => null, + (error: unknown) => error + ) + try { + await expect.poll(() => tailNotified, { timeout: 10000 }).toBe(true) + expect(JSON.parse(await readFile(report, 'utf8')).status).toBe('running') + } finally { + releaseNotifications.resolve() + await repair + } + expect(await repair).toMatchObject({ code: 1 }) + expect(await cache.get(cacheKey)).toBeNull() + await expect + .poll(() => published, { timeout: 2000 }) + .toContainEqual({ serverId: mcpServerId, workspaceId: 'env' }) + expect(JSON.parse(await readFile(report, 'utf8'))).toMatchObject({ + status: 'failed', + repairsCompleted: [], + }) + expect(await client`SELECT id FROM workflow`).toHaveLength(9) + expect( + await client`SELECT id FROM workflow WHERE archived_at IS DISTINCT FROM '2026-05-01'::timestamp OR is_deployed OR is_public_api` + ).toHaveLength(0) + expect(await client`SELECT status,next_run_at,last_queued_at FROM workflow_schedule`).toEqual([ + { status: 'disabled', next_run_at: null, last_queued_at: null }, + ]) + expect( + await client`SELECT id FROM webhook WHERE archived_at IS NULL OR is_active` + ).toHaveLength(0) + expect(await client`SELECT id FROM chat WHERE archived_at IS NULL OR is_active`).toHaveLength(0) + expect(await client`SELECT id FROM workflow_deployment_version WHERE is_active`).toHaveLength(0) + expect(await client`SELECT id FROM workflow_mcp_tool WHERE archived_at IS NULL`).toHaveLength(0) + expect( + await client`SELECT id FROM workflow_mcp_server WHERE deleted_at IS NULL OR is_public` + ).toHaveLength(0) + expect(await client`SELECT id FROM workspace WHERE project_id IS NULL`).toHaveLength(1) + await expect(run('verify')).rejects.toMatchObject({ code: 2 }) + expect( + await client`SELECT workspace_id FROM project_backfill_archive_repairs WHERE completed_at IS NULL` + ).toEqual([{ workspace_id: 'env' }]) + await expect( + execute( + 'bun', + [ + '--no-env-file', + '-e', + ` + import postgres from 'postgres'; + import { runScriptMigrations, scriptMigrations } from '@sim/db/script-migrations/index'; + const sql = postgres(process.env.MIGRATION_DATABASE_URL, {max:1}); + try { await runScriptMigrations(sql, scriptMigrations.filter((item) => item.name === '0031_project_membership')); } + finally { await sql.end(); } + `, + ], + { + cwd: new URL('../../../../../packages/db/', import.meta.url), + env: environment, + timeout: 15000, + } + ) + ).rejects.toMatchObject({ code: 1 }) + expect(await client`SELECT to_regclass('project_workspace')::text AS connector`).toEqual([ + { connector: 'project_workspace' }, + ]) + /** The fixture's missing provider credentials are remediated without making an external request. */ + await client`UPDATE webhook SET provider = 'generic'` + await expect(run('repair')).rejects.toMatchObject({ code: 2 }) + expect(JSON.parse(await readFile(report, 'utf8'))).toMatchObject({ repairsCompleted: ['env'] }) + expect( + await client`SELECT workspace_id FROM project_backfill_archive_repairs WHERE completed_at IS NULL` + ).toHaveLength(0) + await run('apply') + await run('verify') + expect(await client`SELECT archived_at::text FROM project`).toEqual([ + { archived_at: '2026-05-01 00:00:00' }, + ]) + expect(await client`SELECT project_id FROM workspace`).toEqual( + await client`SELECT id AS project_id FROM project` + ) + expect(await client`SELECT project_id FROM project_workspace`).toHaveLength(0) + }, 120000) + it('finishes the real migration entrypoint with exact reviewed grouping and retries deployment without the private file', async () => { + const artifacts = { + manifest: join(directory, 'entrypoint-manifest.json'), + report: join(directory, 'entrypoint-report.json'), + } + await client`INSERT INTO "user" (id,name,email,email_verified,created_at,updated_at) + VALUES ('entry-owner','Owner','entry@fixture.test',true,now(),now())` + await client`INSERT INTO project (id,name,owner_id) VALUES ('entry-project','Keep shared context','entry-owner')` + await client`INSERT INTO workspace (id,name,owner_id,billed_account_user_id,project_id) + VALUES ('entry-a','A','entry-owner','entry-owner','entry-project'),('entry-b','B','entry-owner','entry-owner','entry-project')` + await expect(run('plan', artifacts)).rejects.toMatchObject({ code: 2 }) + const migrate = (reviewPath?: string) => + execute('bun', ['--no-env-file', 'scripts/migrate.ts'], { + cwd: new URL('../../../../../packages/db/', import.meta.url), + env: { ...environment, PROJECT_BACKFILL_REVIEW_PATH: reviewPath }, + timeout: 120000, + }) + await expect(migrate()).rejects.toMatchObject({ code: 1 }) + const reviewed = JSON.parse(await readFile(artifacts.manifest, 'utf8')) + reviewed.groupings[0].decision = 'retain' + await writeFile(artifacts.manifest, JSON.stringify(reviewed)) + await client`UPDATE workspace SET archived_at='2026-05-01' WHERE id='entry-b'` + await expect(migrate(artifacts.manifest)).rejects.toMatchObject({ code: 1 }) + expect( + await client`SELECT name FROM script_migrations WHERE name='0031_project_membership'` + ).toHaveLength(0) + expect( + await client`SELECT to_regclass('public.project_workspace') IS NOT NULL AS present` + ).toEqual([{ present: true }]) + await client`UPDATE workspace SET archived_at=NULL WHERE id='entry-b'` + await migrate(artifacts.manifest) + expect( + await client`SELECT name FROM script_migrations WHERE name='0031_project_membership'` + ).toHaveLength(1) + expect(await client`SELECT to_regclass('public.project_workspace') AS connector`).toEqual([ + { connector: null }, + ]) + expect( + await client`SELECT id,project_id FROM workspace WHERE owner_id='entry-owner' ORDER BY id` + ).toEqual([ + { id: 'entry-a', project_id: 'entry-project' }, + { id: 'entry-b', project_id: 'entry-project' }, + ]) + await rm(artifacts.manifest) + await migrate() + }, 120000) +}) diff --git a/apps/sim/lib/projects/__integration__/foundation.integration.ts b/apps/sim/lib/projects/__integration__/foundation.integration.ts index eed5e20ea18..e67dbf785bd 100644 --- a/apps/sim/lib/projects/__integration__/foundation.integration.ts +++ b/apps/sim/lib/projects/__integration__/foundation.integration.ts @@ -7,8 +7,6 @@ import { permissionGroup, permissions, project, - projectMembershipRollout, - projectWorkspace, user, userStats, workflow, @@ -27,17 +25,14 @@ import { createDeferred } from '@sim/testing/helpers/deferred' import { featureFlagsMock, featureFlagsMockFns } from '@sim/testing/mocks/feature-flags.mock' import { getErrorMessage, getPostgresErrorCode } from '@sim/utils/errors' import { generateId } from '@sim/utils/id' -import { and, eq, inArray, or, sql } from 'drizzle-orm' +import { and, eq, inArray, sql } from 'drizzle-orm' import { NextRequest } from 'next/server' import { afterAll, beforeEach, describe, expect, it, vi } from 'vitest' import { removeUserFromOrganization, transferOrganizationOwnership, } from '@/lib/billing/organizations/membership' -import { - getProjectAccountDeletionBlockers, - prepareProjectsForAccountDeletion, -} from '@/lib/projects/account-deletion' +import { prepareProjectsForAccountDeletion } from '@/lib/projects/account-deletion' import { archiveProject, createProject, @@ -47,17 +42,10 @@ import { listProjects, renameProject, } from '@/lib/projects/application' -import { - getProjectEnvironmentSource, - getProjectMembershipPhase, -} from '@/lib/projects/environment-source' -import { ProjectConflictError } from '@/lib/projects/errors' import { archiveProjectInTransaction } from '@/lib/projects/lifecycle' import { createProjectRecord, lockProject, - lockWorkspaceProject, - projectBackfillLockKey, splitForkProject, transferWorkspaceProjects, } from '@/lib/projects/membership' @@ -116,20 +104,33 @@ function setProjectsEnabled(enabled: boolean) { } beforeEach(async () => { - await db - .update(projectMembershipRollout) - .set({ phase: 'column' }) - .where(eq(projectMembershipRollout.id, 'membership')) setProjectsEnabled(true) + const rows = + await db.execute(sql`SELECT conname, condeferrable, condeferred, convalidated FROM pg_constraint + WHERE conrelid = 'workspace'::regclass + AND conname IN ('workspace_project_organization_fk', 'workspace_fork_project_fk') + ORDER BY conname`) + expect(rows).toEqual([ + { + conname: 'workspace_fork_project_fk', + condeferrable: true, + condeferred: true, + convalidated: true, + }, + { + conname: 'workspace_project_organization_fk', + condeferrable: true, + condeferred: true, + convalidated: true, + }, + ]) }) -const users: string[] = [] -const organizations: string[] = [] const request = { requestId: 'project-foundation-integration', headers: new Headers() } const checks: { name: string; status: 'passed' | 'failed'; durationMs: number; error?: string }[] = [] -/** Registers a test and records its status and duration in the suite report. */ +/** Exercises durable auth and lifecycle invariants against real Postgres, including concurrent writers. */ function check(name: string, run: () => Promise) { it(name, async () => { const started = performance.now() @@ -154,7 +155,6 @@ async function fixture(org = true, count = 2) { const outsiderId = generateId() const now = new Date() for (const id of [ownerId, teammateId, outsiderId]) { - users.push(id) await db.insert(user).values({ id, email: `${id}@projects.invalid`, @@ -167,7 +167,6 @@ async function fixture(org = true, count = 2) { await db.insert(userStats).values({ id: generateId(), userId: ownerId }) const organizationId = org ? generateId() : null if (organizationId) { - organizations.push(organizationId) await db .insert(organization) .values({ id: organizationId, name: 'Project fixture', slug: organizationId, createdAt: now }) @@ -218,17 +217,15 @@ async function fixture(org = true, count = 2) { /** * Waits until the operation under test is blocked behind `blockerPid`: a session whose * current statement contains `waitingIn` (an advisory lock tag or row-lock clause). - * When the caller owns the transaction, `waitingPid` identifies that exact backend. */ -async function waitUntilBlockedBy(blockerPid: number, waitingIn: string, waitingPid?: number) { +async function waitUntilBlockedBy(blockerPid: number, waitingIn: string) { await expect .poll( async () => ( await db.execute(sql` SELECT 1 FROM pg_stat_activity - WHERE ${waitingPid === undefined ? sql`true` : sql`pid = ${waitingPid}`} - AND ${blockerPid} = ANY(pg_blocking_pids(pid)) + WHERE ${blockerPid} = ANY(pg_blocking_pids(pid)) AND position(${waitingIn.toLowerCase()} in lower(query)) > 0 `) ).length, @@ -259,13 +256,6 @@ async function addOrganizationProject(organizationId: string, ownerId: string) { return { workspaceId, projectId } } -async function useLegacyMemberships(ids: string[], projectId: string) { - await db.transaction(async (tx) => { - await tx.update(workspace).set({ projectId: null }).where(inArray(workspace.id, ids)) - await tx.insert(projectWorkspace).values(ids.map((workspaceId) => ({ projectId, workspaceId }))) - }) -} - async function addWorkflow(workspaceId: string, userId: string) { const id = generateId() const now = new Date() @@ -283,19 +273,6 @@ async function addWorkflow(workspaceId: string, userId: string) { return id } -async function clearFixtures() { - await db.transaction(async (tx) => { - if (users.length) - await tx - .delete(workspace) - .where(or(inArray(workspace.ownerId, users), inArray(workspace.billedAccountUserId, users))) - if (users.length) await tx.delete(project).where(inArray(project.ownerId, users)) - if (organizations.length) - await tx.delete(organization).where(inArray(organization.id, organizations)) - if (users.length) await tx.delete(user).where(inArray(user.id, users)) - }) -} - afterAll(async () => { try { const reportPath = @@ -315,71 +292,81 @@ afterAll(async () => { describe('Project foundation at the database and application boundary', () => { check( - 'completed contraction needs no rollout marker, but incomplete schemas fail closed', + 'application creation, disconnect, organization deletion and archive commit with SQL enforcement', async () => { - const f = await fixture(false, 1) - const rollback = new Error('Restore expansion schema after contraction proof') - await expect( - db.transaction(async (tx) => { - await tx.execute(sql`LOCK TABLE workspace IN ACCESS EXCLUSIVE MODE NOWAIT`) - await tx.execute(sql`DROP TABLE project_membership_rollout`) - await expect(getProjectMembershipPhase(tx)).rejects.toThrow('authority is missing') - await tx.execute(sql`DROP TABLE project_workspace`) - await expect(getProjectMembershipPhase(tx)).rejects.toThrow('authority is missing') - await tx.execute(sql`ALTER TABLE workspace ALTER COLUMN project_id SET NOT NULL`) - await expect(getProjectMembershipPhase(tx)).rejects.toThrow('authority is missing') - await tx.execute(sql`ALTER TABLE project ADD COLUMN organization_scope_key text - GENERATED ALWAYS AS (CASE WHEN organization_id IS NULL THEN 'personal' ELSE 'organization:' || organization_id END) STORED`) - await tx.execute(sql`ALTER TABLE workspace ADD COLUMN organization_scope_key text - GENERATED ALWAYS AS (CASE WHEN organization_id IS NULL THEN 'personal' ELSE 'organization:' || organization_id END) STORED`) - await tx.execute( - sql`ALTER TABLE project ADD CONSTRAINT project_id_organization_scope_unique UNIQUE (id, organization_scope_key)` - ) - await tx.execute( - sql`ALTER TABLE workspace ADD CONSTRAINT workspace_id_project_unique UNIQUE (id, project_id)` - ) - await tx.execute(sql`ALTER TABLE workspace ADD CONSTRAINT workspace_project_organization_fk - FOREIGN KEY (project_id, organization_scope_key) REFERENCES project(id, organization_scope_key) DEFERRABLE INITIALLY DEFERRED`) - await tx.execute(sql`ALTER TABLE workspace ADD CONSTRAINT workspace_fork_project_fk - FOREIGN KEY (forked_from_workspace_id, project_id) REFERENCES workspace(id, project_id) DEFERRABLE INITIALLY DEFERRED`) - await tx.execute( - sql`ALTER TABLE workspace VALIDATE CONSTRAINT workspace_project_id_project_id_fk` - ) - expect(await getProjectMembershipPhase(tx)).toBe('column') - const created = await createWorkspaceInTransaction(tx, { - userId: f.ownerId, - name: 'After contraction', - organizationId: null, - observedOrganizationId: null, - governingPermissionGroupOrganizationId: null, - workspaceMode: 'personal', - billedAccountUserId: f.ownerId, - skipDefaultWorkflow: true, - }) - const environments = await getProjectEnvironmentSource(tx) - const [row] = await tx - .select({ projectId: environments.projectId }) - .from(environments) - .where(eq(environments.id, created.id)) - expect(row.projectId).toEqual(expect.any(String)) - throw rollback + const f = await fixture(true, 1) + const organizationId = f.organizationId + if (!organizationId) throw new Error('Missing organization fixture') + const created = await createProject.execute({ + principal: f.owner, + input: { + organizationId: f.organizationId, + name: 'Enforced', + initialEnvironment: { name: 'Production' }, + }, + request, + }) + + const source = await getWorkspaceWithOwner(created.initialEnvironment.id) + if (!source) throw new Error('Missing source environment') + const resolved = await getWorkspaceProject.execute({ + principal: f.owner, + input: { workspaceId: source.id }, + request, + }) + expect(resolved.project.id).toBe(created.project.id) + expect(resolved.project).not.toHaveProperty('organizationScopeKey') + const fork = await createFork({ + source, + policy: await getWorkspaceCreationPolicy({ userId: f.ownerId }), + userId: f.ownerId, + name: 'Staging', + }) + const [forkEnvironment] = await db + .select({ projectId: workspace.projectId }) + .from(workspace) + .where(eq(workspace.id, fork.workspace.id)) + expect(forkEnvironment.projectId).toBe(created.project.id) + + await unlinkForkEdge({ parentWorkspaceId: source.id, childWorkspaceId: fork.workspace.id }) + const [detached] = await db + .select({ + projectId: workspace.projectId, + workspaceId: workspace.id, }) - ).rejects.toBe(rollback) + .from(workspace) + .where(eq(workspace.id, fork.workspace.id)) + expect(detached.projectId).not.toBe(created.project.id) + await db.transaction(async (tx) => { + await detachOrganizationWorkspacesTx(tx, organizationId) + await tx.delete(organization).where(eq(organization.id, organizationId)) + }) + await archiveProject.execute({ + principal: f.owner, + input: { projectId: created.project.id }, + request, + }) + const [archived] = await db.select().from(project).where(eq(project.id, created.project.id)) + expect(archived.organizationId).toBeNull() + expect(archived.archivedAt).not.toBeNull() + expect( + await db + .select() + .from(workflow) + .where(and(eq(workflow.workspaceId, source.id), sql`${workflow.archivedAt} IS NULL`)) + ).toHaveLength(0) } ) - for (const phase of ['connector', 'column'] as const) { - check(`workspace creation and fork/disconnect use only ${phase} authority`, async () => { + check( + 'workspace creation and fork/disconnect assign column membership while APIs remain disabled', + async () => { setProjectsEnabled(false) const f = await fixture(false, 1) - await db - .update(projectMembershipRollout) - .set({ phase }) - .where(eq(projectMembershipRollout.id, 'membership')) const source = await db.transaction((tx) => createWorkspaceInTransaction(tx, { userId: f.ownerId, - name: 'Source', + name: 'Legacy source', organizationId: null, observedOrganizationId: null, governingPermissionGroupOrganizationId: null, @@ -388,204 +375,28 @@ describe('Project foundation at the database and application boundary', () => { skipDefaultWorkflow: true, }) ) + const [sourceMembership] = await db + .select({ projectId: workspace.projectId }) + .from(workspace) + .where(eq(workspace.id, source.id)) const parent = await getWorkspaceWithOwner(source.id) if (!parent) throw new Error('Missing source fixture') const fork = await createFork({ source: parent, policy: await getWorkspaceCreationPolicy({ userId: f.ownerId }), userId: f.ownerId, - name: 'Child', + name: 'Legacy child', }) - async function membership(id: string) { - return db.transaction(async (tx) => { - const environments = await getProjectEnvironmentSource(tx) - const [row] = await tx - .select({ projectId: environments.projectId }) - .from(environments) - .where(eq(environments.id, id)) - return row?.projectId - }) - } - const sourceProject = await membership(source.id) - expect(sourceProject).toEqual(expect.any(String)) - expect(await membership(fork.workspace.id)).toBe(sourceProject) - await unlinkForkEdge({ parentWorkspaceId: source.id, childWorkspaceId: fork.workspace.id }) - const childProject = await membership(fork.workspace.id) - expect(childProject).toEqual(expect.any(String)) - expect(childProject).not.toBe(sourceProject) - if (!childProject) throw new Error('Detached environment is missing its Project') - const rows = await db - .select({ projectId: workspace.projectId, parent: workspace.forkedFromWorkspaceId }) + const [forkMembership] = await db + .select({ projectId: workspace.projectId }) .from(workspace) - .where(inArray(workspace.id, [source.id, fork.workspace.id])) - expect(rows.every((row) => row.parent === null)).toBe(true) - const connectors = await db - .select({ id: projectWorkspace.workspaceId }) - .from(projectWorkspace) - .where(inArray(projectWorkspace.workspaceId, [source.id, fork.workspace.id])) - expect(connectors).toHaveLength(phase === 'connector' ? 2 : 0) - expect( - rows.every((row) => - phase === 'connector' ? row.projectId === null : row.projectId !== null - ) - ).toBe(true) - if (phase === 'connector') { - /** The old Project-first teardown must remove a newly created private environment. */ - await db.transaction(async (tx) => { - await tx.delete(projectWorkspace).where(eq(projectWorkspace.projectId, childProject)) - await tx.delete(project).where(eq(project.id, childProject)) - await tx.delete(workspace).where(eq(workspace.id, fork.workspace.id)) - }) - expect(await membership(fork.workspace.id)).toBeUndefined() - } - }) - } - - check( - 'authority contention is bounded and maps to a Project conflict for reads and writers', - async () => { - const f = await fixture(false, 1) - const entered = createDeferred() - const release = createDeferred() - const holder = db.transaction(async (tx) => { - await tx.execute(sql`LOCK TABLE workspace IN ACCESS EXCLUSIVE MODE NOWAIT`) - entered.resolve() - await release.promise - }) - try { - await entered.promise - await Promise.all( - [ - db.transaction( - async (tx) => { - await tx.execute(sql`SET LOCAL statement_timeout = '7s'`) - await getProjectMembershipPhase(tx) - }, - { isolationLevel: 'repeatable read', accessMode: 'read only' } - ), - db.transaction(async (tx) => { - await tx.execute(sql`SET LOCAL statement_timeout = '7s'`) - await lockProject(tx, f.projectId) - }), - ].map((operation) => expect(operation).rejects.toBeInstanceOf(ProjectConflictError)) - ) - } finally { - release.resolve() - await holder - } - await db.transaction(async (tx) => { - await tx.execute(sql`SET LOCAL lock_timeout = '1234ms'`) - expect(await getProjectMembershipPhase(tx)).toBe('column') - const [setting] = await tx.execute<{ lock_timeout: string }>(sql`SHOW lock_timeout`) - expect(setting.lock_timeout).toBe('1234ms') - }) - } - ) - - check('authority barrier excludes cutover until a connector transaction commits', async () => { - await db - .update(projectMembershipRollout) - .set({ phase: 'connector' }) - .where(eq(projectMembershipRollout.id, 'membership')) - const entered = createDeferred() - const release = createDeferred() - const reader = db.transaction(async (tx) => { - expect(await getProjectMembershipPhase(tx)).toBe('connector') - entered.resolve() - await release.promise - expect(await getProjectMembershipPhase(tx)).toBe('connector') - }) - try { - await entered.promise - await expect( - db.transaction(async (tx) => { - await tx.execute(sql`LOCK TABLE workspace IN ACCESS EXCLUSIVE MODE NOWAIT`) - await tx.update(projectMembershipRollout).set({ phase: 'column' }) - }) - ).rejects.toSatisfy((error: unknown) => getPostgresErrorCode(error) === '55P03') - } finally { - release.resolve() - await reader - } - await db.transaction(async (tx) => { - await tx.execute(sql`LOCK TABLE workspace IN ACCESS EXCLUSIVE MODE NOWAIT`) - await tx.update(projectMembershipRollout).set({ phase: 'column' }) - }) - expect(await db.transaction((tx) => getProjectMembershipPhase(tx))).toBe('column') - }) - - check( - 'Project snapshot waits before its first SELECT and observes committed cutover', - async () => { - const f = await fixture(false, 1) - await useLegacyMemberships(f.ids, f.projectId) - await db.update(projectMembershipRollout).set({ phase: 'connector' }) - const entered = createDeferred() - const release = createDeferred() - const cutover = db.transaction(async (tx) => { - await tx.execute(sql`LOCK TABLE workspace IN ACCESS EXCLUSIVE MODE NOWAIT`) - const [backend] = await tx.execute<{ pid: number }>(sql`SELECT pg_backend_pid() AS pid`) - await tx.update(projectMembershipRollout).set({ phase: 'column' }) - await tx - .update(workspace) - .set({ projectId: f.projectId, name: 'Committed after cutover' }) - .where(eq(workspace.id, f.ids[0])) - await tx.delete(projectWorkspace).where(eq(projectWorkspace.workspaceId, f.ids[0])) - entered.resolve(backend.pid) - await release.promise - }) - let read: ReturnType | undefined - try { - const pid = await entered.promise - read = getProject.execute({ - principal: f.owner, - input: { projectId: f.projectId }, - request, - }) - await waitUntilBlockedBy(pid, 'LOCK TABLE') - } finally { - release.resolve() - await cutover - } - if (!read) throw new Error('Project snapshot read was not started') - const result = await read - expect(result.project.environments.map((row) => row.id)).toEqual(f.ids) - expect(result.project.environments[0].name).toBe('Committed after cutover') - } - ) - - check( - 'missing authority refuses membership reads and creation without partial writes', - async () => { - const f = await fixture(false, 1) - await db.delete(projectMembershipRollout) - try { - await expect( - getProject.execute({ principal: f.owner, input: { projectId: f.projectId }, request }) - ).rejects.toThrow('Project membership authority is missing') - await expect( - db.transaction((tx) => - createWorkspaceInTransaction(tx, { - userId: f.ownerId, - name: 'Refused', - organizationId: null, - observedOrganizationId: null, - governingPermissionGroupOrganizationId: null, - workspaceMode: 'personal', - billedAccountUserId: f.ownerId, - skipDefaultWorkflow: true, - }) - ) - ).rejects.toThrow('Project membership authority is missing') - expect( - await db - .select({ id: workspace.id }) - .from(workspace) - .where(eq(workspace.ownerId, f.ownerId)) - ).toHaveLength(1) - } finally { - await db.insert(projectMembershipRollout).values({ id: 'membership', phase: 'column' }) - } + .where(eq(workspace.id, fork.workspace.id)) + expect(forkMembership.projectId).toBe(sourceMembership.projectId) + await unlinkForkEdge({ parentWorkspaceId: source.id, childWorkspaceId: fork.workspace.id }) + const [child] = await db.select().from(workspace).where(eq(workspace.id, fork.workspace.id)) + expect(child.forkedFromWorkspaceId).toBeNull() + expect(child.projectId).not.toBe(sourceMembership.projectId) + expect(await db.select().from(project).where(eq(project.ownerId, f.ownerId))).toHaveLength(3) } ) @@ -653,7 +464,6 @@ describe('Project foundation at the database and application boundary', () => { .from(workspace) .where(eq(workspace.id, fork.workspace.id)) expect(detached.projectId).not.toBe(f.projectId) - if (!detached.projectId) throw new Error('Missing detached Project assignment') await expect( archiveWorkspace(fork.workspace.id, { requestId: 'detached-fork-archive' }) ).resolves.toMatchObject({ archived: true }) @@ -664,174 +474,6 @@ describe('Project foundation at the database and application boundary', () => { expect(detachedProject.archivedAt).not.toBeNull() }) - check('legacy unassigned forks and disconnects preserve the unassigned lineage', async () => { - const f = await fixture(false, 1) - await db.update(workspace).set({ projectId: null }).where(eq(workspace.id, f.ids[0])) - await db.delete(project).where(eq(project.id, f.projectId)) - const parent = await getWorkspaceWithOwner(f.ids[0]) - if (!parent) throw new Error('Missing legacy source fixture') - const fork = await createFork({ - source: parent, - policy: await getWorkspaceCreationPolicy({ userId: f.ownerId }), - userId: f.ownerId, - name: 'Legacy unassigned child', - }) - await expect( - getWorkspaceProject.execute({ - principal: f.owner, - input: { workspaceId: fork.workspace.id }, - request, - }) - ).rejects.toMatchObject({ code: 'not_found' }) - await unlinkForkEdge({ parentWorkspaceId: parent.id, childWorkspaceId: fork.workspace.id }) - const [child] = await db - .select({ projectId: workspace.projectId, parentId: workspace.forkedFromWorkspaceId }) - .from(workspace) - .where(eq(workspace.id, fork.workspace.id)) - expect(child).toEqual({ projectId: null, parentId: null }) - expect(await db.select().from(project).where(eq(project.ownerId, f.ownerId))).toEqual([]) - }) - - check('fork refuses a partially assigned lineage', async () => { - const f = await fixture(false, 3) - await db - .update(workspace) - .set({ projectId: null }) - .where(inArray(workspace.id, f.ids.slice(0, 2))) - const parent = await getWorkspaceWithOwner(f.ids[1]) - if (!parent) throw new Error('Missing source fixture') - await expect( - createFork({ - source: parent, - policy: await getWorkspaceCreationPolicy({ userId: f.ownerId }), - userId: f.ownerId, - name: 'Invalid child', - }) - ).rejects.toMatchObject({ code: 'conflict' }) - const [child] = await db.select().from(workspace).where(eq(workspace.id, f.ids[1])) - expect(child.forkedFromWorkspaceId).toBe(f.ids[0]) - expect(await db.select().from(workspace).where(eq(workspace.ownerId, f.ownerId))).toHaveLength( - 3 - ) - }) - - check( - 'legacy membership decisions exclude backfill until the writing transaction commits', - async () => { - const f = await fixture(false, 1) - await db.update(workspace).set({ projectId: null }).where(eq(workspace.id, f.ids[0])) - await db.delete(project).where(eq(project.id, f.projectId)) - const [legacyWorkspace] = await db.select().from(workspace).where(eq(workspace.id, f.ids[0])) - expect(legacyWorkspace.projectId).toBeNull() - const read = createDeferred() - const release = createDeferred() - const writer = db.transaction(async (tx) => { - expect(await lockWorkspaceProject(tx, f.ids[0])).toBeNull() - read.resolve() - await release.promise - await tx - .update(workspace) - .set({ name: 'Concurrent legacy edit' }) - .where(eq(workspace.id, f.ids[0])) - }) - try { - await Promise.race([read.promise, writer]) - await db.transaction(async (tx) => { - const [lock] = await tx.execute<{ acquired: boolean }>(sql` - SELECT pg_try_advisory_xact_lock(hashtextextended(${projectBackfillLockKey(f.ids[0])}, 0)) AS acquired - `) - expect(lock.acquired).toBe(false) - }) - } finally { - release.resolve() - await writer - } - await db.transaction(async (tx) => { - await tx.execute( - sql`SELECT pg_advisory_xact_lock(hashtextextended(${projectBackfillLockKey(f.ids[0])}, 0))` - ) - const projectId = await createProjectRecord(tx, { - name: 'Concurrent legacy edit', - organizationId: null, - ownerId: f.ownerId, - }) - await tx.update(workspace).set({ projectId }).where(eq(workspace.id, f.ids[0])) - }) - expect( - await db - .select({ - projectId: workspace.projectId, - workspaceId: workspace.id, - }) - .from(workspace) - .where(eq(workspace.id, f.ids[0])) - ).toHaveLength(1) - } - ) - - check( - 'a fork waiting for a legacy assignment inherits it without copying the parent column', - async () => { - const f = await fixture(false, 1) - await db.update(workspace).set({ projectId: null }).where(eq(workspace.id, f.ids[0])) - await db.delete(project).where(eq(project.id, f.projectId)) - const parent = await getWorkspaceWithOwner(f.ids[0]) - if (!parent) throw new Error('Missing source fixture') - const policy = await getWorkspaceCreationPolicy({ userId: f.ownerId }) - const locked = createDeferred() - const release = createDeferred() - const backfill = db.transaction(async (tx) => { - await tx.execute( - sql`SELECT pg_advisory_xact_lock(hashtextextended(${projectBackfillLockKey(f.ids[0])}, 0))` - ) - const [connection] = await tx.execute<{ pid: number }>(sql`SELECT pg_backend_pid() AS pid`) - locked.resolve(connection.pid) - await release.promise - const projectId = await createProjectRecord(tx, { - name: 'Backfilled source', - organizationId: null, - ownerId: f.ownerId, - }) - await tx.insert(projectWorkspace).values({ projectId, workspaceId: f.ids[0] }) - return projectId - }) - await Promise.race([locked.promise, backfill]) - const fork = createFork({ - source: parent, - policy, - userId: f.ownerId, - name: 'Concurrent child', - }) - try { - await waitUntilBlockedBy(await locked.promise, "lock='project_backfill'") - } finally { - release.resolve() - } - const [projectId, result] = await Promise.all([backfill, fork]) - const [membership] = await db - .select({ - projectId: workspace.projectId, - workspaceId: workspace.id, - }) - .from(workspace) - .where(eq(workspace.id, result.workspace.id)) - expect(membership.projectId).toBe(projectId) - const [persistedFork] = await db - .select() - .from(workspace) - .where(eq(workspace.id, result.workspace.id)) - expect(persistedFork.projectId).toBe(projectId) - const [legacyParent] = await db.select().from(workspace).where(eq(workspace.id, f.ids[0])) - expect(legacyParent.projectId).toBeNull() - const resolved = await getWorkspaceProject.execute({ - principal: f.owner, - input: { workspaceId: f.ids[0] }, - request, - }) - expect(resolved.project.id).toBe(projectId) - } - ) - check( 'Project creation commits its named first environment and starter workflow in the requested scope', async () => { @@ -1161,6 +803,13 @@ describe('Project foundation at the database and application boundary', () => { expect(rows.map((row) => row.archivedAt)).not.toContain(null) const [record] = await db.select().from(project).where(eq(project.id, f.projectId)) expect(record.archivedAt).not.toBeNull() + expect( + results.flatMap((result) => + result.status === 'fulfilled' && result.value.archivedProject + ? [result.value.archivedProject] + : [] + ) + ).toEqual([{ id: record.id, name: record.name }]) }) check('removing one of several environments keeps the Project active', async () => { @@ -1368,12 +1017,6 @@ describe('Project foundation at the database and application boundary', () => { .from(workspace) .where(inArray(workspace.id, f.ids)) ).toEqual(f.ids.map(() => ({ projectId: f.projectId }))) - expect( - await db - .select({ projectId: workspace.projectId }) - .from(workspace) - .where(inArray(workspace.id, f.ids)) - ).toEqual(f.ids.map(() => ({ projectId: f.projectId }))) expect(await db.select().from(project).where(eq(project.ownerId, f.ownerId))).toHaveLength(1) expect(await unlinkForkEdge(edge)).toEqual({ unlinked: true }) for (const table of [ @@ -1402,17 +1045,6 @@ describe('Project foundation at the database and application boundary', () => { expect(root.projectId).toBe(f.projectId) expect(child.projectId).not.toBe(f.projectId) expect(grandchild.projectId).toBe(child.projectId) - const environments = await db - .select({ id: workspace.id, projectId: workspace.projectId }) - .from(workspace) - .where(inArray(workspace.id, f.ids)) - expect(environments).toEqual( - expect.arrayContaining([ - { id: f.ids[0], projectId: f.projectId }, - { id: f.ids[1], projectId: child.projectId }, - { id: f.ids[2], projectId: child.projectId }, - ]) - ) expect(await unlinkForkEdge(edge)).toEqual({ unlinked: false }) const [group] = await db.select().from(permissionGroup).where(eq(permissionGroup.id, groupId)) expect(group.config).toEqual({ @@ -1429,27 +1061,6 @@ describe('Project foundation at the database and application boundary', () => { } ) - check( - 'disconnect refuses a partially assigned subtree without moving any environments', - async () => { - const f = await fixture(true, 3) - await db.update(workspace).set({ projectId: null }).where(eq(workspace.id, f.ids[2])) - await expect( - unlinkForkEdge({ parentWorkspaceId: f.ids[0], childWorkspaceId: f.ids[1] }) - ).rejects.toMatchObject({ code: 'conflict' }) - const [child] = await db - .select({ - projectId: workspace.projectId, - workspaceId: workspace.id, - }) - .from(workspace) - .where(eq(workspace.id, f.ids[1])) - expect(child.projectId).toBe(f.projectId) - const [edge] = await db.select().from(workspace).where(eq(workspace.id, f.ids[1])) - expect(edge.forkedFromWorkspaceId).toBe(f.ids[0]) - } - ) - check( 'organization moves remove obsolete Project restrictions while preserving unrelated policy', async () => { @@ -1594,62 +1205,6 @@ describe('Project foundation at the database and application boundary', () => { } ) - check( - 'column reassignment waits for a legacy membership writer before locking workspace rows', - async () => { - const f = await fixture(false) - await useLegacyMemberships(f.ids, f.projectId) - const held = createDeferred() - const release = createDeferred() - let replacementId = '' - const legacy = db.transaction(async (tx) => { - await lockProject(tx, f.projectId) - replacementId = await createProjectRecord(tx, { - name: 'Legacy disconnect', - ownerId: f.ownerId, - organizationId: null, - }) - await tx.execute( - sql`SELECT workspace_id FROM project_workspace WHERE workspace_id = ${f.ids[1]} FOR UPDATE` - ) - const [backend] = await tx.execute<{ pid: number }>(sql`SELECT pg_backend_pid() AS pid`) - held.resolve(backend.pid) - await release.promise - await tx.execute( - sql`UPDATE project_workspace SET project_id = ${replacementId} WHERE workspace_id = ${f.ids[1]}` - ) - }) - const blocker = await held.promise - const detachPid = createDeferred() - const detach = db - .transaction(async (tx) => { - const [backend] = await tx.execute<{ pid: number }>(sql`SELECT pg_backend_pid() AS pid`) - detachPid.resolve(backend.pid) - return splitForkProject(tx, f.ids[1]) - }) - .then( - () => null, - (error: unknown) => { - detachPid.reject(error) - return error - } - ) - try { - await waitUntilBlockedBy(blocker, "lock='project'", await detachPid.promise) - } finally { - release.resolve() - await legacy - } - expect(await detach).toMatchObject({ - message: 'Project membership changed; retry the operation', - }) - expect( - await db.execute(sql`SELECT w.project_id, pw.project_id AS connector_id - FROM workspace w JOIN project_workspace pw ON pw.workspace_id = w.id WHERE w.id = ${f.ids[1]}`) - ).toEqual([{ project_id: null, connector_id: replacementId }]) - } - ) - check( 'account teardown re-reads Projects created by an unlink while waiting for the old Project', async () => { @@ -1780,319 +1335,4 @@ describe('Project foundation at the database and application boundary', () => { ).toEqual([]) } ) - - for (const phase of ['connector', 'column'] as const) { - check( - `legacy Project account preview and teardown preserve surviving environments in ${phase} mode`, - async () => { - const f = await fixture() - await useLegacyMemberships(f.ids, f.projectId) - await db.update(projectMembershipRollout).set({ phase }) - if (!f.organizationId) throw new Error('Missing organization fixture') - await db.insert(member).values({ - id: generateId(), - organizationId: f.organizationId, - userId: f.teammateId, - role: 'admin', - createdAt: new Date(), - }) - expect(await getProjectAccountDeletionBlockers(f.ownerId, [])).toEqual([]) - await db.transaction(async (tx) => { - expect(await prepareProjectsForAccountDeletion(tx, f.ownerId, [])).toEqual([]) - }) - const [record] = await db.select().from(project).where(eq(project.id, f.projectId)) - expect(record.ownerId).toBe(f.teammateId) - expect( - await db.select({ id: workspace.id }).from(workspace).where(inArray(workspace.id, f.ids)) - ).toHaveLength(2) - } - ) - } - - check( - 'legacy detach assigns the whole subtree and stale connector rows cannot restore membership', - async () => { - const f = await fixture(false, 3) - await useLegacyMemberships(f.ids, f.projectId) - await unlinkForkEdge({ parentWorkspaceId: f.ids[0], childWorkspaceId: f.ids[1] }) - const rows = await db.select().from(workspace).where(inArray(workspace.id, f.ids)) - const child = rows.find((row) => row.id === f.ids[1]) - const grandchild = rows.find((row) => row.id === f.ids[2]) - expect(child?.projectId).toEqual(expect.any(String)) - expect(child?.projectId).not.toBe(f.projectId) - expect(grandchild?.projectId).toBe(child?.projectId) - expect( - await db - .select({ projectId: projectWorkspace.projectId }) - .from(projectWorkspace) - .where(inArray(projectWorkspace.workspaceId, f.ids)) - ).toEqual(f.ids.map(() => ({ projectId: f.projectId }))) - const current = await getWorkspaceProject.execute({ - principal: f.owner, - input: { workspaceId: f.ids[1] }, - request, - }) - expect(current.project.id).toBe(child?.projectId) - await archiveWorkspace(f.ids[0], request) - const [original] = await db.select().from(project).where(eq(project.id, f.projectId)) - expect(original.archivedAt).not.toBeNull() - const detached = await getWorkspaceProject.execute({ - principal: f.owner, - input: { workspaceId: f.ids[2] }, - request, - }) - expect(detached.project.archivedAt).toBeNull() - } - ) - - check( - 'legacy organization deletion transfers Projects without filling their columns', - async () => { - const f = await fixture() - await useLegacyMemberships(f.ids, f.projectId) - const organizationId = f.organizationId - if (!organizationId) throw new Error('Missing organization fixture') - await db.transaction(async (tx) => { - await detachOrganizationWorkspacesTx(tx, organizationId) - await tx.delete(organization).where(eq(organization.id, organizationId)) - }) - const [record] = await db.select().from(project).where(eq(project.id, f.projectId)) - expect(record).toMatchObject({ organizationId: null, ownerId: f.ownerId }) - expect( - await db - .select({ projectId: workspace.projectId }) - .from(workspace) - .where(inArray(workspace.id, f.ids)) - ).toEqual(f.ids.map(() => ({ projectId: null }))) - } - ) - - check( - 'Project administration includes legacy environments during partial column assignment', - async () => { - const f = await fixture(false) - await useLegacyMemberships([f.ids[1]], f.projectId) - await expect( - renameProject.execute({ - principal: f.teammate, - input: { projectId: f.projectId, name: 'Forbidden rename' }, - request, - }) - ).rejects.toMatchObject({ code: 'forbidden' }) - const visible = await getProject.execute({ - principal: f.owner, - input: { projectId: f.projectId }, - request, - }) - expect(visible.project.environments).toHaveLength(2) - } - ) - - check( - 'connector retirement refuses busy workspace or Project readers without retaining locks', - async () => { - const f = await fixture(false) - await useLegacyMemberships(f.ids, f.projectId) - for (const held of ['workspace', 'project']) { - await db.transaction(async (tx) => { - if (held === 'workspace') - expect((await lockWorkspaceProject(tx, f.ids[0]))?.id).toBe(f.projectId) - else await tx.select().from(project).where(eq(project.id, f.projectId)) - await expect( - isolated.client.begin(async (ddl) => { - await ddl`LOCK TABLE workspace, project, project_workspace IN ACCESS EXCLUSIVE MODE NOWAIT` - await ddl`DROP TABLE project_workspace` - }) - ).rejects.toSatisfy((error: unknown) => getPostgresErrorCode(error) === '55P03') - await isolated.client.begin(async (writer) => { - await writer`SET LOCAL lock_timeout = '100ms'` - await writer`UPDATE workspace SET name = 'Still writable' WHERE id = ${f.ids[0]}` - }) - }) - } - } - ) - - check( - 'account teardown after a legacy detach removes only obsolete connector references', - async () => { - const f = await fixture(false) - await useLegacyMemberships(f.ids, f.projectId) - await db - .delete(permissions) - .where(and(eq(permissions.entityId, f.ids[0]), eq(permissions.userId, f.teammateId))) - await db.insert(permissions).values({ - id: generateId(), - userId: f.teammateId, - entityId: f.ids[1], - entityType: 'workspace', - permissionType: 'admin', - }) - await unlinkForkEdge({ parentWorkspaceId: f.ids[0], childWorkspaceId: f.ids[1] }) - const [detached] = await db.select().from(workspace).where(eq(workspace.id, f.ids[1])) - if (!detached.projectId) throw new Error('Missing detached Project') - await db.update(workspace).set({ projectId: f.projectId }).where(eq(workspace.id, f.ids[0])) - await expect( - db.transaction(async (tx) => { - await prepareProjectsForAccountDeletion(tx, f.ownerId, [f.ids[0]]) - throw new Error('Cancel account deletion') - }) - ).rejects.toThrow('Cancel account deletion') - expect( - await db - .select({ workspaceId: projectWorkspace.workspaceId }) - .from(projectWorkspace) - .where(eq(projectWorkspace.projectId, f.projectId)) - ).toHaveLength(2) - await db.transaction(async (tx) => { - const ids = await prepareProjectsForAccountDeletion(tx, f.ownerId, [f.ids[0]]) - expect(ids).toEqual([f.projectId]) - expect( - await tx - .select({ workspaceId: projectWorkspace.workspaceId }) - .from(projectWorkspace) - .where(eq(projectWorkspace.projectId, f.projectId)) - ).toEqual([{ workspaceId: f.ids[0] }]) - await tx.delete(workspace).where(eq(workspace.id, f.ids[0])) - await tx.delete(project).where(inArray(project.id, ids)) - }) - expect( - await db.select({ id: project.id }).from(project).where(eq(project.id, f.projectId)) - ).toEqual([]) - const [surviving] = await db.select().from(project).where(eq(project.id, detached.projectId)) - expect(surviving.ownerId).toBe(f.teammateId) - expect( - await db.select({ id: workspace.id }).from(workspace).where(eq(workspace.id, f.ids[1])) - ).toEqual([{ id: f.ids[1] }]) - } - ) - - check( - 'column-only application operations remain valid after the connector is removed', - async () => { - await clearFixtures() - const f = await fixture(true, 1) - const locked = createDeferred() - const release = createDeferred() - const retirement = isolated.client.begin(async (tx) => { - await tx`LOCK TABLE workspace, project, project_workspace IN ACCESS EXCLUSIVE MODE NOWAIT` - const [connection] = await tx`SELECT pg_backend_pid() AS pid` - locked.resolve(connection.pid) - await release.promise - await tx`DROP TABLE project_workspace` - await tx`ALTER TABLE workspace ALTER COLUMN project_id SET NOT NULL` - await tx`ALTER TABLE workspace VALIDATE CONSTRAINT workspace_project_id_project_id_fk` - }) - await Promise.race([locked.promise, retirement]) - const overlappingRead = getWorkspaceProject.execute({ - principal: f.owner, - input: { workspaceId: f.ids[0] }, - request, - }) - try { - await waitUntilBlockedBy(await locked.promise, 'workspace') - } finally { - release.resolve() - } - const [, overlappingResult] = await Promise.all([retirement, overlappingRead]) - expect(overlappingResult.project.environments.map((row) => row.id)).toEqual(f.ids) - const organizationId = f.organizationId - if (!organizationId) throw new Error('Missing organization fixture') - const created = await createProject.execute({ - principal: f.owner, - input: { - organizationId, - name: 'Column-only Project', - initialEnvironment: { name: 'Production' }, - }, - request, - }) - const source = await getWorkspaceWithOwner(created.initialEnvironment.id) - if (!source) throw new Error('Missing source environment') - const resolved = await getWorkspaceProject.execute({ - principal: f.owner, - input: { workspaceId: source.id }, - request, - }) - expect(resolved.project.id).toBe(created.project.id) - await expect( - getProject.execute({ - principal: createSessionPrincipal({ userId: f.outsiderId }), - input: { projectId: created.project.id }, - request, - }) - ).rejects.toMatchObject({ code: 'not_found' }) - const listed = await listProjects.execute({ - principal: f.owner, - input: { limit: 10 }, - request, - }) - expect(listed.projects.map((row) => row.id)).toEqual( - expect.arrayContaining([f.projectId, created.project.id]) - ) - await renameProject.execute({ - principal: f.owner, - input: { projectId: created.project.id, name: 'Renamed without connector' }, - request, - }) - const fork = await createFork({ - source, - policy: await getWorkspaceCreationPolicy({ userId: f.ownerId }), - userId: f.ownerId, - name: 'Staging', - }) - const [forkEnvironment] = await db - .select({ projectId: workspace.projectId }) - .from(workspace) - .where(eq(workspace.id, fork.workspace.id)) - expect(forkEnvironment.projectId).toBe(created.project.id) - await unlinkForkEdge({ parentWorkspaceId: source.id, childWorkspaceId: fork.workspace.id }) - const [detached] = await db - .select({ projectId: workspace.projectId, parentId: workspace.forkedFromWorkspaceId }) - .from(workspace) - .where(eq(workspace.id, fork.workspace.id)) - if (!detached.projectId) throw new Error('Disconnected environment has no Project') - expect(detached.projectId).not.toBe(created.project.id) - expect(detached.parentId).toBeNull() - await archiveWorkspace(fork.workspace.id, request) - const [detachedProject] = await db - .select({ archivedAt: project.archivedAt }) - .from(project) - .where(eq(project.id, detached.projectId)) - expect(detachedProject.archivedAt).not.toBeNull() - await db.transaction(async (tx) => { - await detachOrganizationWorkspacesTx(tx, organizationId) - await tx.delete(organization).where(eq(organization.id, organizationId)) - }) - await archiveProject.execute({ - principal: f.owner, - input: { projectId: created.project.id }, - request, - }) - const [archived] = await db.select().from(project).where(eq(project.id, created.project.id)) - expect(archived.name).toBe('Renamed without connector') - expect(archived.organizationId).toBeNull() - expect(archived.archivedAt).not.toBeNull() - expect( - await db - .select({ id: workflow.id }) - .from(workflow) - .where(and(eq(workflow.workspaceId, source.id), sql`${workflow.archivedAt} IS NULL`)) - ).toEqual([]) - const privateProject = await fixture(false, 1) - await db.transaction(async (tx) => { - const projectIds = await prepareProjectsForAccountDeletion( - tx, - privateProject.ownerId, - privateProject.ids - ) - expect(projectIds).toEqual([privateProject.projectId]) - await tx.delete(workspace).where(inArray(workspace.id, privateProject.ids)) - await tx.delete(project).where(inArray(project.id, projectIds)) - }) - expect( - await db.select().from(project).where(eq(project.id, privateProject.projectId)) - ).toEqual([]) - } - ) }) diff --git a/apps/sim/lib/projects/account-deletion.ts b/apps/sim/lib/projects/account-deletion.ts index 07ec1ec61ae..5b04e2854e0 100644 --- a/apps/sim/lib/projects/account-deletion.ts +++ b/apps/sim/lib/projects/account-deletion.ts @@ -3,25 +3,16 @@ import { member, permissions, project, workspace } from '@sim/db/schema' import { ORG_ADMIN_ROLES } from '@sim/platform-authz/workspace' import { and, asc, eq, inArray, ne, or, sql } from 'drizzle-orm' import type { DbOrTx, DbTransaction } from '@/lib/db/types' -import { - deleteObsoleteProjectMemberships, - getProjectEnvironmentSource, -} from '@/lib/projects/environment-source' import { ProjectConflictError } from '@/lib/projects/errors' import { lockProjectBackfillWrites, lockProjects } from '@/lib/projects/membership' /** Two indexed lookups; an `OR` around a membership subquery would scan every Project. */ -async function loadRelatedProjects( - executor: DbTransaction, - userId: string, - doomedWorkspaceIds: string[] -) { - const environments = await getProjectEnvironmentSource(executor) +async function loadRelatedProjects(executor: DbOrTx, userId: string, doomedWorkspaceIds: string[]) { const doomedMemberships = doomedWorkspaceIds.length ? await executor - .select({ projectId: environments.projectId }) - .from(environments) - .where(inArray(environments.id, doomedWorkspaceIds)) + .select({ projectId: workspace.projectId }) + .from(workspace) + .where(inArray(workspace.id, doomedWorkspaceIds)) : [] const projectIds = doomedMemberships.flatMap((row) => (row.projectId ? [row.projectId] : [])) return executor @@ -106,17 +97,16 @@ interface ProjectEnvironment { } /** Every environment of `projectIds`, in one query, keyed by Project. */ -async function loadProjectEnvironments(executor: DbTransaction, projectIds: string[]) { - const environments = await getProjectEnvironmentSource(executor) +async function loadProjectEnvironments(executor: DbOrTx, projectIds: string[]) { const rows = projectIds.length ? await executor .select({ - projectId: environments.projectId, - id: environments.id, - archivedAt: environments.archivedAt, + projectId: workspace.projectId, + id: workspace.id, + archivedAt: workspace.archivedAt, }) - .from(environments) - .where(inArray(environments.projectId, projectIds)) + .from(workspace) + .where(inArray(workspace.projectId, projectIds)) : [] const byProject = new Map() for (const { projectId, ...environment } of rows) { @@ -164,27 +154,25 @@ export async function getProjectAccountDeletionBlockers( userId: string, doomedWorkspaceIds: string[] ): Promise { - return db.transaction(async (tx) => { - const records = await loadRelatedProjects(tx, userId, doomedWorkspaceIds) - const environments = await loadProjectEnvironments( - tx, - records.map((record) => record.id) + const records = await loadRelatedProjects(db, userId, doomedWorkspaceIds) + const environments = await loadProjectEnvironments( + db, + records.map((record) => record.id) + ) + const doomed = new Set(doomedWorkspaceIds) + const blockers: string[] = [] + for (const record of records) { + const decision = await planProjectDeletion( + db, + record, + environments.get(record.id) ?? [], + userId, + doomed, + false ) - const doomed = new Set(doomedWorkspaceIds) - const blockers: string[] = [] - for (const record of records) { - const decision = await planProjectDeletion( - tx, - record, - environments.get(record.id) ?? [], - userId, - doomed, - false - ) - if ('blocker' in decision) blockers.push(decision.blocker) - } - return blockers - }) + if ('blocker' in decision) blockers.push(decision.blocker) + } + return blockers } /** Transfers surviving Projects and returns private Projects to delete after their workspaces. */ @@ -244,6 +232,5 @@ export async function prepareProjectsForAccountDeletion( }) .where(eq(project.id, record.id)) } - await deleteObsoleteProjectMemberships(tx, projectsToDelete) return projectsToDelete } diff --git a/apps/sim/lib/projects/application/authorization.ts b/apps/sim/lib/projects/application/authorization.ts index 5daa073a5a6..cc8c193b35c 100644 --- a/apps/sim/lib/projects/application/authorization.ts +++ b/apps/sim/lib/projects/application/authorization.ts @@ -1,5 +1,5 @@ import type { Principal, SessionPrincipal } from '@sim/auth/principal' -import { member, permissionGroup, permissions, project } from '@sim/db/schema' +import { member, permissionGroup, permissions, project, workspace } from '@sim/db/schema' import { createLogger } from '@sim/logger' import { isOrgAdminRole } from '@sim/platform-authz/workspace' import { and, asc, eq, inArray, sql } from 'drizzle-orm' @@ -11,10 +11,6 @@ import { CAPABILITY_RULES, refuseCapability } from '@/lib/permission-groups/capa import { acquirePermissionGroupOrgLock } from '@/lib/permission-groups/locks' import { resolveVerifiedUserAccessControlContext } from '@/lib/permission-groups/resolve.server' import { type ProjectOperation, projectOperations } from '@/lib/projects/application/operations' -import { - getProjectEnvironmentSource, - lockProjectMembershipBarrier, -} from '@/lib/projects/environment-source' import { lockProject } from '@/lib/projects/membership' const logger = createLogger('ProjectAuthorization') @@ -71,24 +67,23 @@ async function loadProjectAccess( const [membership] = records.some((record) => record.organizationId) ? await (lock ? memberQuery.for('share') : memberQuery) : [] - const source = await getProjectEnvironmentSource(tx) const environments = await tx .select({ - projectId: source.projectId, - id: source.id, - name: source.name, - organizationId: source.organizationId, - archivedAt: source.archivedAt, - parentId: source.parentId, + projectId: workspace.projectId, + id: workspace.id, + name: workspace.name, + organizationId: workspace.organizationId, + archivedAt: workspace.archivedAt, + parentId: workspace.forkedFromWorkspaceId, }) - .from(source) + .from(workspace) .where( inArray( - source.projectId, + workspace.projectId, records.map((record) => record.id) ) ) - .orderBy(asc(source.id)) + .orderBy(asc(workspace.id)) const grantQuery = tx .select({ id: permissions.entityId, permission: permissions.permissionType }) .from(permissions) @@ -223,7 +218,6 @@ export async function authorizeProject( input: ProjectAuthorizationInput & { projectId: string }, mode: ProjectAccessMode ): Promise { - await lockProjectMembershipBarrier(tx) if (mode === 'hold') await lockProject(tx, input.projectId) const [record] = await tx.select().from(project).where(eq(project.id, input.projectId)).limit(1) if ( @@ -242,7 +236,6 @@ export async function authorizeProjectsForRead( principal: SessionPrincipal, projectIds: string[] ): Promise { - await lockProjectMembershipBarrier(tx) if (!projectIds.length) return [] const records = await tx .select() diff --git a/apps/sim/lib/projects/application/use-cases.ts b/apps/sim/lib/projects/application/use-cases.ts index baf95bc7745..70af2249d1b 100644 --- a/apps/sim/lib/projects/application/use-cases.ts +++ b/apps/sim/lib/projects/application/use-cases.ts @@ -14,7 +14,6 @@ import { requireProjectPrincipal, } from '@/lib/projects/application/authorization' import { projectOperations } from '@/lib/projects/application/operations' -import { getProjectEnvironmentSource } from '@/lib/projects/environment-source' import { archiveProjectInTransaction, finishProjectArchive } from '@/lib/projects/lifecycle' import { requireProjectApiEnabled } from '@/lib/projects/rollout.server' @@ -24,7 +23,13 @@ const READ_SNAPSHOT = { isolationLevel: 'repeatable read', accessMode: 'read onl function presentProject(context: AuthorizedProject) { return { - ...context.record, + id: context.record.id, + name: context.record.name, + organizationId: context.record.organizationId, + ownerId: context.record.ownerId, + archivedAt: context.record.archivedAt, + createdAt: context.record.createdAt, + updatedAt: context.record.updatedAt, environments: context.environments, capabilities: { administer: context.canAdminister, issues: context.canUseIssues }, } @@ -86,7 +91,6 @@ export const listProjects: OperationUseCase< requireProjectPrincipal(principal, projectOperations.list) await requireProjectApiEnabled() return db.transaction(async (tx) => { - const environments = await getProjectEnvironmentSource(tx) /** Driven from the caller's grants and admin organization, so cost tracks their reach. */ const candidates = await tx.execute<{ id: string }>(sql` WITH accessible AS ( @@ -98,15 +102,15 @@ export const listProjects: OperationUseCase< JOIN ${workspace} ON ${workspace.organizationId} = ${member.organizationId} WHERE ${member.userId} = ${principal.userId} AND ${inArray(member.role, ORG_ADMIN_ROLES)} ) - SELECT DISTINCT ${environments.projectId} AS id + SELECT DISTINCT ${workspace.projectId} AS id FROM accessible - JOIN ${environments} - ON ${environments.id} = accessible.workspace_id AND ${environments.archivedAt} IS NULL + JOIN ${workspace} + ON ${workspace.id} = accessible.workspace_id AND ${workspace.archivedAt} IS NULL JOIN ${project} - ON ${project.id} = ${environments.projectId} AND ${project.archivedAt} IS NULL + ON ${project.id} = ${workspace.projectId} AND ${project.archivedAt} IS NULL WHERE TRUE ${input.organizationId ? sql`AND ${project.organizationId} = ${input.organizationId}` : sql``} - ${input.cursor ? sql`AND ${environments.projectId} > ${input.cursor}` : sql``} + ${input.cursor ? sql`AND ${workspace.projectId} > ${input.cursor}` : sql``} ORDER BY 1 LIMIT ${input.limit + 1} `) @@ -212,11 +216,10 @@ export const getWorkspaceProject: OperationUseCase< requireProjectPrincipal(principal, projectOperations.get) await requireProjectApiEnabled() return db.transaction(async (tx) => { - const environments = await getProjectEnvironmentSource(tx) const [membership] = await tx - .select({ projectId: environments.projectId }) - .from(environments) - .where(eq(environments.id, input.workspaceId)) + .select({ projectId: workspace.projectId }) + .from(workspace) + .where(eq(workspace.id, input.workspaceId)) .limit(1) if (!membership?.projectId) throw new OrchestrationError('not_found', 'Project not found') return { diff --git a/apps/sim/lib/projects/backfill-repair.ts b/apps/sim/lib/projects/backfill-repair.ts new file mode 100644 index 00000000000..cd4ea4ad3b8 --- /dev/null +++ b/apps/sim/lib/projects/backfill-repair.ts @@ -0,0 +1,250 @@ +import { + type ProjectArchiveRepair, + ProjectBackfillBusy, + ProjectBackfillConflict, + type ProjectBackfillWorkspace, + type ProjectGroupingReview, + projectGroupingEvidence, +} from '@sim/db/maintenance/project-backfill' +import { + completeProjectArchiveRepair, + ensureProjectArchiveRepairJournal, +} from '@sim/db/maintenance/project-repairs' +import { assertProjectColumnAuthority } from '@sim/db/maintenance/project-rollout' +import * as schema from '@sim/db/schema' +import { and, eq, inArray, isNull, sql } from 'drizzle-orm' +import { drizzle } from 'drizzle-orm/postgres-js' +import type { Sql } from 'postgres' +import { tryAcquireAdvisoryXactLocks } from '@/lib/db/advisory-locks' +import { archiveProjectWithLastEnvironment, splitForkProject } from '@/lib/projects/membership' +import { + archiveEnvironmentInTransaction, + type EnvironmentArchiveEffects, + finishEnvironmentArchive, +} from '@/lib/workspaces/lifecycle' + +/** + * Repairs reviewed legacy archives before membership enforcement. The immutable manifest retains + * workflow identities before commit, so reruns can finish provider cleanup after a process crash. + */ +export async function repairArchivedProjectEnvironment( + client: Sql, + repair: ProjectArchiveRepair, + requestId: string +): Promise { + await assertProjectColumnAuthority(client) + await ensureProjectArchiveRepairJournal(client) + const connection = drizzle({ client, schema }) + const effects = await connection.transaction(async (tx) => { + await tx.execute(sql`SELECT set_config(CASE WHEN current_setting('transaction_timeout', true) IS NULL + THEN 'idle_in_transaction_session_timeout' ELSE 'transaction_timeout' END, '5s', true)`) + await tx.execute(sql`SET LOCAL statement_timeout = '3s'`) + await tx.execute(sql`SET LOCAL lock_timeout = '250ms'`) + if ( + !(await tryAcquireAdvisoryXactLocks(tx, 'project_backfill', [ + `project-backfill:${repair.workspaceId}`, + ])) + ) + throw new ProjectBackfillBusy('Archive repair workspace is busy') + const [membership] = await tx + .select({ projectId: schema.workspace.projectId }) + .from(schema.workspace) + .where(eq(schema.workspace.id, repair.workspaceId)) + if ( + membership?.projectId && + !(await tryAcquireAdvisoryXactLocks(tx, 'project', [`project:${membership.projectId}`])) + ) + throw new ProjectBackfillBusy('Archive repair Project is busy') + const [current] = await tx + .select({ + projectId: schema.workspace.projectId, + archivedAt: sql`${schema.workspace.archivedAt}::text`, + }) + .from(schema.workspace) + .where(eq(schema.workspace.id, repair.workspaceId)) + .for('no key update', { noWait: true }) + if ( + !current || + current.archivedAt !== repair.archivedAt || + current.projectId !== membership?.projectId + ) + throw new ProjectBackfillConflict('Reviewed archive changed; rediscover') + const active = await tx + .select({ id: schema.workflow.id }) + .from(schema.workflow) + .where( + and(eq(schema.workflow.workspaceId, repair.workspaceId), isNull(schema.workflow.archivedAt)) + ) + if (active.some((row) => !repair.workflowIds.includes(row.id))) + throw new ProjectBackfillConflict('New workflow found in reviewed archive; rediscover') + const moved = await tx + .select({ id: schema.workflow.id }) + .from(schema.workflow) + .where( + and( + inArray(schema.workflow.id, repair.workflowIds), + sql`${schema.workflow.workspaceId} IS DISTINCT FROM ${repair.workspaceId}` + ) + ) + .limit(1) + if (moved.length) throw new ProjectBackfillConflict('Reviewed workflow moved; rediscover') + const recorded = + await tx.execute(sql`INSERT INTO public.project_backfill_archive_repairs (workspace_id, repair) + VALUES (${repair.workspaceId}, ${JSON.stringify(repair)}::text::jsonb) + ON CONFLICT (workspace_id) DO UPDATE SET repair = EXCLUDED.repair, completed_at = NULL + WHERE project_backfill_archive_repairs.repair = EXCLUDED.repair + OR project_backfill_archive_repairs.completed_at IS NOT NULL + RETURNING workspace_id`) + if (!recorded.length) + throw new ProjectBackfillConflict( + 'Finish the pending repair with its original manifest before replacing it' + ) + const archivedAt = new Date(`${repair.archivedAt.replace(' ', 'T')}Z`) + if (membership?.projectId) { + const [legacy] = await tx.execute<{ present: boolean }>( + sql`SELECT to_regclass('public.project_workspace') IS NOT NULL AS present` + ) + const activeLegacy = legacy?.present + ? await tx.execute(sql` + SELECT 1 FROM project_workspace pw JOIN workspace w ON w.id = pw.workspace_id + WHERE pw.project_id = ${membership.projectId} AND w.project_id IS NULL AND w.archived_at IS NULL LIMIT 1`) + : [] + if (!activeLegacy.length) + await archiveProjectWithLastEnvironment( + tx, + membership.projectId, + repair.workspaceId, + archivedAt + ) + } + await archiveEnvironmentInTransaction(tx, repair.workspaceId, archivedAt) + const tools = await tx + .select({ + workflowId: schema.workflowMcpTool.workflowId, + serverId: schema.workflowMcpTool.serverId, + }) + .from(schema.workflowMcpTool) + .where(inArray(schema.workflowMcpTool.workflowId, repair.workflowIds)) + const servers = await tx + .select({ id: schema.workflowMcpServer.id }) + .from(schema.workflowMcpServer) + .where(eq(schema.workflowMcpServer.workspaceId, repair.workspaceId)) + const result: EnvironmentArchiveEffects = { + workspaceId: repair.workspaceId, + workflows: repair.workflowIds.map((id) => ({ + id, + serverIds: tools.filter((tool) => tool.workflowId === id).map((tool) => tool.serverId), + })), + serverIds: servers.map((server) => server.id), + } + return result + }) + await finishEnvironmentArchive(effects, requestId, { strictExternalCleanup: true }) + await completeProjectArchiveRepair(client, repair) +} + +/** Repairs only an operator-named detached root, preserving the shared split policy and a replayable destination ID. */ +export async function repairProjectGrouping( + client: Sql, + review: ProjectGroupingReview +): Promise { + await assertProjectColumnAuthority(client) + const rootId = review.detachRootId + if ( + review.decision !== 'detach' || + !rootId || + !review.roots.includes(rootId) || + review.destinationProjectId === review.projectId || + review.members.length > 1000 || + review.members.some((row) => (row.projectId ?? row.legacyProjectId) !== review.projectId) || + projectGroupingEvidence(review.members) !== review.evidence + ) + throw new ProjectBackfillConflict( + 'Detach requires an exact reviewed Project grouping; rediscover' + ) + const expected = new Map(review.members.map((row) => [row.id, row])) + const root = expected.get(rootId) + if (!root || root.parentId !== null) + throw new ProjectBackfillConflict('Reviewed detached root is not independent') + const targetIds = new Set([rootId]) + for (let size = 0; size !== targetIds.size; ) { + size = targetIds.size + for (const row of review.members) + if (row.parentId && targetIds.has(row.parentId)) targetIds.add(row.id) + } + const target = review.members.filter((row) => targetIds.has(row.id)) + const connection = drizzle({ client, schema }) + await connection.transaction(async (tx) => { + await tx.execute(sql`SELECT set_config(CASE WHEN current_setting('transaction_timeout', true) IS NULL + THEN 'idle_in_transaction_session_timeout' ELSE 'transaction_timeout' END, '5s', true)`) + await tx.execute(sql`SET LOCAL statement_timeout = '3s'`) + await tx.execute(sql`SET LOCAL lock_timeout = '250ms'`) + const keys = [ + `project:${review.projectId}`, + `project:${review.destinationProjectId}`, + ...review.roots.map((id) => `fork-lineage:${id}`), + ...review.members.map((row) => `project-backfill:${row.id}`), + ...(root.organizationId ? [`permission_group:${root.organizationId}`] : []), + ] + if (!(await tryAcquireAdvisoryXactLocks(tx, 'project_backfill', keys))) + throw new ProjectBackfillBusy('Reviewed Project grouping is busy') + const reviewedIds = review.members.map((row) => row.id) + await tx.execute(sql`SELECT id FROM workspace + WHERE ${inArray(schema.workspace.id, reviewedIds)} + ORDER BY id COLLATE "C" FOR NO KEY UPDATE NOWAIT`) + const [legacy] = await tx.execute<{ present: boolean }>( + sql`SELECT to_regclass('public.project_workspace') IS NOT NULL AS present` + ) + if (legacy?.present) { + await tx.execute(sql`SELECT pw.workspace_id FROM project_workspace pw + JOIN workspace ON workspace.id = pw.workspace_id + WHERE ${inArray(schema.workspace.id, reviewedIds)} AND workspace.project_id IS NULL + ORDER BY pw.workspace_id COLLATE "C" FOR SHARE OF pw NOWAIT`) + } + const legacyAssignment = legacy?.present + ? sql`(SELECT pw.project_id FROM project_workspace pw WHERE pw.workspace_id = workspace.id)` + : sql`NULL::text` + const current = await tx.execute(sql` + SELECT id, forked_from_workspace_id AS "parentId", owner_id AS "ownerId", + organization_id AS "organizationId", archived_at::text AS "archivedAt", project_id AS "projectId", ${legacyAssignment} AS "legacyProjectId" + FROM workspace WHERE coalesce(project_id, ${legacyAssignment}) IN (${review.projectId},${review.destinationProjectId}) + ORDER BY id COLLATE "C" LIMIT 1001 FOR NO KEY UPDATE NOWAIT`) + if (current.length > 1000) + throw new ProjectBackfillConflict('Project grouping exceeds repair limit') + const destination = current.filter( + (row) => (row.projectId ?? row.legacyProjectId) === review.destinationProjectId + ) + if (destination.length) { + if ( + projectGroupingEvidence( + destination.map((row) => ({ ...row, projectId: review.projectId })) + ) !== projectGroupingEvidence(target) + ) + throw new ProjectBackfillConflict('Reviewed detach destination changed; rediscover') + return + } + if (projectGroupingEvidence([...current]) !== review.evidence) + throw new ProjectBackfillConflict('Reviewed Project grouping changed; rediscover') + const [source] = await tx + .select({ ownerId: schema.project.ownerId, organizationId: schema.project.organizationId }) + .from(schema.project) + .where(eq(schema.project.id, review.projectId)) + .for('no key update', { noWait: true }) + if ( + !source || + current.some( + (row) => + row.organizationId !== source.organizationId || + (source.organizationId === null && row.ownerId !== source.ownerId) + ) + ) + throw new ProjectBackfillConflict( + 'Reviewed Project ownership or scope requires reconciliation before detach' + ) + await tx + .update(schema.workspace) + .set({ projectId: review.projectId }) + .where(and(inArray(schema.workspace.id, reviewedIds), isNull(schema.workspace.projectId))) + await splitForkProject(tx, rootId, review.destinationProjectId) + }) +} diff --git a/apps/sim/lib/projects/environment-source.ts b/apps/sim/lib/projects/environment-source.ts deleted file mode 100644 index 473179dd7e8..00000000000 --- a/apps/sim/lib/projects/environment-source.ts +++ /dev/null @@ -1,134 +0,0 @@ -import { projectMembershipRollout, projectWorkspace, workspace } from '@sim/db/schema' -import { getPostgresErrorCode } from '@sim/utils/errors' -import { and, eq, exists, inArray, isNotNull, isNull, ne, sql } from 'drizzle-orm' -import type { SubqueryWithSelection } from 'drizzle-orm/pg-core' -import type { DbTransaction } from '@/lib/db/types' -import { ProjectConflictError } from '@/lib/projects/errors' - -const fields = { - id: workspace.id, - projectId: workspace.projectId, - name: workspace.name, - ownerId: workspace.ownerId, - organizationId: workspace.organizationId, - archivedAt: workspace.archivedAt, - parentId: workspace.forkedFromWorkspaceId, -} - -/** - * Bound every authority wait without establishing a repeatable-read snapshot before the lock. - * SHOW and SET are utility statements; restore the caller's setting only after acquiring it. - */ -export async function lockProjectMembershipBarrier(tx: DbTransaction): Promise { - const [setting] = await tx.execute<{ lock_timeout: string }>(sql`SHOW lock_timeout`) - await tx.execute(sql`SET LOCAL lock_timeout = '5s'`) - try { - await tx.execute(sql`LOCK TABLE ${workspace} IN ACCESS SHARE MODE`) - } catch (error) { - const code = getPostgresErrorCode(error) - if (code === '55P03' || code === '40P01') { - throw new ProjectConflictError('Project membership is changing; retry the operation') - } - throw error - } - await tx.execute(sql`SELECT set_config('lock_timeout', ${setting?.lock_timeout ?? '0'}, true)`) -} - -/** The barrier pins database authority through commit; never cache the phase between transactions. */ -export async function getProjectMembershipPhase( - tx: DbTransaction -): Promise<'connector' | 'column'> { - await lockProjectMembershipBarrier(tx) - const [schema] = await tx.execute<{ marker: boolean; complete: boolean }>(sql` - SELECT to_regclass('public.project_membership_rollout') IS NOT NULL AS marker, - to_regclass('public.project_workspace') IS NULL - AND EXISTS (SELECT 1 FROM pg_attribute WHERE attrelid = 'public.workspace'::regclass - AND attname = 'project_id' AND attnotnull AND NOT attisdropped) - AND (SELECT count(*) = 3 FROM pg_constraint - WHERE conrelid = 'public.workspace'::regclass AND contype = 'f' AND convalidated - AND conname IN ('workspace_project_id_project_id_fk', - 'workspace_project_organization_fk', 'workspace_fork_project_fk')) AS complete - `) - if (!schema?.marker) { - if (schema?.complete) return 'column' - throw new Error('Project membership authority is missing or invalid') - } - const [state] = await tx - .select({ phase: projectMembershipRollout.phase }) - .from(projectMembershipRollout) - .where(eq(projectMembershipRollout.id, 'membership')) - if (state?.phase !== 'connector' && state?.phase !== 'column') { - throw new Error('Project membership authority is missing or invalid') - } - return state.phase -} - -/** Caller holds the workspace barrier against connector retirement. */ -async function hasLegacyMemberships(tx: DbTransaction): Promise { - const [legacy] = await tx.execute<{ present: boolean }>( - sql`SELECT to_regclass('public.project_workspace') IS NOT NULL AS present` - ) - return legacy?.present ?? false -} - -/** Connector authority during overlap; column-first with null fallback after durable cutover. */ -export async function getProjectEnvironmentSource( - tx: DbTransaction -): Promise> { - const phase = await getProjectMembershipPhase(tx) - if (phase === 'connector') { - return tx - .select({ ...fields, projectId: projectWorkspace.projectId }) - .from(workspace) - .leftJoin(projectWorkspace, eq(projectWorkspace.workspaceId, workspace.id)) - .as('project_environments') - } - if (!(await hasLegacyMemberships(tx))) - return tx.select(fields).from(workspace).as('project_environments') - return tx - .select(fields) - .from(workspace) - .where(isNotNull(workspace.projectId)) - .unionAll( - tx - .select({ ...fields, projectId: projectWorkspace.projectId }) - .from(workspace) - .leftJoin(projectWorkspace, eq(projectWorkspace.workspaceId, workspace.id)) - .where(isNull(workspace.projectId)) - ) - .as('project_environments') -} - -/** - * A detached environment can still have a legacy reference to a Project being deleted. - * Remove only references superseded by a different non-null column, under the caller's - * Project locks and deletion transaction. Current memberships remain protected by the FK. - */ -export async function deleteObsoleteProjectMemberships( - tx: DbTransaction, - projectIds: string[] -): Promise { - if ( - !projectIds.length || - (await getProjectMembershipPhase(tx)) !== 'column' || - !(await hasLegacyMemberships(tx)) - ) - return - await tx.delete(projectWorkspace).where( - and( - inArray(projectWorkspace.projectId, projectIds), - exists( - tx - .select({ id: workspace.id }) - .from(workspace) - .where( - and( - eq(workspace.id, projectWorkspace.workspaceId), - isNotNull(workspace.projectId), - ne(workspace.projectId, projectWorkspace.projectId) - ) - ) - ) - ) - ) -} diff --git a/apps/sim/lib/projects/lifecycle.ts b/apps/sim/lib/projects/lifecycle.ts index ea78e6c7eca..b75d7e798c7 100644 --- a/apps/sim/lib/projects/lifecycle.ts +++ b/apps/sim/lib/projects/lifecycle.ts @@ -1,8 +1,7 @@ import { project, workspace } from '@sim/db/schema' -import { asc, eq, sql } from 'drizzle-orm' +import { asc, eq } from 'drizzle-orm' import { OrchestrationError } from '@/lib/core/orchestration/types' import type { DbTransaction } from '@/lib/db/types' -import { getProjectEnvironmentSource } from '@/lib/projects/environment-source' import { lockProject } from '@/lib/projects/membership' import { archiveEnvironmentInTransaction, @@ -19,13 +18,10 @@ export async function archiveProjectInTransaction( const [record] = await tx.select().from(project).where(eq(project.id, projectId)) if (!record) throw new OrchestrationError('not_found', 'Project not found') const now = record.archivedAt ?? new Date() - const source = await getProjectEnvironmentSource(tx) const environments = await tx .select({ id: workspace.id }) .from(workspace) - .where( - sql`${workspace.id} IN (SELECT ${source.id} FROM ${source} WHERE ${source.projectId} = ${projectId})` - ) + .where(eq(workspace.projectId, projectId)) .orderBy(asc(workspace.id)) .for('no key update') const effects: EnvironmentArchiveEffects[] = [] diff --git a/apps/sim/lib/projects/membership.ts b/apps/sim/lib/projects/membership.ts index 17c93c13732..fe70a37461e 100644 --- a/apps/sim/lib/projects/membership.ts +++ b/apps/sim/lib/projects/membership.ts @@ -1,8 +1,8 @@ -import { permissionGroup, project, projectWorkspace, workspace } from '@sim/db/schema' +import { permissionGroup, project, workspace } from '@sim/db/schema' import { getPostgresErrorCode } from '@sim/utils/errors' import { generateId } from '@sim/utils/id' import { compareStrings, truncateAtCodePoint } from '@sim/utils/string' -import { and, asc, eq, inArray, isNotNull, isNull, notInArray, type SQL, sql } from 'drizzle-orm' +import { and, asc, eq, inArray, isNull, notInArray, type SQL, sql } from 'drizzle-orm' import { acquireAdvisoryXactLock, acquireAdvisoryXactLocks, @@ -11,11 +11,6 @@ import { import { textArrayLiteral } from '@/lib/db/arrays' import type { DbTransaction } from '@/lib/db/types' import { acquirePermissionGroupOrgLock } from '@/lib/permission-groups/locks' -import { - getProjectEnvironmentSource, - getProjectMembershipPhase, - lockProjectMembershipBarrier, -} from '@/lib/projects/environment-source' import { ProjectConflictError } from '@/lib/projects/errors' const PROJECT_LOCK_TIMEOUT_MS = 5_000 @@ -30,7 +25,6 @@ async function withProjectLockTimeout( message: string, acquire: () => Promise ): Promise { - await lockProjectMembershipBarrier(tx) const [setting] = await tx.execute<{ previous: string }>( sql`SELECT current_setting('lock_timeout') AS previous` ) @@ -52,7 +46,7 @@ async function withProjectLockTimeout( * assigns it; writers take it shared. Every holder locks in code-unit order of workspace id * (`ORDER BY id COLLATE "C"` in SQL) so the two sides cannot deadlock. */ -export function projectBackfillLockKey(workspaceId: string): string { +function projectBackfillLockKey(workspaceId: string): string { return `project-backfill:${workspaceId}` } @@ -66,7 +60,7 @@ function acquireBackfillWriteLocks(tx: DbTransaction, workspaceIds: string[]) { return acquireAdvisoryXactLocks(tx, 'project_backfill', locks) } -/** Shared per-environment gate keeps membership absence reads stable during backfill. */ +/** Shared per-environment gate keeps membership absence reads stable during SQL backfill. */ export async function lockProjectBackfillWrites( tx: DbTransaction, workspaceIds: string[] @@ -105,15 +99,13 @@ function generatedProjectName(workspaceName: string): string { } /** Selects `workspaceId` and every fork descendant, archived ones included, as `descendants`. */ -async function forkSubtree(tx: DbTransaction, workspaceId: string): Promise { - const environments = await getProjectEnvironmentSource(tx) +function forkSubtree(workspaceId: string): SQL { return sql` WITH RECURSIVE descendants AS ( - SELECT id, name, owner_id, archived_at, project_id FROM ${environments} WHERE id = ${workspaceId} + SELECT id, name, owner_id, archived_at, project_id FROM workspace WHERE id = ${workspaceId} UNION - SELECT ${environments.id}, ${environments.name}, ${environments.ownerId}, - ${environments.archivedAt}, ${environments.projectId} - FROM ${environments} JOIN descendants d ON ${environments.parentId} = d.id + SELECT w.id, w.name, w.owner_id, w.archived_at, w.project_id + FROM workspace w JOIN descendants d ON w.forked_from_workspace_id = d.id )` } @@ -127,7 +119,6 @@ export async function createProjectRecord( projectName?: string } ): Promise { - await getProjectMembershipPhase(tx) const id = generateId() await tx.insert(project).values({ id, @@ -138,23 +129,32 @@ export async function createProjectRecord( return id } -/** Returns null only when neither the column nor legacy connector assigns a Project. */ +/** Locks and rechecks the workspace’s Project; null means the workspace no longer exists. */ export async function lockWorkspaceProject(tx: DbTransaction, workspaceId: string) { return withProjectLockTimeout(tx, PROJECT_CHANGING, async () => { await acquireBackfillWriteLocks(tx, [workspaceId]) - const environments = await getProjectEnvironmentSource(tx) const [membership] = await tx - .select({ projectId: environments.projectId }) - .from(environments) - .where(eq(environments.id, workspaceId)) + .select({ projectId: workspace.projectId }) + .from(workspace) + .where(eq(workspace.id, workspaceId)) .limit(1) - if (!membership?.projectId) return null + if (!membership) return null + if (!membership.projectId) + throw new ProjectConflictError('Project membership needs reconciliation') await acquireAdvisoryXactLock(tx, 'project', projectLockKey(membership.projectId)) const [current] = await tx - .select({ project }) - .from(environments) - .innerJoin(project, eq(project.id, environments.projectId)) - .where(eq(environments.id, workspaceId)) + .select({ + project: { + id: project.id, + name: project.name, + ownerId: project.ownerId, + organizationId: project.organizationId, + archivedAt: project.archivedAt, + }, + }) + .from(workspace) + .innerJoin(project, eq(project.id, workspace.projectId)) + .where(eq(workspace.id, workspaceId)) .limit(1) if (!current || current.project.id !== membership.projectId) { throw new ProjectConflictError('Project membership changed; retry the operation') @@ -163,38 +163,14 @@ export async function lockWorkspaceProject(tx: DbTransaction, workspaceId: strin }) } -/** - * Locks the parent's Project for a new fork; null means a legacy parent awaiting - * backfill whose subtree must still be unassigned. Refuses an archived Project. - */ +/** Locks the parent's Project for a new fork and refuses an archived Project. */ export async function requireForkProject(tx: DbTransaction, parentWorkspaceId: string) { const parent = await lockWorkspaceProject(tx, parentWorkspaceId) - if (!parent) { - await requireUnassignedForkSubtree(tx, parentWorkspaceId) - return null - } + if (!parent) throw new ProjectConflictError('Source workspace no longer exists') if (parent.archivedAt) throw new ProjectConflictError('Cannot fork an archived Project') return parent } -/** Legacy fallback must not hide partially assigned descendants. Caller holds the lineage lock. */ -async function requireUnassignedForkSubtree(tx: DbTransaction, workspaceId: string): Promise { - const descendants = await tx.execute<{ id: string }>( - sql`${await forkSubtree(tx, workspaceId)} SELECT id FROM descendants` - ) - if (!descendants.length) return - const ids = descendants.map((row) => row.id) - await lockProjectBackfillWrites(tx, ids) - const environments = await getProjectEnvironmentSource(tx) - const rows = await tx - .select({ id: environments.id }) - .from(environments) - .where(and(inArray(environments.id, ids), isNotNull(environments.projectId))) - .limit(1) - if (rows.length) - throw new ProjectConflictError('Fork descendants need Project membership reconciliation') -} - /** * Keeps an active Project from outliving its environments: archiving its last active * environment archives it too. Returns whether it did. Caller holds the Project lock. @@ -205,7 +181,6 @@ export async function archiveProjectWithLastEnvironment( workspaceId: string, now: Date ): Promise { - const environments = await getProjectEnvironmentSource(tx) const archived = await tx .update(project) .set({ archivedAt: now, updatedAt: now }) @@ -214,10 +189,10 @@ export async function archiveProjectWithLastEnvironment( eq(project.id, projectId), isNull(project.archivedAt), sql`NOT EXISTS ( - SELECT 1 FROM ${environments} - WHERE ${environments.projectId} = ${projectId} - AND ${environments.id} <> ${workspaceId} - AND ${environments.archivedAt} IS NULL + SELECT 1 FROM ${workspace} + WHERE ${workspace.projectId} = ${projectId} + AND ${workspace.id} <> ${workspaceId} + AND ${workspace.archivedAt} IS NULL )` ) ) @@ -226,26 +201,24 @@ export async function archiveProjectWithLastEnvironment( } /** - * Moves the detached subtree into a new Project and returns its id; null for a legacy - * unassigned subtree. Called before clearing the edge, under the existing lineage lock. + * Moves the detached subtree into a new Project and returns its id. Called before + * clearing the edge, under the existing lineage lock. */ export async function splitForkProject( tx: DbTransaction, - workspaceId: string -): Promise { + workspaceId: string, + newProjectId = generateId() +): Promise { const owner = await lockWorkspaceProject(tx, workspaceId) - if (!owner) { - await requireUnassignedForkSubtree(tx, workspaceId) - return null - } + if (!owner) throw new ProjectConflictError('Workspace no longer exists') if (owner.archivedAt) throw new ProjectConflictError('Cannot disconnect an archived Project') const rows = await tx.execute<{ id: string name: string owner_id: string archived_at: Date | null - project_id: string | null - }>(sql`${await forkSubtree(tx, workspaceId)} SELECT * FROM descendants`) + project_id: string + }>(sql`${forkSubtree(workspaceId)} SELECT * FROM descendants`) if (rows.some((row) => row.project_id !== owner.id)) throw new ProjectConflictError( 'Fork descendants need Project membership reconciliation before disconnecting' @@ -254,15 +227,14 @@ export async function splitForkProject( if (!root || rows.every((row) => row.archived_at)) throw new ProjectConflictError('A new Project needs an active environment') const ids = rows.map((row) => row.id) - const environments = await getProjectEnvironmentSource(tx) const [remaining] = await tx - .select({ id: environments.id }) - .from(environments) + .select({ id: workspace.id }) + .from(workspace) .where( and( - eq(environments.projectId, owner.id), - isNull(environments.archivedAt), - notInArray(environments.id, ids) + eq(workspace.projectId, owner.id), + isNull(workspace.archivedAt), + notInArray(workspace.id, ids) ) ) .limit(1) @@ -270,7 +242,7 @@ export async function splitForkProject( throw new ProjectConflictError( 'Disconnecting would remove the last active environment from this Project' ) - const id = generateId() + const id = newProjectId await tx.insert(project).values({ id, name: generatedProjectName(root.name), @@ -284,14 +256,7 @@ export async function splitForkProject( .where(inArray(workspace.id, ids)) .orderBy(asc(workspace.id)) .for('no key update') - if ((await getProjectMembershipPhase(tx)) === 'connector') { - await tx - .update(projectWorkspace) - .set({ projectId: id }) - .where(inArray(projectWorkspace.workspaceId, ids)) - } else { - await tx.update(workspace).set({ projectId: id }).where(inArray(workspace.id, ids)) - } + await tx.update(workspace).set({ projectId: id }).where(inArray(workspace.id, ids)) }) if (owner.organizationId) { await acquirePermissionGroupOrgLock(tx, owner.organizationId) @@ -319,20 +284,19 @@ export async function transferWorkspaceProjects( ): Promise { if (!workspaceIds.length) return await lockProjectBackfillWrites(tx, workspaceIds) - const environments = await getProjectEnvironmentSource(tx) const owners = await tx - .selectDistinct({ id: environments.projectId }) - .from(environments) - .where(inArray(environments.id, workspaceIds)) - .orderBy(asc(environments.projectId)) - const projectIds = owners.flatMap((row) => (row.id ? [row.id] : [])) - if (!projectIds.length) return + .selectDistinct({ id: workspace.projectId }) + .from(workspace) + .where(inArray(workspace.id, workspaceIds)) + .orderBy(asc(workspace.projectId)) + if (!owners.length) return + const projectIds = owners.map((row) => row.id) await tryLockProjects(tx, projectIds) const selected = new Set(workspaceIds) const members = await tx - .select({ id: environments.id }) - .from(environments) - .where(inArray(environments.projectId, projectIds)) + .select({ id: workspace.id }) + .from(workspace) + .where(inArray(workspace.projectId, projectIds)) if (members.some((row) => !selected.has(row.id))) { throw new ProjectConflictError( 'Move all environments in the Project together, or disconnect the fork first' diff --git a/apps/sim/lib/selectors/__integration__/copilot-nested-selectors.integration.ts b/apps/sim/lib/selectors/__integration__/copilot-nested-selectors.integration.ts index 8a8251b4165..572621ba952 100644 --- a/apps/sim/lib/selectors/__integration__/copilot-nested-selectors.integration.ts +++ b/apps/sim/lib/selectors/__integration__/copilot-nested-selectors.integration.ts @@ -1,3 +1,4 @@ +import { deleteWorkspaceFixture, insertWorkspaceFixture } from '@sim/db/testing/workspace-fixtures' /** Chat's in-process CLI reaching selectors whose options are owned by another domain. */ import { db } from '@sim/db' @@ -46,7 +47,7 @@ describe('Copilot selectors backed by another domain', () => { createdAt: now, updatedAt: now, }) - await db.insert(workspace).values({ + await insertWorkspaceFixture(db, { id: workspaceId, name: 'Selector fixture', ownerId: userId, @@ -100,7 +101,7 @@ describe('Copilot selectors backed by another domain', () => { }) }) afterAll(async () => { - await db.delete(workspace).where(eq(workspace.id, workspaceId)) + await deleteWorkspaceFixture(db, eq(workspace.id, workspaceId)) await db.delete(user).where(eq(user.id, userId)) await db.$client.end() }) diff --git a/apps/sim/lib/table/application/row-audit.integration.ts b/apps/sim/lib/table/application/row-audit.integration.ts index 4aebd85ad50..ebc871c5ebf 100644 --- a/apps/sim/lib/table/application/row-audit.integration.ts +++ b/apps/sim/lib/table/application/row-audit.integration.ts @@ -7,6 +7,7 @@ import { userTableRows, workspace, } from '@sim/db/schema' +import { deleteWorkspaceFixture, insertWorkspaceFixture } from '@sim/db/testing/workspace-fixtures' import { createSessionPrincipal } from '@sim/testing/factories/principal.factory' import { storageServiceMock } from '@sim/testing/mocks/storage-service.mock' import { tableBillingMock, tableBillingMockFns } from '@sim/testing/mocks/table-billing.mock' @@ -54,7 +55,7 @@ describe('table mutation audit history in PostgreSQL', () => { createdAt: now, updatedAt: now, }) - await db.insert(workspace).values({ + await insertWorkspaceFixture(db, { id: workspaceId, name: 'Table audit fixture', ownerId: userId, @@ -85,7 +86,7 @@ describe('table mutation audit history in PostgreSQL', () => { afterEach(async () => { await db.delete(auditLog).where(eq(auditLog.workspaceId, workspaceId)) - await db.delete(workspace).where(eq(workspace.id, workspaceId)) + await deleteWorkspaceFixture(db, eq(workspace.id, workspaceId)) await db.delete(user).where(eq(user.id, userId)) }) diff --git a/apps/sim/lib/table/dispatcher.integration.ts b/apps/sim/lib/table/dispatcher.integration.ts index 04f17305ebe..f7d004b0092 100644 --- a/apps/sim/lib/table/dispatcher.integration.ts +++ b/apps/sim/lib/table/dispatcher.integration.ts @@ -37,13 +37,20 @@ describe('table run dispatcher against real PostgreSQL', () => { beforeAll(async () => { await control`INSERT INTO "user" (id, name, email, email_verified, created_at, updated_at) VALUES (${userId}, 'Dispatcher fixture', ${`${userId}@example.test`}, true, now(), now())` - await control`INSERT INTO workspace (id, name, owner_id, billed_account_user_id) - VALUES (${workspaceId}, 'Dispatcher fixtures', ${userId}, ${userId})` + await control.begin(async (tx) => { + await tx`INSERT INTO project (id, name, owner_id) + VALUES (${workspaceId}, 'Dispatcher fixture project', ${userId})` + await tx`INSERT INTO workspace (id, project_id, name, owner_id, billed_account_user_id) + VALUES (${workspaceId}, ${workspaceId}, 'Dispatcher fixtures', ${userId}, ${userId})` + }) }) afterAll(async () => { - await control`DELETE FROM workspace WHERE id = ${workspaceId}` - await control`DELETE FROM "user" WHERE id = ${userId}` + await control.begin(async (tx) => { + await tx`DELETE FROM workspace WHERE id = ${workspaceId}` + await tx`DELETE FROM project WHERE id = ${workspaceId}` + await tx`DELETE FROM "user" WHERE id = ${userId}` + }) await control.end() }) diff --git a/apps/sim/lib/table/row-changes.integration.ts b/apps/sim/lib/table/row-changes.integration.ts index 4042b5ae006..7c27baa75ef 100644 --- a/apps/sim/lib/table/row-changes.integration.ts +++ b/apps/sim/lib/table/row-changes.integration.ts @@ -3,9 +3,11 @@ * moves it into the definition row without changing what a reader sees, under concurrent appends. */ import { db } from '@sim/db' -import { userTableDefinitions } from '@sim/db/schema' +import { userTableDefinitions, workspace } from '@sim/db/schema' import { readTestDatabaseUrl } from '@sim/db/testing/test-infrastructure' +import { deleteWorkspaceFixture, insertWorkspaceFixture } from '@sim/db/testing/workspace-fixtures' import { generateId } from '@sim/utils/id' +import { eq } from 'drizzle-orm' import postgres from 'postgres' import { afterAll, beforeAll, describe, expect, it } from 'vitest' import { @@ -63,12 +65,16 @@ describe('table row-change log against real PostgreSQL', () => { beforeAll(async () => { await control`INSERT INTO "user" (id, name, email, email_verified, created_at, updated_at) VALUES (${userId}, 'Row change fixture', ${`${userId}@example.test`}, true, now(), now())` - await control`INSERT INTO workspace (id, name, owner_id, billed_account_user_id) - VALUES (${workspaceId}, 'Row change fixtures', ${userId}, ${userId})` + await insertWorkspaceFixture(db, { + id: workspaceId, + name: 'Row change fixtures', + ownerId: userId, + billedAccountUserId: userId, + }) }) afterAll(async () => { - await control`DELETE FROM workspace WHERE id = ${workspaceId}` + await deleteWorkspaceFixture(db, eq(workspace.id, workspaceId)) await control`DELETE FROM "user" WHERE id = ${userId}` await control.end() }) diff --git a/apps/sim/lib/table/rows/row-writes.integration.ts b/apps/sim/lib/table/rows/row-writes.integration.ts index 4ebc908c01a..a7184708495 100644 --- a/apps/sim/lib/table/rows/row-writes.integration.ts +++ b/apps/sim/lib/table/rows/row-writes.integration.ts @@ -205,8 +205,11 @@ describe('table row writes against real PostgreSQL', () => { beforeAll(async () => { await control`INSERT INTO "user" (id, name, email, email_verified, created_at, updated_at) VALUES (${userId}, 'Row write fixture', ${`${userId}@example.test`}, true, now(), now())` - await control`INSERT INTO workspace (id, name, owner_id, billed_account_user_id) - VALUES (${workspaceId}, 'Row write fixtures', ${userId}, ${userId})` + await control.begin(async (tx) => { + await tx`INSERT INTO project (id, name, owner_id) VALUES (${workspaceId}, 'Fixture project', ${userId})` + await tx`INSERT INTO workspace (id, project_id, name, owner_id, billed_account_user_id) + VALUES (${workspaceId}, ${workspaceId}, 'Row write fixtures', ${userId}, ${userId})` + }) }) beforeEach(() => { @@ -214,7 +217,10 @@ describe('table row writes against real PostgreSQL', () => { }) afterAll(async () => { - await control`DELETE FROM workspace WHERE id = ${workspaceId}` + await control.begin(async (tx) => { + await tx`DELETE FROM workspace WHERE id = ${workspaceId}` + await tx`DELETE FROM project WHERE id = ${workspaceId}` + }) await control`DELETE FROM "user" WHERE id = ${userId}` await control.end() }) diff --git a/apps/sim/lib/uploads/contexts/workspace/__integration__/file-names.integration.ts b/apps/sim/lib/uploads/contexts/workspace/__integration__/file-names.integration.ts index e8f2d62cdc8..56c47a40ee9 100644 --- a/apps/sim/lib/uploads/contexts/workspace/__integration__/file-names.integration.ts +++ b/apps/sim/lib/uploads/contexts/workspace/__integration__/file-names.integration.ts @@ -8,6 +8,7 @@ import { tmpdir } from 'node:os' import path from 'node:path' import { db, dbFor } from '@sim/db' import { copilotChats, organization, user, workspace, workspaceFiles } from '@sim/db/schema' +import { deleteWorkspaceFixture } from '@sim/db/testing/workspace-fixtures' import { generateId } from '@sim/utils/id' import { and, eq, inArray, isNull, sql } from 'drizzle-orm' import { afterAll, beforeAll, describe, expect, it, vi } from 'vitest' @@ -51,7 +52,7 @@ describe('workspace file names in PostgreSQL', () => { afterAll(async () => { vi.restoreAllMocks() for (const ids of fixtures) { - await db.delete(workspace).where(eq(workspace.id, ids.workspaceId)) + await deleteWorkspaceFixture(db, eq(workspace.id, ids.workspaceId)) await db.delete(organization).where(eq(organization.id, ids.organizationId)) await db.delete(user).where(inArray(user.id, [ids.aliceId, ids.bobId])) } diff --git a/apps/sim/lib/uploads/contexts/workspace/__integration__/file-versions.integration.ts b/apps/sim/lib/uploads/contexts/workspace/__integration__/file-versions.integration.ts index 21ab15dacce..ac4434e5146 100644 --- a/apps/sim/lib/uploads/contexts/workspace/__integration__/file-versions.integration.ts +++ b/apps/sim/lib/uploads/contexts/workspace/__integration__/file-versions.integration.ts @@ -13,6 +13,7 @@ import { workspaceFiles, workspaceFileVersion, } from '@sim/db/schema' +import { deleteWorkspaceFixture } from '@sim/db/testing/workspace-fixtures' import { generateId } from '@sim/utils/id' import { and, asc, eq, inArray, sql } from 'drizzle-orm' import { afterAll, beforeAll, describe, expect, it, vi } from 'vitest' @@ -68,7 +69,7 @@ describe('workspace file version history in PostgreSQL', () => { afterAll(async () => { for (const ids of fixtures) { - await db.delete(workspace).where(eq(workspace.id, ids.workspaceId)) + await deleteWorkspaceFixture(db, eq(workspace.id, ids.workspaceId)) await db.delete(organization).where(eq(organization.id, ids.organizationId)) await db.delete(user).where(inArray(user.id, [ids.aliceId, ids.bobId])) } diff --git a/apps/sim/lib/webhooks/registration-store.integration.ts b/apps/sim/lib/webhooks/registration-store.integration.ts index d46fa413659..1d62346117c 100644 --- a/apps/sim/lib/webhooks/registration-store.integration.ts +++ b/apps/sim/lib/webhooks/registration-store.integration.ts @@ -12,6 +12,7 @@ import { workflowDeploymentVersion, workspace, } from '@sim/db/schema' +import { deleteWorkspaceFixture, insertWorkspaceFixture } from '@sim/db/testing/workspace-fixtures' import { generateId } from '@sim/utils/id' import { eq } from 'drizzle-orm' import { afterAll, beforeAll, describe, expect, it } from 'vitest' @@ -58,7 +59,7 @@ beforeAll(async () => { createdAt: now, updatedAt: now, }) - await db.insert(workspace).values({ + await insertWorkspaceFixture(db, { id: workspaceId, name: 'Registration Store', ownerId: owner, @@ -111,7 +112,7 @@ beforeAll(async () => { }) afterAll(async () => { - await db.delete(workspace).where(eq(workspace.id, workspaceId)) + await deleteWorkspaceFixture(db, eq(workspace.id, workspaceId)) await db.delete(user).where(eq(user.id, owner)) }) diff --git a/apps/sim/lib/webhooks/trigger-block-deployment.integration.ts b/apps/sim/lib/webhooks/trigger-block-deployment.integration.ts index 4b94dd6a7ca..b6fb5d22d49 100644 --- a/apps/sim/lib/webhooks/trigger-block-deployment.integration.ts +++ b/apps/sim/lib/webhooks/trigger-block-deployment.integration.ts @@ -5,6 +5,7 @@ */ import { db } from '@sim/db' import { user, webhook, workflow, workflowDeploymentVersion, workspace } from '@sim/db/schema' +import { deleteWorkspaceFixture, insertWorkspaceFixture } from '@sim/db/testing/workspace-fixtures' import { generateId } from '@sim/utils/id' import { eq } from 'drizzle-orm' import { afterAll, beforeAll, describe, expect, it } from 'vitest' @@ -35,9 +36,12 @@ beforeAll(async () => { createdAt: now, updatedAt: now, }) - await db - .insert(workspace) - .values({ id: workspaceId, name: 'Trigger Block', ownerId: owner, billedAccountUserId: owner }) + await insertWorkspaceFixture(db, { + id: workspaceId, + name: 'Trigger Block', + ownerId: owner, + billedAccountUserId: owner, + }) await db.insert(workflow).values( [deployedWorkflow, undeployedWorkflow].map((id) => ({ id, @@ -100,7 +104,7 @@ beforeAll(async () => { }) afterAll(async () => { - await db.delete(workspace).where(eq(workspace.id, workspaceId)) + await deleteWorkspaceFixture(db, eq(workspace.id, workspaceId)) await db.delete(user).where(eq(user.id, owner)) }) diff --git a/apps/sim/lib/workflows/application/compare-workflow-versions.integration.ts b/apps/sim/lib/workflows/application/compare-workflow-versions.integration.ts index 76e957180f4..bb057a080fd 100644 --- a/apps/sim/lib/workflows/application/compare-workflow-versions.integration.ts +++ b/apps/sim/lib/workflows/application/compare-workflow-versions.integration.ts @@ -1,5 +1,6 @@ import { db } from '@sim/db' import { user, workflow, workflowDeploymentVersion, workspace } from '@sim/db/schema' +import { deleteWorkspaceFixture, insertWorkspaceFixture } from '@sim/db/testing/workspace-fixtures' import { createWorkspaceApiKeyPrincipal } from '@sim/testing/factories/principal.factory' import { generateId } from '@sim/utils/id' import { eq, inArray } from 'drizzle-orm' @@ -152,7 +153,8 @@ beforeAll(async () => { createdAt: now, updatedAt: now, }) - await db.insert(workspace).values( + await insertWorkspaceFixture( + db, [workspaceId, otherWorkspaceId].map((id) => ({ id, name: 'Comparison fixture', @@ -188,7 +190,7 @@ beforeAll(async () => { }) afterAll(async () => { - await db.delete(workspace).where(inArray(workspace.id, [workspaceId, otherWorkspaceId])) + await deleteWorkspaceFixture(db, inArray(workspace.id, [workspaceId, otherWorkspaceId])) await db.delete(user).where(eq(user.id, ownerId)) await db.$client.end() }) diff --git a/apps/sim/lib/workflows/application/read-workflow-version.integration.ts b/apps/sim/lib/workflows/application/read-workflow-version.integration.ts index c5ac33ca8af..d165548eefa 100644 --- a/apps/sim/lib/workflows/application/read-workflow-version.integration.ts +++ b/apps/sim/lib/workflows/application/read-workflow-version.integration.ts @@ -1,5 +1,6 @@ import { db } from '@sim/db' import { permissions, user, workflow, workflowDeploymentVersion, workspace } from '@sim/db/schema' +import { deleteWorkspaceFixture, insertWorkspaceFixture } from '@sim/db/testing/workspace-fixtures' import { createSessionPrincipal, createWorkspaceApiKeyPrincipal, @@ -98,7 +99,7 @@ beforeAll(async () => { updatedAt: now, })) ) - await db.insert(workspace).values({ + await insertWorkspaceFixture(db, { id: workspaceId, name: 'Version read fixture', ownerId, @@ -128,7 +129,7 @@ beforeAll(async () => { }) afterAll(async () => { - await db.delete(workspace).where(eq(workspace.id, workspaceId)) + await deleteWorkspaceFixture(db, eq(workspace.id, workspaceId)) await db.delete(user).where(inArray(user.id, [ownerId, outsiderId])) await db.$client.end() }) diff --git a/apps/sim/lib/workflows/custom-blocks/workspace-rows.integration.ts b/apps/sim/lib/workflows/custom-blocks/workspace-rows.integration.ts index a40c76c63f2..d47490f35b1 100644 --- a/apps/sim/lib/workflows/custom-blocks/workspace-rows.integration.ts +++ b/apps/sim/lib/workflows/custom-blocks/workspace-rows.integration.ts @@ -1,6 +1,7 @@ /** The custom-block rows an execution overlays, read against real PostgreSQL. */ import { db } from '@sim/db' import { customBlock, organization, user, workflow, workspace } from '@sim/db/schema' +import { deleteWorkspaceFixture, insertWorkspaceFixture } from '@sim/db/testing/workspace-fixtures' import { billingSubscriptionMock, billingSubscriptionMockFns, @@ -38,7 +39,7 @@ beforeAll(async () => { { id: organizations.withBlocks, name: 'With Blocks', slug: organizations.withBlocks }, { id: organizations.withoutBlocks, name: 'Without Blocks', slug: organizations.withoutBlocks }, ]) - await db.insert(workspace).values([ + await insertWorkspaceFixture(db, [ { id: workspaces.withBlocks, name: 'With Blocks', @@ -90,7 +91,7 @@ beforeEach(() => { }) afterAll(async () => { - await db.delete(workspace).where(inArray(workspace.id, Object.values(workspaces))) + await deleteWorkspaceFixture(db, inArray(workspace.id, Object.values(workspaces))) await db.delete(organization).where(inArray(organization.id, Object.values(organizations))) await db.delete(user).where(eq(user.id, owner)) }) diff --git a/apps/sim/lib/workflows/executor/pause-persistence.integration.ts b/apps/sim/lib/workflows/executor/pause-persistence.integration.ts index 1d5c8eeafd9..be02edfa899 100644 --- a/apps/sim/lib/workflows/executor/pause-persistence.integration.ts +++ b/apps/sim/lib/workflows/executor/pause-persistence.integration.ts @@ -11,6 +11,7 @@ import { workflowExecutionSnapshots, workspace, } from '@sim/db/schema' +import { deleteWorkspaceFixture, insertWorkspaceFixture } from '@sim/db/testing/workspace-fixtures' import { createDeferred } from '@sim/testing' import { generateId } from '@sim/utils/id' import { eq } from 'drizzle-orm' @@ -129,7 +130,7 @@ beforeAll(async () => { createdAt: now, updatedAt: now, }) - await db.insert(workspace).values({ + await insertWorkspaceFixture(db, { id: ids.workspace, name: 'Pause Publish', ownerId: ids.owner, @@ -153,7 +154,7 @@ afterAll(async () => { await db .delete(workflowExecutionSnapshots) .where(eq(workflowExecutionSnapshots.workflowId, ids.workflow)) - await db.delete(workspace).where(eq(workspace.id, ids.workspace)) + await deleteWorkspaceFixture(db, eq(workspace.id, ids.workspace)) await db.delete(user).where(eq(user.id, ids.owner)) }) diff --git a/apps/sim/lib/workflows/lifecycle.ts b/apps/sim/lib/workflows/lifecycle.ts index 69ecc8afca0..183f7ee0c2f 100644 --- a/apps/sim/lib/workflows/lifecycle.ts +++ b/apps/sim/lib/workflows/lifecycle.ts @@ -29,6 +29,7 @@ interface ArchiveWorkflowOptions { requestId: string notifySocket?: boolean archivedAt?: Date + strictExternalCleanup?: boolean } async function notifyWorkflowArchived(workflowId: string, requestId: string): Promise { @@ -54,7 +55,8 @@ async function notifyWorkflowArchived(workflowId: string, requestId: string): Pr async function cleanupExternalWebhooksForWorkflow( workflowId: string, - requestId: string + requestId: string, + strict = false ): Promise { try { const { cleanupExternalWebhook } = await import('@/lib/webhooks/provider-subscriptions') @@ -73,8 +75,11 @@ async function cleanupExternalWebhooksForWorkflow( for (const webhookData of webhooksToCleanup) { try { - await cleanupExternalWebhook(webhookData.webhook, webhookData.workflow, requestId) + await cleanupExternalWebhook(webhookData.webhook, webhookData.workflow, requestId, { + throwOnError: strict, + }) } catch (error) { + if (strict) throw error logger.warn( `[${requestId}] Failed to cleanup external webhook ${webhookData.webhook.id} for workflow ${workflowId}`, { error } @@ -82,6 +87,7 @@ async function cleanupExternalWebhooksForWorkflow( } } } catch (error) { + if (strict) throw error logger.warn(`[${requestId}] Error during external webhook cleanup for workflow ${workflowId}`, { error, }) @@ -406,11 +412,22 @@ export async function finishWorkflowArchive( await notifyWorkflowArchived(workflowId, options.requestId) } - await cleanupExternalWebhooksForWorkflow(workflowId, options.requestId) + await cleanupExternalWebhooksForWorkflow( + workflowId, + options.requestId, + options.strictExternalCleanup + ) if (workspaceId && mcpPubSub) { for (const serverId of new Set(serverIds)) { - mcpPubSub.publishWorkflowToolsChanged({ serverId, workspaceId }) + try { + mcpPubSub.publishWorkflowToolsChanged({ serverId, workspaceId }) + } catch (error) { + logger.warn( + `[${options.requestId}] MCP tools-changed publish failed for server ${serverId}`, + { error } + ) + } } } } diff --git a/apps/sim/lib/workflows/orchestration/create-workflow-with-state.integration.ts b/apps/sim/lib/workflows/orchestration/create-workflow-with-state.integration.ts index 9b4f9f10674..864925778ef 100644 --- a/apps/sim/lib/workflows/orchestration/create-workflow-with-state.integration.ts +++ b/apps/sim/lib/workflows/orchestration/create-workflow-with-state.integration.ts @@ -3,6 +3,7 @@ import { mkdir, writeFile } from 'node:fs/promises' import { dirname, resolve } from 'node:path' import { db } from '@sim/db' import { user, workflow, workspace } from '@sim/db/schema' +import { deleteWorkspaceFixture, insertWorkspaceFixture } from '@sim/db/testing/workspace-fixtures' import { getErrorMessage } from '@sim/utils/errors' import { generateId } from '@sim/utils/id' import type { WorkflowState } from '@sim/workflow-types/workflow' @@ -60,7 +61,7 @@ describe('createWorkflowWithState against PostgreSQL', () => { createdAt: now, updatedAt: now, }) - await db.insert(workspace).values([ + await insertWorkspaceFixture(db, [ { id: workspaceId, name: 'Import target', ownerId: userId, billedAccountUserId: userId }, { id: archivedWorkspaceId, @@ -78,7 +79,7 @@ describe('createWorkflowWithState against PostgreSQL', () => { resolve('test-results/create-workflow-with-state.json') await mkdir(dirname(reportPath), { recursive: true }) await writeFile(reportPath, JSON.stringify({ checks }, null, 2)) - await db.delete(workspace).where(eq(workspace.ownerId, userId)) + await deleteWorkspaceFixture(db, eq(workspace.ownerId, userId)) await db.delete(user).where(eq(user.id, userId)) }) diff --git a/apps/sim/lib/workspace-files/__integration__/copilot-file-versions.integration.ts b/apps/sim/lib/workspace-files/__integration__/copilot-file-versions.integration.ts index f791f4c7c7b..ee1caefb6bb 100644 --- a/apps/sim/lib/workspace-files/__integration__/copilot-file-versions.integration.ts +++ b/apps/sim/lib/workspace-files/__integration__/copilot-file-versions.integration.ts @@ -15,6 +15,7 @@ import { workspace, workspaceFileVersion, } from '@sim/db/schema' +import { deleteWorkspaceFixture, insertWorkspaceFixture } from '@sim/db/testing/workspace-fixtures' import { envFlagsMock } from '@sim/testing/mocks/env-flags.mock' import { generateId } from '@sim/utils/id' import { and, asc, eq, inArray } from 'drizzle-orm' @@ -73,10 +74,10 @@ describe('chat-delegated file version history', () => { afterAll(async () => { if (extraWorkspaceIds.length > 0) - await db.delete(workspace).where(inArray(workspace.id, extraWorkspaceIds)) + await deleteWorkspaceFixture(db, inArray(workspace.id, extraWorkspaceIds)) for (const ids of fixtures) { await db.delete(auditLog).where(eq(auditLog.workspaceId, ids.workspaceId)) - await db.delete(workspace).where(eq(workspace.id, ids.workspaceId)) + await deleteWorkspaceFixture(db, eq(workspace.id, ids.workspaceId)) await db.delete(organization).where(eq(organization.id, ids.organizationId)) await db.delete(user).where(inArray(user.id, [ids.aliceId, ids.bobId])) } @@ -303,7 +304,7 @@ describe('chat-delegated file version history', () => { const fixture = await seedVersionedFile() const otherWorkspaceId = generateId() extraWorkspaceIds.push(otherWorkspaceId) - await db.insert(workspace).values({ + await insertWorkspaceFixture(db, { id: otherWorkspaceId, organizationId: fixture.organizationId, name: 'Second workspace', diff --git a/apps/sim/lib/workspaces/__integration__/fork-sync.integration.ts b/apps/sim/lib/workspaces/__integration__/fork-sync.integration.ts index 24725b5dfdd..c2b3f30669a 100644 --- a/apps/sim/lib/workspaces/__integration__/fork-sync.integration.ts +++ b/apps/sim/lib/workspaces/__integration__/fork-sync.integration.ts @@ -10,7 +10,6 @@ import { outboxEvent, permissions, project, - projectWorkspace, user, userTableDefinitions, workflow, @@ -23,6 +22,7 @@ import { workspaceOperationReceipt, workspaceSandbox, } from '@sim/db/schema' +import { insertWorkspaceFixture } from '@sim/db/testing/workspace-fixtures' import { generateId } from '@sim/utils/id' import { and, eq, inArray, sql } from 'drizzle-orm' import { afterAll, beforeAll, describe, expect, it, vi } from 'vitest' @@ -30,8 +30,6 @@ import { withWorkspaceInvocationScope } from '@/lib/core/application/workspace-i import { processOutboxEventById } from '@/lib/core/outbox/service' import * as workflowMcpSync from '@/lib/mcp/workflow-mcp-sync' import { createScopedCliTransport } from '@/lib/mothership/agent-cli/scoped-transport' -import { getProjectMembershipPhase } from '@/lib/projects/environment-source' -import { createProjectRecord } from '@/lib/projects/membership' import { readWorkflowVersion } from '@/lib/workflows/application/read-workflow-version' import { workflowDeploymentOutboxHandlers } from '@/lib/workflows/deployment-outbox' import { @@ -148,24 +146,12 @@ describe('authorized fork and sync against PostgreSQL', () => { createdAt: now, updatedAt: now, }) - await db.transaction(async (tx) => { - const phase = await getProjectMembershipPhase(tx) - const projectId = await createProjectRecord(tx, { - name: 'Fork source fixture', - ownerId: userId, - organizationId: null, - }) - await tx.insert(workspace).values({ - id: sourceWorkspaceId, - projectId: phase === 'column' ? projectId : null, - name: 'Fork source fixture', - ownerId: userId, - billedAccountUserId: userId, - allowPersonalApiKeys: true, - }) - if (phase === 'connector') { - await tx.insert(projectWorkspace).values({ projectId, workspaceId: sourceWorkspaceId }) - } + await insertWorkspaceFixture(db, { + id: sourceWorkspaceId, + name: 'Fork source fixture', + ownerId: userId, + billedAccountUserId: userId, + allowPersonalApiKeys: true, }) await db.insert(permissions).values({ id: generateId(), @@ -199,10 +185,13 @@ describe('authorized fork and sync against PostgreSQL', () => { }) }) afterAll(async () => { - for (const id of createdWorkspaceIds) await db.delete(workspace).where(eq(workspace.id, id)) - await db.delete(workspace).where(eq(workspace.id, sourceWorkspaceId)) - await db.delete(project).where(eq(project.ownerId, userId)) - await db.delete(user).where(eq(user.id, userId)) + await db.transaction(async (tx) => { + await tx + .delete(workspace) + .where(inArray(workspace.id, [sourceWorkspaceId, ...createdWorkspaceIds])) + await tx.delete(project).where(eq(project.ownerId, userId)) + await tx.delete(user).where(eq(user.id, userId)) + }) await db.$client.end() }) @@ -1225,9 +1214,11 @@ describe('authorized fork and sync against PostgreSQL', () => { const childWorkspaceId = generateId() const sourceWorkspaceId = generateId() createdWorkspaceIds.push(sourceWorkspaceId, childWorkspaceId) - await db.insert(workspace).values( - [sourceWorkspaceId, childWorkspaceId].map((id) => ({ + await insertWorkspaceFixture( + db, + [childWorkspaceId, sourceWorkspaceId].map((id) => ({ id, + forkedFromWorkspaceId: id === childWorkspaceId ? sourceWorkspaceId : null, name: 'Knowledge copy fixture', ownerId: userId, billedAccountUserId: userId, diff --git a/apps/sim/lib/workspaces/__integration__/http-cli.integration.ts b/apps/sim/lib/workspaces/__integration__/http-cli.integration.ts index ce4f836e5be..d1d3ac48968 100644 --- a/apps/sim/lib/workspaces/__integration__/http-cli.integration.ts +++ b/apps/sim/lib/workspaces/__integration__/http-cli.integration.ts @@ -14,6 +14,7 @@ import { workspace, workspaceOperationReceipt, } from '@sim/db/schema' +import { deleteWorkspaceFixture, insertWorkspaceFixture } from '@sim/db/testing/workspace-fixtures' import { generateId } from '@sim/utils/id' import { eq } from 'drizzle-orm' import { NextRequest } from 'next/server' @@ -117,7 +118,7 @@ describe('v2 and CLI workflow protocol against PostgreSQL', () => { createdAt: now, updatedAt: now, }) - await db.insert(workspace).values({ + await insertWorkspaceFixture(db, { id: workspaceId, name: 'HTTP fixture', ownerId: userId, @@ -131,7 +132,7 @@ describe('v2 and CLI workflow protocol against PostgreSQL', () => { entityId: workspaceId, permissionType: 'admin', }) - await db.insert(workspace).values({ + await insertWorkspaceFixture(db, { id: foreignWorkspaceId, name: 'Inaccessible log fixture', ownerId: userId, @@ -245,9 +246,9 @@ describe('v2 and CLI workflow protocol against PostgreSQL', () => { server.close((error) => (error ? reject(error) : resolveClose())) server.closeAllConnections() }) - for (const id of childWorkspaceIds) await db.delete(workspace).where(eq(workspace.id, id)) - await db.delete(workspace).where(eq(workspace.id, workspaceId)) - await db.delete(workspace).where(eq(workspace.id, foreignWorkspaceId)) + for (const id of childWorkspaceIds) await deleteWorkspaceFixture(db, eq(workspace.id, id)) + await deleteWorkspaceFixture(db, eq(workspace.id, workspaceId)) + await deleteWorkspaceFixture(db, eq(workspace.id, foreignWorkspaceId)) await db .delete(workflowExecutionSnapshots) .where(eq(workflowExecutionSnapshots.id, logSnapshotId)) diff --git a/apps/sim/lib/workspaces/__integration__/mapped-import.integration.ts b/apps/sim/lib/workspaces/__integration__/mapped-import.integration.ts index b583499ae48..f3b45b96e9b 100644 --- a/apps/sim/lib/workspaces/__integration__/mapped-import.integration.ts +++ b/apps/sim/lib/workspaces/__integration__/mapped-import.integration.ts @@ -11,6 +11,7 @@ import { workspaceOperationReceipt, workspaceSandbox, } from '@sim/db/schema' +import { deleteWorkspaceFixture, insertWorkspaceFixture } from '@sim/db/testing/workspace-fixtures' import { generateId } from '@sim/utils/id' import { and, eq, sql } from 'drizzle-orm' import { afterAll, beforeAll, describe, expect, it } from 'vitest' @@ -103,7 +104,7 @@ describe('authorized mapped imports against PostgreSQL', () => { createdAt: now, updatedAt: now, }) - await db.insert(workspace).values({ + await insertWorkspaceFixture(db, { id: workspaceId, name: 'Mapped import fixture', ownerId: userId, @@ -127,7 +128,7 @@ describe('authorized mapped imports against PostgreSQL', () => { }) }) afterAll(async () => { - await db.delete(workspace).where(eq(workspace.id, workspaceId)) + await deleteWorkspaceFixture(db, eq(workspace.id, workspaceId)) await db.delete(user).where(eq(user.id, userId)) await db.$client.end() }) diff --git a/apps/sim/lib/workspaces/__integration__/pagination.integration.ts b/apps/sim/lib/workspaces/__integration__/pagination.integration.ts index ef15cfe20b0..ef6d9548476 100644 --- a/apps/sim/lib/workspaces/__integration__/pagination.integration.ts +++ b/apps/sim/lib/workspaces/__integration__/pagination.integration.ts @@ -1,5 +1,6 @@ import { db } from '@sim/db' import { permissions, user, workspace, workspaceOperationReceipt } from '@sim/db/schema' +import { deleteWorkspaceFixture, insertWorkspaceFixture } from '@sim/db/testing/workspace-fixtures' import { generateId } from '@sim/utils/id' import { eq, inArray, sql } from 'drizzle-orm' import { afterAll, beforeAll, describe, expect, it } from 'vitest' @@ -34,7 +35,8 @@ describe('workspace pagination against PostgreSQL', () => { createdAt: now, updatedAt: now, }) - await db.insert(workspace).values( + await insertWorkspaceFixture( + db, [workspaceId, otherWorkspaceId].map((id) => ({ id, name: 'Pagination parent fixture', @@ -52,7 +54,8 @@ describe('workspace pagination against PostgreSQL', () => { permissionType: 'admin' as const, })) ) - await db.insert(workspace).values( + await insertWorkspaceFixture( + db, childIds.map((id, index) => ({ id, name: `Pagination child ${index}`, @@ -89,9 +92,10 @@ describe('workspace pagination against PostgreSQL', () => { }) afterAll(async () => { - await db - .delete(workspace) - .where(inArray(workspace.id, [...childIds, workspaceId, otherWorkspaceId])) + await deleteWorkspaceFixture( + db, + inArray(workspace.id, [...childIds, workspaceId, otherWorkspaceId]) + ) await db.delete(user).where(eq(user.id, userId)) await db.$client.end() }) diff --git a/apps/sim/lib/workspaces/__integration__/receipts.integration.ts b/apps/sim/lib/workspaces/__integration__/receipts.integration.ts index 055c9dc7985..34340223bf7 100644 --- a/apps/sim/lib/workspaces/__integration__/receipts.integration.ts +++ b/apps/sim/lib/workspaces/__integration__/receipts.integration.ts @@ -1,5 +1,6 @@ import { db } from '@sim/db' import { user, workflow, workspace, workspaceOperationReceipt } from '@sim/db/schema' +import { deleteWorkspaceFixture, insertWorkspaceFixture } from '@sim/db/testing/workspace-fixtures' import { generateId } from '@sim/utils/id' import { eq } from 'drizzle-orm' import { afterAll, beforeAll, describe, expect, it } from 'vitest' @@ -25,7 +26,7 @@ describe('workspace receipts against PostgreSQL', () => { createdAt: now, updatedAt: now, }) - await db.insert(workspace).values({ + await insertWorkspaceFixture(db, { id: workspaceId, name: 'Workflow sync fixture', ownerId: userId, @@ -33,7 +34,7 @@ describe('workspace receipts against PostgreSQL', () => { }) }) afterAll(async () => { - await db.delete(workspace).where(eq(workspace.id, workspaceId)) + await deleteWorkspaceFixture(db, eq(workspace.id, workspaceId)) await db.delete(user).where(eq(user.id, userId)) await db.$client.end() }) diff --git a/apps/sim/lib/workspaces/admin-move.test.ts b/apps/sim/lib/workspaces/admin-move.test.ts index a379100a3ff..dc226c249e7 100644 --- a/apps/sim/lib/workspaces/admin-move.test.ts +++ b/apps/sim/lib/workspaces/admin-move.test.ts @@ -4,6 +4,7 @@ import { member, organization, outboxEvent, + project, subscription, workspace, } from '@sim/db/schema' @@ -154,6 +155,7 @@ vi.mock('@/lib/workspaces/admin-move-source-impact', () => ({ const movedWorkspace = { id: 'workspace-1', + projectId: 'project-1', name: 'Already moved', ownerId: 'workspace-owner', ownerName: 'Workspace Owner', @@ -191,13 +193,18 @@ const destination = { } /** - * Queues the optimistic scope, locked workspace, optional unassigned Project projection, - * and summary reads. An already completed move skips the Project transfer. + * Queues the optimistic scope, locked workspace, Project transfer and summary reads. An already completed move skips the Project transfer. */ function queueMoveSelects(workspaceRow: Record) { queueTableRows(workspace, [workspaceRow]) queueTableRows(workspace, [workspaceRow]) - if (workspaceRow.organizationId !== destination.id) queueTableRows(workspace, [{ id: null }]) + if (workspaceRow.organizationId !== destination.id) { + queueTableRows(workspace, [{ id: workspaceRow.projectId }]) + queueTableRows(workspace, [{ id: workspaceRow.id }]) + queueTableRows(project, [ + { id: workspaceRow.projectId, organizationId: workspaceRow.organizationId }, + ]) + } queueTableRows(workspace, [workspaceRow]) queueTableRows(organization, [destination]) } @@ -230,9 +237,7 @@ afterAll(resetDbChainMock) beforeEach(() => { resetDbChainMock() - /** Real Drizzle SQL bypasses the shared fragment mock; model the expansion schema explicitly. */ - dbChainMockFns.execute.mockResolvedValue([{ marker: true, complete: false }]) - dbChainMockFns.as.mockReturnValue(workspace) + dbChainMockFns.execute.mockResolvedValue([{ acquired: true }]) isInvitationExpired.mockReturnValue(false) /** * `vi.clearAllMocks` clears call records but keeps implementations, so a @@ -666,7 +671,9 @@ describe('moveWorkspaceToOrganization retries', () => { queueTableRows(workspace, [organizationWorkspace]) queueTableRows(workspace, [{ ...organizationWorkspace, organizationId: 'org-moved' }]) queueTableRows(workspace, [{ ...organizationWorkspace, organizationId: 'org-moved' }]) - queueTableRows(workspace, [{ id: null }]) + queueTableRows(workspace, [{ id: organizationWorkspace.projectId }]) + queueTableRows(workspace, [{ id: organizationWorkspace.id }]) + queueTableRows(project, [{ id: organizationWorkspace.projectId, organizationId: 'org-moved' }]) queueTableRows(workspace, [{ ...organizationWorkspace, organizationId: 'org-moved' }]) queueTableRows(organization, [destination]) diff --git a/apps/sim/lib/workspaces/create.ts b/apps/sim/lib/workspaces/create.ts index 35e467f3639..ab8d951af88 100644 --- a/apps/sim/lib/workspaces/create.ts +++ b/apps/sim/lib/workspaces/create.ts @@ -1,11 +1,10 @@ import { db } from '@sim/db' -import { permissions, projectWorkspace, type WorkspaceMode, workspace } from '@sim/db/schema' +import { permissions, type WorkspaceMode, workspace } from '@sim/db/schema' import { createLogger } from '@sim/logger' import { getPostgresConstraintName, getPostgresErrorCode } from '@sim/utils/errors' import { generateId } from '@sim/utils/id' import { PlatformEvents } from '@/lib/core/telemetry' import type { DbTransaction } from '@/lib/db/types' -import { getProjectMembershipPhase } from '@/lib/projects/environment-source' import { createProjectRecord } from '@/lib/projects/membership' import { buildDefaultWorkflowArtifacts } from '@/lib/workflows/defaults' import { insertNewWorkflowRow } from '@/lib/workflows/persistence/new-workflow-row' @@ -107,7 +106,6 @@ export async function createWorkspaceWithProjectInTransaction( governingPermissionGroupOrganizationId, }: TransactionalCreateWorkspaceParams & { projectName?: string } ): Promise<{ projectId: string; workspace: CreatedWorkspace }> { - const phase = await getProjectMembershipPhase(tx) const workspaceId = generateId() const workflowId = generateId() const now = new Date() @@ -133,7 +131,7 @@ export async function createWorkspaceWithProjectInTransaction( await tx.insert(workspace).values({ id: workspaceId, - projectId: phase === 'column' ? projectId : null, + projectId, name, ownerId: userId, organizationId, @@ -144,10 +142,6 @@ export async function createWorkspaceWithProjectInTransaction( updatedAt: now, }) - if (phase === 'connector') { - await tx.insert(projectWorkspace).values({ projectId, workspaceId }) - } - const permissionRows = [ { id: generateId(), diff --git a/apps/sim/lib/workspaces/lifecycle.ts b/apps/sim/lib/workspaces/lifecycle.ts index d6c88cb377c..525dfb2c50f 100644 --- a/apps/sim/lib/workspaces/lifecycle.ts +++ b/apps/sim/lib/workspaces/lifecycle.ts @@ -29,7 +29,7 @@ const logger = createLogger('WorkspaceLifecycle') /** Bounds each batched workflow archive statement's parameter list. */ const WORKFLOW_ARCHIVE_BATCH_SIZE = 1_000 -/** Bounds concurrent post-commit notifications; each is best-effort and catches its own errors. */ +/** Bounds concurrent post-commit notifications and provider cleanup. */ const ARCHIVE_NOTIFICATION_CONCURRENCY = 8 /** What an environment archive must announce once its transaction commits. */ @@ -135,30 +135,44 @@ export async function archiveEnvironmentInTransaction( } /** - * Announces a committed environment archive. Every step is best-effort and isolated, so - * one failed notification never skips the rest. + * Announces a committed environment archive. Cleanup is best-effort by default; maintenance + * callers can require provider cleanup to succeed before recording completion. */ export async function finishEnvironmentArchive( effects: EnvironmentArchiveEffects, - requestId: string + requestId: string, + options: { strictExternalCleanup?: boolean } = {} ): Promise { const { workspaceId } = effects - await mapWithConcurrency(effects.workflows, ARCHIVE_NOTIFICATION_CONCURRENCY, (row) => - finishWorkflowArchive(row.id, workspaceId, row.serverIds, { requestId }).catch((error) => - logger.warn(`[${requestId}] Post-archive notification failed for workflow ${row.id}`, { - error, + try { + const cleanupErrors: unknown[] = [] + await mapWithConcurrency(effects.workflows, ARCHIVE_NOTIFICATION_CONCURRENCY, (row) => + finishWorkflowArchive(row.id, workspaceId, row.serverIds, { + requestId, + strictExternalCleanup: options.strictExternalCleanup, + }).catch((error: unknown) => { + if (options.strictExternalCleanup) { + cleanupErrors.push(error) + return + } + logger.warn(`[${requestId}] Post-archive notification failed for workflow ${row.id}`, { + error, + }) }) ) - ) - await mcpService.clearCache(workspaceId).catch(() => undefined) - if (!mcpPubSub) return - for (const serverId of effects.serverIds) { - try { - mcpPubSub.publishWorkflowToolsChanged({ serverId, workspaceId }) - } catch (error) { - logger.warn(`[${requestId}] MCP tools-changed publish failed for server ${serverId}`, { - error, - }) + if (cleanupErrors.length > 0) throw cleanupErrors[0] + } finally { + await mcpService.clearCache(workspaceId).catch(() => undefined) + if (mcpPubSub) { + for (const serverId of effects.serverIds) { + try { + mcpPubSub.publishWorkflowToolsChanged({ serverId, workspaceId }) + } catch (error) { + logger.warn(`[${requestId}] MCP tools-changed publish failed for server ${serverId}`, { + error, + }) + } + } } } } diff --git a/apps/sim/lib/workspaces/organization-workspaces.integration.ts b/apps/sim/lib/workspaces/organization-workspaces.integration.ts index ff18f1315e8..805331de6e0 100644 --- a/apps/sim/lib/workspaces/organization-workspaces.integration.ts +++ b/apps/sim/lib/workspaces/organization-workspaces.integration.ts @@ -33,8 +33,6 @@ beforeAll(async () => { organization_id text REFERENCES organization(id) ON DELETE RESTRICT, updated_at timestamp ); - CREATE TABLE project_membership_rollout (id text PRIMARY KEY, phase text NOT NULL); - INSERT INTO project_membership_rollout VALUES ('membership', 'column'); CREATE TABLE permission_group ( id text PRIMARY KEY, organization_id text, config jsonb, updated_at timestamp ); diff --git a/apps/sim/lib/workspaces/organization-workspaces.test.ts b/apps/sim/lib/workspaces/organization-workspaces.test.ts index 34f0eae8f5f..b8457acffa2 100644 --- a/apps/sim/lib/workspaces/organization-workspaces.test.ts +++ b/apps/sim/lib/workspaces/organization-workspaces.test.ts @@ -54,6 +54,7 @@ idMockFns.mockGenerateShortId.mockReturnValue('short-id') describe('organization workspace helpers', () => { beforeEach(() => { resetDbChainMock() + dbChainMockFns.execute.mockResolvedValue([{ acquired: true }]) mockEnsureUserInOrganizationTx.mockReset() mockChangeWorkspaceStoragePayersInTx.mockReset() mockSyncUsageLimitsFromSubscription.mockResolvedValue(undefined) @@ -191,7 +192,9 @@ describe('organization workspace helpers', () => { { id: 'ws-1', ownerId: 'creator-1', billedAccountUserId: 'old-owner' }, ]) queueTableRows(schemaMock.invitation, [{ id: 'invite-pending' }, { id: 'invite-terminal' }]) - queueTableRows(schemaMock.workspace, [{ id: null }]) + queueTableRows(schemaMock.workspace, [{ id: 'project-1' }]) + queueTableRows(schemaMock.workspace, [{ id: 'ws-1' }]) + queueTableRows(schemaMock.project, [{ id: 'project-1', organizationId: 'org-1' }]) queueTableRows(schemaMock.workspace, [{ id: 'ws-1' }]) const result = @@ -245,7 +248,13 @@ describe('organization workspace helpers', () => { organizationAssignedAt: null, }) ) - expect(dbChainMockFns.update).toHaveBeenCalledTimes(1) + expect(dbChainMockFns.update.mock.calls.map(([table]) => table)).toEqual([ + schemaMock.project, + schemaMock.workspace, + ]) + expect(dbChainMockFns.set).toHaveBeenCalledWith( + expect.objectContaining({ organizationId: null, ownerId: 'owner-1' }) + ) expect(dbChainMockFns.insert).toHaveBeenCalledTimes(1) expect(dbChainMockFns.values).toHaveBeenCalledWith([ expect.objectContaining({ entityId: 'ws-1', userId: 'owner-1' }), @@ -329,7 +338,9 @@ describe('organization workspace helpers', () => { { id: 'ws-2', ownerId: 'creator-2', billedAccountUserId: 'old-owner' }, ]) queueTableRows(schemaMock.invitation, [{ id: 'invite-2' }]) - queueTableRows(schemaMock.workspace, [{ id: null }]) + queueTableRows(schemaMock.workspace, [{ id: 'project-2' }]) + queueTableRows(schemaMock.workspace, [{ id: 'ws-2' }]) + queueTableRows(schemaMock.project, [{ id: 'project-2', organizationId: 'org-1' }]) queueTableRows(schemaMock.workspace, [{ id: 'ws-2' }]) const result = await detachOrganizationWorkspaces('org-1') diff --git a/apps/sim/scripts/backfill-projects.ts b/apps/sim/scripts/backfill-projects.ts new file mode 100644 index 00000000000..82a9cf46e76 --- /dev/null +++ b/apps/sim/scripts/backfill-projects.ts @@ -0,0 +1,570 @@ +#!/usr/bin/env bun + +import { createHash } from 'node:crypto' +import { link, open, readFile, rename, stat, unlink } from 'node:fs/promises' +import { resolve } from 'node:path' +import { parseArgs } from 'node:util' +import { + assertProjectBackfillDatabase, + assignProjectBackfillBatch, + discoverProjectBackfill, + ProjectBackfillBusy, + ProjectBackfillConflict, + projectBackfillDatabaseId, + readProjectGroupingReviews, + verifyProjectBackfill, +} from '@sim/db/maintenance/project-backfill' +import { assertProjectColumnAuthority } from '@sim/db/maintenance/project-rollout' +import { withUtcTimestamps } from '@sim/db/timestamps' +import { createLogger } from '@sim/logger' +import { describeError, getTransientDatabaseFailure } from '@sim/utils/errors' +import { sleep } from '@sim/utils/helpers' +import { generateId } from '@sim/utils/id' +import { backoffWithJitter } from '@sim/utils/retry' +import postgres from 'postgres' +import { z } from 'zod' + +const logger = createLogger('ProjectBackfillOperator', { enabled: true, logLevel: 'INFO' }) +const MAX_ARTIFACT_BYTES = 128 * 1024 * 1024 +const text = z.string().min(1).max(1024) +const member = z + .object({ + id: text, + parentId: text.nullable(), + ownerId: text, + organizationId: text.nullable(), + archivedAt: text.nullable(), + projectId: text.nullable(), + legacyProjectId: text.nullable(), + }) + .strict() +const manifestSchema = z + .object({ + version: z.literal(1), + databaseId: text, + createdAt: text, + families: z + .array( + z + .object({ + rootId: text, + members: z.array(member).min(1).max(1000), + projectMembers: z.array(member).min(1).max(1000).optional(), + }) + .strict() + ) + .max(250000), + repairs: z + .array( + z + .object({ + workspaceId: text, + archivedAt: text, + workflowIds: z.array(text).min(1).max(1000), + }) + .strict() + ) + .max(250000), + groupings: z + .array( + z + .object({ + projectId: text, + evidence: text, + members: z.array(member).min(1).max(1000), + destinationProjectId: text, + roots: z.array(text).max(1000), + decision: z.enum(['pending', 'retain', 'detach']), + detachRootId: text.optional(), + }) + .strict() + ) + .max(250000) + .default([]), + conflicts: z.array(z.object({ id: text, reason: text }).strict()).max(250000), + }) + .strict() +const progressSchema = z + .object({ + version: z.literal(1), + manifestHash: text, + databaseId: text, + codeHash: text, + runId: text, + nextIndex: z.number().int().min(0), + deferred: z.array(z.number().int().min(0)), + repairsCompleted: z.array(text), + groupingsCompleted: z.array(text).default([]), + assigned: z.number().int().min(0), + projectsCreated: z.number().int().min(0), + alreadyAssigned: z.number().int().min(0), + batches: z.number().int().min(0), + problems: z.array(z.object({ id: text, reason: text })), + status: z.enum(['running', 'paused', 'incomplete', 'complete', 'failed']), + updatedAt: text, + }) + .strict() + +async function readJson(path: string): Promise { + if ((await stat(path)).size > MAX_ARTIFACT_BYTES) + throw new Error('Backfill artifact exceeds 128 MiB') + return JSON.parse(await readFile(path, 'utf8')) +} + +async function writeJson(path: string, value: unknown, replace = true): Promise { + const serialized = `${JSON.stringify(value)}\n` + if (Buffer.byteLength(serialized, 'utf8') > MAX_ARTIFACT_BYTES) + throw new Error('Backfill artifact exceeds 128 MiB') + const temporary = `${path}.${generateId()}.partial` + const file = await open(temporary, 'wx', 0o600) + try { + await file.writeFile(serialized) + await file.sync() + } finally { + await file.close() + } + try { + if (replace) await rename(temporary, path) + else await link(temporary, path) + } finally { + await unlink(temporary).catch((error: unknown) => { + if (!(error instanceof Error) || !('code' in error) || error.code !== 'ENOENT') throw error + }) + } +} + +function boundedInteger(value: string | undefined, fallback: number, max: number): number { + const result = value === undefined ? fallback : Number(value) + if (!Number.isSafeInteger(result) || result < 1 || result > max) + throw new Error(`Expected integer between 1 and ${max}`) + return result +} + +async function main() { + const { positionals, values } = parseArgs({ + args: process.argv.slice(2), + allowPositionals: true, + options: { + manifest: { type: 'string' }, + review: { type: 'string' }, + report: { type: 'string' }, + 'batch-size': { type: 'string' }, + 'max-batches': { type: 'string' }, + seconds: { type: 'string' }, + 'pause-ms': { type: 'string' }, + 'ack-release-drained': { type: 'boolean', default: false }, + help: { type: 'boolean', default: false }, + }, + }) + if (values.help || !positionals.length) { + process.stdout.write(`Operator-controlled Project inspection and exceptional archive repair. +Routine assignment and enforcement run through db:migrate.\n +From apps/sim: bun --no-env-file scripts/backfill-projects.ts [options]\n +plan Read-only discovery; writes a new --manifest PATH. Optional --review PATH carries reviewed grouping decisions.\nrepair Repair reviewed archives and explicitly selected detached families; requires --report PATH.\napply Optional maintenance: assign reviewed families from --manifest; requires --report PATH.\nverify Validate the database and repair completion; requires --manifest and --report.\nstatus Read --report; does not connect or resume.\n +Writes require MIGRATION_DATABASE_URL pointing directly to the primary and --ack-release-drained.\nDeploy #8830 and verify old servers/workers drained first. Keep the reviewed manifest and report\nin durable private job storage. Review conflicts and repairs before repair/apply. Repair requires\nthe app runtime environment (DATABASE_URL must target the same database) for provider cleanup.\n +Defaults: --batch-size 50 (1–50), --max-batches 100 (1–10000), --seconds 60 (1–3600), --pause-ms 100 (1–60000).\nA budget limits scheduling. Transactions use 3s statement limits and a 5s total limit on PG17+,\notherwise a 5s idle-transaction limit on PG16. Reuse the same report\nto resume; lost reports can be recreated safely from the same manifest. Changed families need a\nnew plan/report. Review groupings explicitly: retain a legitimate group, or select one detachRootId +and keep its generated destinationProjectId for replay. After detach, rediscover. Resolve all other +conflicts and complete archive/provider cleanup before #8590. The registered runner can read exact retained-group decisions from PROJECT_BACKFILL_REVIEW_PATH. +After an interrupted deployment, run packages/db/scripts/migrate.ts from the exact release checkout +on an authorized operator host with that private manifest. Direct invocation does not run the GHA +AWS preflight: freshly verify the compatible deployed release, old app/worker drainage and release +coordination first. Prior failed CI is not continuing proof if deployment state changed. Once the +runner succeeds and writes its normal receipt, the ordinary deployment retry needs no local file. +The registered migration first commits the durable column-authority switch after draining incompatible writers, then backfills remaining assignments, verifies, and enforces. A failed migration never returns to connector mode. Run db:migrate once before repair/apply; its committed switch remains effective if discovery requires remediation.\nExit: 0 complete, 2 running/paused/incomplete, 1 failure. Use db:migrate for routine backfill and enforcement after writer drainage.\n`) + return + } + const [command] = positionals + if ( + positionals.length !== 1 || + !['plan', 'repair', 'apply', 'verify', 'status'].includes(command) + ) + throw new Error('Unknown command; use --help') + if (values.review && command !== 'plan') throw new Error('--review is only supported by plan') + const manifestPath = values.manifest ? resolve(values.manifest) : null + const reportPath = values.report ? resolve(values.report) : null + if (manifestPath && manifestPath === reportPath) + throw new Error('Manifest and report must be different files') + if (command === 'status') { + if (!reportPath) throw new Error('status requires --report') + const progress = progressSchema.parse(await readJson(reportPath)) + logger.info('Project backfill status', progress) + process.exitCode = progress.status === 'complete' ? 0 : progress.status === 'failed' ? 1 : 2 + return + } + const rawUrl = process.env.MIGRATION_DATABASE_URL + if (!rawUrl) throw new Error('MIGRATION_DATABASE_URL is required; no application DSN fallback') + const url = new URL(rawUrl) + if (!['postgres:', 'postgresql:'].includes(url.protocol) || !url.pathname.slice(1)) + throw new Error('Invalid PostgreSQL connection') + const databaseId = projectBackfillDatabaseId(rawUrl) + const live = command === 'repair' || command === 'apply' + if (live && !values['ack-release-drained']) + throw new Error('Verify #8830 server/worker drainage and pass --ack-release-drained') + const batchSize = boundedInteger(values['batch-size'], 50, 50) + const maxBatches = boundedInteger(values['max-batches'], 100, 10000) + const seconds = boundedInteger(values.seconds, 60, 3600) + const pauseMs = boundedInteger(values['pause-ms'], 100, 60000) + /** URL startup parameters override driver options; pin the operator on every connection. */ + url.searchParams.set('search_path', 'public, pg_temp') + const sql = postgres( + url.toString(), + withUtcTimestamps({ + max: 1, + idle_timeout: 0, + max_lifetime: null, + connect_timeout: 10, + onnotice: () => {}, + connection: { + application_name: 'sim-project-backfill', + statement_timeout: 10000, + lock_timeout: 250, + }, + }) + ) + let stop = false + const requestStop = () => { + stop = true + } + if (live) { + process.on('SIGINT', requestStop) + process.on('SIGTERM', requestStop) + } + let lockPid: number | undefined + let repairRuntimeLoaded = false + async function assertLock() { + const [row] = await sql`SELECT pg_backend_pid() AS pid, EXISTS (SELECT 1 FROM pg_locks + WHERE pid = pg_backend_pid() AND locktype = 'advisory' AND granted + AND classid = ((hashtextextended('sim:project-backfill-operator',0) >> 32) & 4294967295)::oid + AND objid = (hashtextextended('sim:project-backfill-operator',0) & 4294967295)::oid + AND objsubid = 1) AS held` + if (row.pid !== lockPid || !row.held) + throw new Error('Operator session lock was lost; stop and resume with a fresh connection') + } + try { + await assertProjectBackfillDatabase(sql, live) + const [lock] = + await sql`SELECT pg_try_advisory_lock(hashtextextended('sim:project-backfill-operator',0)) AS held, pg_backend_pid() AS pid` + if (!lock.held) throw new Error('Another Project preparation or enforcement runner is active') + lockPid = lock.pid + if (live) await assertProjectColumnAuthority(sql) + if (command === 'plan') { + if (!manifestPath) throw new Error('plan requires --manifest') + const reviews = values.review + ? await readProjectGroupingReviews(resolve(values.review), databaseId) + : [] + const manifest = await discoverProjectBackfill(sql, databaseId, reviews) + await assertLock() + await writeJson(manifestPath, manifest, false) + logger.info('Reviewable Project plan written', { + databaseId, + manifestPath, + families: manifest.families.length, + repairs: manifest.repairs.length, + conflicts: manifest.conflicts.length, + }) + if (manifest.conflicts.length) process.exitCode = 2 + return + } + if (command === 'verify') { + if (!manifestPath || !reportPath) + throw new Error('Verification requires both --manifest and --report') + const counts = await verifyProjectBackfill(sql) + const manifest = manifestSchema.parse(await readJson(manifestPath)) + const report = progressSchema.parse(await readJson(reportPath)) + const hash = createHash('sha256').update(JSON.stringify(manifest)).digest('hex') + if ( + manifest.databaseId !== databaseId || + report.databaseId !== databaseId || + report.manifestHash !== hash + ) + throw new Error('Verification artifacts do not match this database and manifest') + counts.pendingCleanup = Math.max( + counts.pendingCleanup, + manifest.repairs.filter((repair) => !report.repairsCompleted.includes(repair.workspaceId)) + .length + ) + counts.conflicts = manifest.conflicts.length + await assertLock() + logger.info('Project verification', { databaseId, ...counts }) + if (Object.values(counts).some((count) => count !== 0)) process.exitCode = 2 + return + } + if (!manifestPath || !reportPath) throw new Error('Writes require --manifest and --report') + const manifest = manifestSchema.parse(await readJson(manifestPath)) + if (manifest.databaseId !== databaseId) throw new Error('Manifest belongs to another database') + const manifestHash = createHash('sha256').update(JSON.stringify(manifest)).digest('hex') + const codeHash = createHash('sha256') + .update(await readFile(import.meta.filename)) + .update( + await readFile( + new URL('../../../packages/db/maintenance/project-backfill.ts', import.meta.url) + ) + ) + .update( + await readFile( + new URL('../../../packages/db/maintenance/project-rollout.ts', import.meta.url) + ) + ) + .update(await readFile(new URL('../lib/projects/backfill-repair.ts', import.meta.url))) + .digest('hex') + let progress: z.infer + try { + progress = progressSchema.parse(await readJson(reportPath)) + } catch (error) { + if (!(error instanceof Error) || !('code' in error) || error.code !== 'ENOENT') throw error + progress = { + version: 1, + manifestHash, + databaseId, + codeHash, + runId: generateId(), + nextIndex: 0, + deferred: [], + repairsCompleted: [], + groupingsCompleted: [], + assigned: 0, + alreadyAssigned: 0, + projectsCreated: 0, + batches: 0, + problems: [], + status: 'running', + updatedAt: new Date().toISOString(), + } + } + if (progress.manifestHash !== manifestHash || progress.databaseId !== databaseId) + throw new Error('Report does not match the reviewed manifest') + if (progress.codeHash !== codeHash) + throw new Error( + 'Report belongs to different backfill code; use the original checkout or a new report' + ) + if ( + progress.nextIndex > manifest.families.length || + progress.deferred.some((index) => index >= manifest.families.length) + ) + throw new Error('Invalid report cursor') + progress.runId = generateId() + progress.status = 'running' + const started = performance.now() + let attempts = 0 + let currentBatchSize = batchSize + const pending = [ + ...new Set(progress.deferred), + ...Array.from( + { length: manifest.families.length - progress.nextIndex }, + (_, i) => progress.nextIndex + i + ), + ] + const singleRetry = new Set() + const retryCounts = new Map() + const deferred = new Set(progress.deferred) + const completedRepairs = new Set(progress.repairsCompleted) + async function checkpoint() { + progress.updatedAt = new Date().toISOString() + progress.deferred = [...deferred].sort((a, b) => a - b) + progress.repairsCompleted = [...completedRepairs] + await writeJson(reportPath as string, progress) + } + logger.info('Project operator run started', { + command, + databaseId, + runId: progress.runId, + codeHash, + batchSize, + maxBatches, + seconds, + }) + await checkpoint() + try { + if (command === 'repair') { + if ( + !process.env.DATABASE_URL || + projectBackfillDatabaseId(process.env.DATABASE_URL) !== databaseId + ) + throw new Error( + 'Repair requires DATABASE_URL and MIGRATION_DATABASE_URL to identify the same database' + ) + repairRuntimeLoaded = true + const { repairArchivedProjectEnvironment, repairProjectGrouping } = await import( + '@/lib/projects/backfill-repair' + ) + for (const repair of manifest.repairs) { + if (completedRepairs.has(repair.workspaceId)) continue + if (stop || attempts >= maxBatches || performance.now() - started >= seconds * 1000) break + await assertLock() + attempts++ + try { + await repairArchivedProjectEnvironment(sql, repair, progress.runId) + await assertLock() + completedRepairs.add(repair.workspaceId) + await checkpoint() + logger.info('Archive repair completed', { + workspaceId: repair.workspaceId, + workflows: repair.workflowIds.length, + }) + } catch (error) { + const transient = getTransientDatabaseFailure(error) + if (!(error instanceof ProjectBackfillBusy) && !transient) throw error + await assertLock() + logger.warn('Archive repair deferred', { + workspaceId: repair.workspaceId, + failure: transient ?? 'busy', + error: describeError(error), + }) + } + await sleep(pauseMs) + } + for (const grouping of manifest.groupings) { + if ( + grouping.decision !== 'detach' || + progress.groupingsCompleted.includes(grouping.projectId) + ) + continue + if (stop || attempts >= maxBatches || performance.now() - started >= seconds * 1000) break + await assertLock() + attempts++ + try { + await repairProjectGrouping(sql, grouping) + await assertLock() + progress.groupingsCompleted.push(grouping.projectId) + await checkpoint() + } catch (error) { + if (!(error instanceof ProjectBackfillBusy) && !getTransientDatabaseFailure(error)) + throw error + await assertLock() + logger.warn('Reviewed grouping repair deferred', { + projectId: grouping.projectId, + error: describeError(error), + }) + } + await sleep(pauseMs) + } + } else { + for (let position = 0; position < pending.length; ) { + if (stop || attempts >= maxBatches || performance.now() - started >= seconds * 1000) break + await assertLock() + const indices = [pending[position++]] + if (manifest.families[indices[0]].members.length === 1 && !singleRetry.has(indices[0])) { + while ( + indices.length < currentBatchSize && + position < pending.length && + manifest.families[pending[position]].members.length === 1 && + !singleRetry.has(pending[position]) + ) + indices.push(pending[position++]) + } + attempts++ + const batchStarted = performance.now() + try { + const result = await assignProjectBackfillBatch( + sql, + indices.map((index) => manifest.families[index]) + ) + await assertLock() + progress.assigned += result.assigned + progress.alreadyAssigned += result.alreadyAssigned + progress.projectsCreated += result.projectsCreated + progress.batches++ + if (performance.now() - batchStarted > 250 && currentBatchSize > 1) { + currentBatchSize = Math.max(1, Math.floor(currentBatchSize / 2)) + logger.warn('Reducing Project batch size after a slow transaction', { + currentBatchSize, + }) + } + for (const index of indices) deferred.delete(index) + progress.problems = progress.problems.filter( + (problem) => !indices.some((index) => manifest.families[index].rootId === problem.id) + ) + logger.info('Project batch committed', { + runId: progress.runId, + ...result, + elapsedMs: performance.now() - batchStarted, + totalAssigned: progress.assigned, + batches: progress.batches, + }) + } catch (error) { + const transient = getTransientDatabaseFailure(error) + if ( + !(error instanceof ProjectBackfillBusy) && + !(error instanceof ProjectBackfillConflict) && + !transient + ) + throw error + await assertLock() + for (const index of indices) { + deferred.add(index) + if (error instanceof ProjectBackfillConflict) { + const id = manifest.families[index].rootId + progress.problems = progress.problems.filter((problem) => problem.id !== id) + progress.problems.push({ id, reason: error.message }) + } + } + if (indices.length > 1) { + for (const index of indices) singleRetry.add(index) + pending.splice(position, 0, ...indices) + } else if (!(error instanceof ProjectBackfillConflict)) { + const index = indices[0] + const retries = (retryCounts.get(index) ?? 0) + 1 + retryCounts.set(index, retries) + if (retries < 3) pending.push(index) + } + logger.warn('Project batch deferred', { + roots: indices.map((index) => manifest.families[index].rootId), + failure: transient ?? (error instanceof Error ? error.name : 'unknown'), + error: describeError(error), + }) + await sleep(backoffWithJitter(1, null, { baseMs: 250, maxMs: 1000 })) + } + progress.nextIndex = Math.max(progress.nextIndex, ...indices.map((index) => index + 1)) + await checkpoint() + await sleep(pauseMs) + } + } + const counts = await verifyProjectBackfill(sql) + const pendingRepairs = manifest.repairs.filter( + (repair) => !completedRepairs.has(repair.workspaceId) + ).length + const workLeft = + progress.nextIndex < manifest.families.length || deferred.size > 0 || pendingRepairs > 0 + progress.status = + !workLeft && + !manifest.conflicts.length && + Object.values(counts).every((count) => count === 0) + ? 'complete' + : stop || attempts >= maxBatches || performance.now() - started >= seconds * 1000 + ? 'paused' + : 'incomplete' + await checkpoint() + logger.info('Project operator run finished', { + ...progress, + problems: progress.problems.length, + deferred: deferred.size, + repairsCompleted: completedRepairs.size, + elapsedMs: performance.now() - started, + verification: counts, + }) + if (progress.status !== 'complete') process.exitCode = 2 + } catch (error) { + progress.status = 'failed' + await checkpoint() + throw error + } + } finally { + process.off('SIGINT', requestStop) + process.off('SIGTERM', requestStop) + await sql.end({ timeout: 5 }) + if (repairRuntimeLoaded) { + const { mcpPubSub } = await import('@/lib/mcp/pubsub') + const { closeRedisConnection } = await import('@/lib/core/config/redis') + const { db, dbReplica } = await import('@sim/db') + mcpPubSub?.dispose() + await closeRedisConnection() + await Promise.all( + [...new Set([db.$client, dbReplica.$client])].map((client) => client.end({ timeout: 5 })) + ) + } + } +} +await main().catch((error) => { + logger.error('Project operator run failed', { error: describeError(error) }) + process.exitCode = 1 +}) diff --git a/apps/sim/scripts/test-cli-run-latency-e2e.ts b/apps/sim/scripts/test-cli-run-latency-e2e.ts index 0c26f73fe11..1846ada1cbb 100644 --- a/apps/sim/scripts/test-cli-run-latency-e2e.ts +++ b/apps/sim/scripts/test-cli-run-latency-e2e.ts @@ -183,8 +183,10 @@ async function seed() { await tx`insert into "user" (id, name, email, normalized_email, email_verified, created_at, updated_at) values (${userId}, 'CLI latency fixture', ${email}, ${email}, true, now(), now())` await tx`insert into user_stats (id, user_id) values (${generateId()}, ${userId})` - await tx`insert into workspace (id, name, owner_id, billed_account_user_id) - values (${workspaceId}, 'CLI latency fixture', ${userId}, ${userId})` + await tx`insert into project (id, name, owner_id) + values (${workspaceId}, 'E2E fixture project', ${userId})` + await tx`insert into workspace (id, project_id, name, owner_id, billed_account_user_id) + values (${workspaceId}, ${workspaceId}, 'CLI latency fixture', ${userId}, ${userId})` await tx`insert into permissions (id, user_id, entity_type, entity_id, permission_type) values (${generateId()}, ${userId}, 'workspace', ${workspaceId}, 'admin')` await tx`insert into api_key (id, user_id, name, key, key_hash, type) @@ -207,8 +209,11 @@ async function cleanup() { // after the logs that reference them. await sql`delete from workflow_execution_logs where workflow_id = ${workflowId}` await sql`delete from workflow_execution_snapshots where workflow_id = ${workflowId}` - await sql`delete from workspace where id = ${workspaceId}` - await sql`delete from "user" where id = ${userId}` + await sql.begin(async (tx) => { + await tx`delete from workspace where id = ${workspaceId}` + await tx`delete from project where id = ${workspaceId}` + await tx`delete from "user" where id = ${userId}` + }) } interface TransportMarks { diff --git a/apps/sim/scripts/test-desktop-inbox-e2e.ts b/apps/sim/scripts/test-desktop-inbox-e2e.ts index 0f107054653..69c782b2129 100644 --- a/apps/sim/scripts/test-desktop-inbox-e2e.ts +++ b/apps/sim/scripts/test-desktop-inbox-e2e.ts @@ -152,8 +152,10 @@ async function signIn(existing?: Desktop): Promise { await tx`insert into "user" (id, name, email, normalized_email, email_verified, created_at, updated_at) values (${userId}, 'Desktop inbox E2E', ${email}, ${email}, true, now(), now())` await tx`insert into user_stats (id, user_id) values (${generateId()}, ${userId})` - await tx`insert into workspace (id, name, owner_id, billed_account_user_id) - values (${workspaceId}, 'Desktop inbox E2E', ${userId}, ${userId})` + await tx`insert into project (id, name, owner_id) + values (${workspaceId}, 'E2E fixture project', ${userId})` + await tx`insert into workspace (id, project_id, name, owner_id, billed_account_user_id) + values (${workspaceId}, ${workspaceId}, 'Desktop inbox E2E', ${userId}, ${userId})` await tx`insert into permissions (id, user_id, entity_type, entity_id, permission_type) values (${generateId()}, ${userId}, 'workspace', ${workspaceId}, 'admin')` } @@ -672,8 +674,11 @@ async function cleanup() { if (userIds.length === 0) return await sql`delete from copilot_chats where user_id = any(${userIds})` await sql`delete from desktop_devices where user_id = any(${userIds})` - await sql`delete from workspace where owner_id = any(${userIds})` - await sql`delete from "user" where id = any(${userIds})` + await sql.begin(async (tx) => { + await tx`delete from workspace where owner_id = any(${userIds})` + await tx`delete from project where owner_id = any(${userIds})` + await tx`delete from "user" where id = any(${userIds})` + }) } try { diff --git a/apps/sim/scripts/test-mobile-e2e.ts b/apps/sim/scripts/test-mobile-e2e.ts index 73e54fc9733..0e7de0b3560 100644 --- a/apps/sim/scripts/test-mobile-e2e.ts +++ b/apps/sim/scripts/test-mobile-e2e.ts @@ -204,8 +204,10 @@ async function seed(): Promise { await tx`insert into "user" (id, name, email, normalized_email, email_verified, created_at, updated_at) values (${ownerId}, 'Mobile E2E', ${email}, ${email}, true, now(), now())` await tx`insert into user_stats (id, user_id) values (${generateId()}, ${ownerId})` - await tx`insert into workspace (id, name, owner_id, billed_account_user_id) - values (${workspaceId}, 'Mobile E2E workspace', ${ownerId}, ${ownerId})` + await tx`insert into project (id, name, owner_id) + values (${workspaceId}, 'Mobile E2E project', ${ownerId})` + await tx`insert into workspace (id, project_id, name, owner_id, billed_account_user_id) + values (${workspaceId}, ${workspaceId}, 'Mobile E2E workspace', ${ownerId}, ${ownerId})` await tx`insert into permissions (id, user_id, entity_type, entity_id, permission_type) values (${generateId()}, ${ownerId}, 'workspace', ${workspaceId}, 'admin')` await tx`insert into session (id, token, user_id, expires_at, created_at, updated_at) @@ -1182,9 +1184,16 @@ try { await sql.begin(async (tx) => { await tx`delete from permissions where entity_type = 'workspace' and entity_id = ${workspaceId}` await tx`delete from workspace where id = ${workspaceId}` + await tx`delete from project where id = ${workspaceId} and owner_id = ${ownerId}` await tx`delete from workflow_execution_snapshots where id = ${snapshotId}` await tx`delete from "user" where id = ${ownerId}` }) + const [remaining] = await sql`select + exists(select 1 from "user" where id = ${ownerId}) as owner, + exists(select 1 from workspace where id = ${workspaceId}) as workspace, + exists(select 1 from project where id = ${workspaceId}) as project, + exists(select 1 from workflow_execution_snapshots where id = ${snapshotId}) as snapshot` + assert.deepEqual(remaining, { owner: false, workspace: false, project: false, snapshot: false }) }) await sql.end() await check('same-origin HTTP responses succeed', undefined, async () => { diff --git a/apps/sim/scripts/test-scim-e2e.ts b/apps/sim/scripts/test-scim-e2e.ts index 4ec8e28d00b..d81e5955608 100644 --- a/apps/sim/scripts/test-scim-e2e.ts +++ b/apps/sim/scripts/test-scim-e2e.ts @@ -170,8 +170,10 @@ async function seedFixture(): Promise { values (${fixture.orgId}, 'SCIM E2E', ${`scim-e2e-${suffix}`}, now())` await tx`insert into member (id, user_id, organization_id, role) values (${generateId()}, ${fixture.ownerId}, ${fixture.orgId}, 'owner')` - await tx`insert into workspace (id, name, owner_id, billed_account_user_id, organization_id) - values (${fixture.workspaceId}, 'SCIM E2E Workspace', ${fixture.ownerId}, ${fixture.ownerId}, ${fixture.orgId})` + await tx`insert into project (id, name, owner_id, organization_id) + values (${fixture.workspaceId}, 'SCIM E2E Project', ${fixture.ownerId}, ${fixture.orgId})` + await tx`insert into workspace (id, project_id, name, owner_id, billed_account_user_id, organization_id) + values (${fixture.workspaceId}, ${fixture.workspaceId}, 'SCIM E2E Workspace', ${fixture.ownerId}, ${fixture.ownerId}, ${fixture.orgId})` await tx`insert into permissions (id, user_id, entity_type, entity_id, permission_type) values (${generateId()}, ${fixture.ownerId}, 'workspace', ${fixture.workspaceId}, 'admin')` await tx`insert into subscription (id, plan, reference_id, status, seats, metadata, period_start, period_end) @@ -222,6 +224,7 @@ async function cleanup() { await tx`delete from rate_limit_bucket where key in ( select 'route:scim:connection:' || id from scim_connection where organization_id = ${fixture.orgId} )` + await tx`delete from project where organization_id = ${fixture.orgId}` await tx`delete from organization where id = ${fixture.orgId}` if (userIds.length) await tx`delete from "user" where id in ${tx(userIds)}` }) diff --git a/apps/sim/scripts/test-workflow-stop-after-e2e.ts b/apps/sim/scripts/test-workflow-stop-after-e2e.ts index d889a1e0943..32328fd084f 100644 --- a/apps/sim/scripts/test-workflow-stop-after-e2e.ts +++ b/apps/sim/scripts/test-workflow-stop-after-e2e.ts @@ -229,8 +229,10 @@ async function seed() { await tx`insert into "user" (id, name, email, normalized_email, email_verified, created_at, updated_at) values (${ownerId}, 'Stop-after fixture', ${email}, ${email}, true, now(), now())` await tx`insert into user_stats (id, user_id) values (${generateId()}, ${ownerId})` - await tx`insert into workspace (id, name, owner_id, billed_account_user_id) - values (${workspaceId}, 'Stop-after fixture', ${ownerId}, ${ownerId})` + await tx`insert into project (id, name, owner_id) + values (${workspaceId}, 'Stop-after project', ${ownerId})` + await tx`insert into workspace (id, project_id, name, owner_id, billed_account_user_id) + values (${workspaceId}, ${workspaceId}, 'Stop-after fixture', ${ownerId}, ${ownerId})` await tx`insert into permissions (id, user_id, entity_type, entity_id, permission_type) values (${generateId()}, ${ownerId}, 'workspace', ${workspaceId}, 'admin')` await tx`insert into api_key (id, user_id, name, key, key_hash, type) @@ -837,6 +839,7 @@ try { try { await sql.begin(async (tx) => { await tx`delete from workspace where id = ${workspaceId}` + await tx`delete from project where id = ${workspaceId}` await tx`delete from "user" where id = ${ownerId}` }) break diff --git a/apps/sim/scripts/test-workflow-version-compare-e2e.ts b/apps/sim/scripts/test-workflow-version-compare-e2e.ts index da3accc1730..076fc073b86 100644 --- a/apps/sim/scripts/test-workflow-version-compare-e2e.ts +++ b/apps/sim/scripts/test-workflow-version-compare-e2e.ts @@ -223,8 +223,9 @@ async function seed() { values (${generateId()}, ${ownerId}, 'credential', ${ownerId}, ${password}, now(), now())` } for (const id of [workspaceId, foreignWorkspaceId]) { - await tx`insert into workspace (id, name, owner_id, billed_account_user_id) - values (${id}, 'Version comparison fixture', ${ownerId}, ${ownerId})` + await tx`insert into project (id, name, owner_id) values (${id}, 'Version comparison project', ${ownerId})` + await tx`insert into workspace (id, project_id, name, owner_id, billed_account_user_id) + values (${id}, ${id}, 'Version comparison fixture', ${ownerId}, ${ownerId})` } await tx`insert into permissions (id, user_id, entity_type, entity_id, permission_type) values (${generateId()}, ${ownerId}, 'workspace', ${workspaceId}, 'admin')` @@ -600,7 +601,10 @@ try { : 'remove disposable fixtures', async () => { if (!keepFixtures) { - await sql`delete from workspace where id in (${workspaceId}, ${foreignWorkspaceId})` + await sql.begin(async (tx) => { + await tx`delete from workspace where id in (${workspaceId}, ${foreignWorkspaceId})` + await tx`delete from project where id in (${workspaceId}, ${foreignWorkspaceId})` + }) await sql`delete from "user" where id in (${ownerId}, ${outsiderId})` } if (directory) await rm(directory, { recursive: true, force: true }) diff --git a/apps/sim/tools/powerbi/powerbi.integration.ts b/apps/sim/tools/powerbi/powerbi.integration.ts index 276f923b111..e3c1a55f45d 100644 --- a/apps/sim/tools/powerbi/powerbi.integration.ts +++ b/apps/sim/tools/powerbi/powerbi.integration.ts @@ -18,6 +18,7 @@ import { workflowExecutionSnapshots, workspace, } from '@sim/db/schema' +import { deleteWorkspaceFixture, insertWorkspaceFixture } from '@sim/db/testing/workspace-fixtures' import { createBlock } from '@sim/testing/factories/block.factory' import { createDelegatedPrincipal, @@ -229,9 +230,12 @@ beforeAll(async () => { })) ) await db.insert(userStats).values({ id: generateId(), userId: ownerId }) - await db - .insert(workspace) - .values({ id: workspaceId, name: 'Power BI', ownerId, billedAccountUserId: ownerId }) + await insertWorkspaceFixture(db, { + id: workspaceId, + name: 'Power BI', + ownerId, + billedAccountUserId: ownerId, + }) await db.insert(permissions).values({ id: generateId(), userId: ownerId, @@ -310,7 +314,7 @@ afterAll(async () => { ) ) } - await db.delete(workspace).where(eq(workspace.id, workspaceId)) + await deleteWorkspaceFixture(db, eq(workspace.id, workspaceId)) await db.delete(user).where(inArray(user.id, [ownerId, outsiderId])) await rm(path.join(UPLOAD_DIR_SERVER, 'execution', workspaceId), { recursive: true, diff --git a/packages/db/PROJECT_MEMBERSHIP.md b/packages/db/PROJECT_MEMBERSHIP.md deleted file mode 100644 index 9a16ffa8292..00000000000 --- a/packages/db/PROJECT_MEMBERSHIP.md +++ /dev/null @@ -1,76 +0,0 @@ -# Project membership storage and rollout - -## Current decisions - -Each workspace belongs to one Project through `workspace.project_id`; one Project can -contain many workspaces. The connector is temporary migration evidence, not the final -relationship. `project.updated_at` remains ordinary Project-record metadata, not a -workspace activity counter or a concurrency version. - -Application transactions own Project creation with its first workspace, empty-Project -prevention, archive lifecycle, workflow admission/restoration, and subtree disconnection. -Keep their Project, lineage, edge, and workspace locks. These responsibilities no longer -require permanent lifecycle/count/workflow validation triggers. - -Release 2 uses native composite foreign keys for the two structural invariants: - -- Workspace and Project organizations agree, including personal (`NULL`) scope. Both - tables have a stored generated `organization_scope_key`: `personal` for NULL and - `organization:` followed by the organization ID otherwise. The key cannot be supplied - independently by application writers. A unique Project `(id, organization_scope_key)` - index supports the workspace `(project_id, organization_scope_key)` foreign key. -- A connected fork and its parent share a Project. A unique workspace `(id, project_id)` - index supports `(forked_from_workspace_id, project_id)` referencing that pair. The - existing parent-ID foreign key retains `ON DELETE SET NULL`; it clears only the parent - pointer. The composite check uses deferred `NO ACTION`, not cascading Project moves. - -The composite constraints are `DEFERRABLE INITIALLY DEFERRED` so a transaction can move -an entire subtree or organization before validation. The ordinary required Project-ID -foreign key remains restrictive. Projects may contain disconnected roots; connected -membership does not imply one tree per Project. - -These native constraints supersede the custom Project integrity triggers, their advisory -lock wrappers, row-version dispatch, and `UPDATE project SET updated_at = updated_at`. -PostgreSQL owns referential-integrity concurrency; application business locks remain. -Drizzle records the columns, indexes, and FK relationships. The shared finalizer sets -deferred timing after fresh schema push because Drizzle does not represent that timing. - -## Two releases - -1. #8830 adds nullable membership and the authority marker. Connector-mode writes remain - connector-only. Column-mode writes use the workspace column, with connector fallback - only for unassigned legacy rows. It performs no bulk membership backfill. -2. After verifying incompatible membership-sensitive requests/workers have drained, - #8590 switches authority, performs bounded backfill/reconciliation, validates native - constraints, requires membership, and retires the connector and authority marker atomically. Its migration runs - while #8830 is serving, before the new app is promoted. - -The all-at-once traffic switch stops fresh requests to retired app servers. Remaining -in-flight operations and independently scheduled workers must be identified explicitly. -No new infrastructure maintenance mechanism, synchronization triggers, dual writes, or -extra compatibility release is required. After authority switches, #8830 is the oldest -supported application rollback; retain the database changes. #8830 recognizes the completed schema (no connector or -marker, required membership, and validated foreign keys) as column authority; a missing -marker before completion remains an error. This supersedes retaining the marker for rollback. - -On PostgreSQL 16/17, adding stored generated scope columns rewrites their tables. The -finalizer bounds the rewrite and refuses busy tables without waiting. A timeout leaves -the connector available and requires retry/capacity assessment, never a silent increase -in lock duration. Supporting unique indexes build concurrently; constraints install as -NOT VALID and validate before contraction. This is a bounded blocking schema operation, -not an online column addition with zero write interruption. - -Backfill still verifies and repairs legacy lifecycle state before contraction. That -one-time migration prerequisite does not make lifecycle policy database-owned forever. - -## Validation and downstream work - -Use focused local Docker database checks and local HTTP servers: null scope transitions, -atomic transfers/subtree moves, parent deletion, stale snapshots/concurrent writers, -replay, schema push, application lifecycle, and the unchanged #8830 rollback runtime. -Run full CI only on GitHub Actions. Do not add Project-specific PostgreSQL 16 CI. - -Changes to these decisions apply to both #8830 and #8590. Downstream #8609 and #8610 -must retain the same storage boundary when synchronized; their separate worktrees are -not updated merely by updating this stack. Historical test reports prove earlier code, -not the current implementation or a requirement to preserve superseded trigger tests. diff --git a/packages/db/drizzle.config.ts b/packages/db/drizzle.config.ts index cffe391ded6..23aef61ba21 100644 --- a/packages/db/drizzle.config.ts +++ b/packages/db/drizzle.config.ts @@ -18,6 +18,7 @@ export default { /** Runner-owned journals and resumable cleanup cursors must survive a development schema push. */ tablesFilter: [ '!script_migrations', + '!project_backfill_archive_repairs', '!search_embedding_cleanup_progress', '!search_embedding_cleanup_targets', '!search_retirement_*', diff --git a/packages/db/maintenance/project-backfill.test.ts b/packages/db/maintenance/project-backfill.test.ts new file mode 100644 index 00000000000..bf7be552419 --- /dev/null +++ b/packages/db/maintenance/project-backfill.test.ts @@ -0,0 +1,16 @@ +import { projectBackfillDatabaseId } from '@sim/db/maintenance/project-backfill' +import { describe, expect, it } from 'vitest' + +describe('Project manifest database binding', () => { + it('distinguishes socket routing and query overrides without binding credentials', () => { + expect(projectBackfillDatabaseId('postgres:///prod?host=/var/run/postgresql')).not.toBe( + projectBackfillDatabaseId('postgres:///prod?host=/tmp/other') + ) + expect(projectBackfillDatabaseId('postgres://localhost/prod?port=5433')).not.toBe( + projectBackfillDatabaseId('postgres://localhost/prod?port=5434') + ) + expect(projectBackfillDatabaseId('postgres://alice:one@localhost/prod')).toBe( + projectBackfillDatabaseId('postgres://bob:two@localhost:5432/prod') + ) + }) +}) diff --git a/packages/db/maintenance/project-backfill.ts b/packages/db/maintenance/project-backfill.ts new file mode 100644 index 00000000000..ecdecba7c9f --- /dev/null +++ b/packages/db/maintenance/project-backfill.ts @@ -0,0 +1,702 @@ +import { createHash } from 'node:crypto' +import { readFile, stat } from 'node:fs/promises' +import { countPendingProjectArchiveRepairs } from '@sim/db/maintenance/project-repairs' +import { assertProjectColumnAuthority } from '@sim/db/maintenance/project-rollout' +import { createLogger } from '@sim/logger' +import { generateId } from '@sim/utils/id' +import { isRecordLike } from '@sim/utils/object' +import { compareStrings, truncateAtCodePoint } from '@sim/utils/string' +import type { Sql, TransactionSql } from 'postgres' + +const logger = createLogger('ProjectBackfill') +const MAX_WORKSPACES = 250_000 +const MAX_FAMILY = 1_000 + +export type ProjectBackfillWorkspace = { + id: string + parentId: string | null + ownerId: string + organizationId: string | null + archivedAt: string | null + projectId: string | null + legacyProjectId: string | null +} + +interface ProjectBackfillFamily { + rootId: string + members: ProjectBackfillWorkspace[] + projectMembers?: ProjectBackfillWorkspace[] +} + +export interface ProjectArchiveRepair { + workspaceId: string + archivedAt: string + workflowIds: string[] +} + +export interface ProjectGroupingReview { + projectId: string + evidence: string + members: ProjectBackfillWorkspace[] + destinationProjectId: string + roots: string[] + decision: 'pending' | 'retain' | 'detach' + detachRootId?: string +} + +interface ProjectBackfillManifest { + version: 1 + databaseId: string + createdAt: string + families: ProjectBackfillFamily[] + repairs: ProjectArchiveRepair[] + groupings: ProjectGroupingReview[] + conflicts: { id: string; reason: string }[] +} + +/** A changed plan or ambiguous legacy state requires new discovery, never a guessed merge. */ +export class ProjectBackfillConflict extends Error { + readonly code = '55000' +} + +/** Contention releases the whole transaction before the controller defers the work. */ +export class ProjectBackfillBusy extends Error {} + +/** Non-secret identity binds reviewed intent to one database endpoint and database name. */ +export function projectBackfillDatabaseId(url: string): string { + const target = new URL(url) + return createHash('sha256') + .update( + JSON.stringify({ + host: target.hostname.toLowerCase() || process.env.PGHOST || 'localhost', + port: target.port || process.env.PGPORT || '5432', + database: target.pathname, + routing: [ + 'host', + 'hostaddr', + 'hostname', + 'port', + 'database', + 'dbname', + 'service', + 'servicefile', + 'target_session_attrs', + ].map((key) => [key, target.searchParams.getAll(key)]), + }) + ) + .digest('hex') +} + +/** Binds an explicit grouping decision to membership, lineage, lifecycle and ownership evidence. */ +export function projectGroupingEvidence(rows: ProjectBackfillWorkspace[]): string { + return createHash('sha256') + .update( + JSON.stringify( + [...rows] + .sort((a, b) => compareStrings(a.id, b.id)) + .map((row) => ({ + id: row.id, + parentId: row.parentId, + ownerId: row.ownerId, + organizationId: row.organizationId, + archivedAt: row.archivedAt, + projectId: effectiveProjectId(row), + })) + ) + ) + .digest('hex') +} + +function isProjectBackfillWorkspace(value: unknown): value is ProjectBackfillWorkspace { + return ( + isRecordLike(value) && + typeof value.id === 'string' && + typeof value.ownerId === 'string' && + ['parentId', 'organizationId', 'archivedAt', 'projectId', 'legacyProjectId'].every( + (key) => value[key] === null || typeof value[key] === 'string' + ) + ) +} + +/** Exceptional operator review stays in the existing private manifest, never the retiring connector. */ +export async function readProjectGroupingReviews( + path: string, + databaseId: string +): Promise { + if ((await stat(path)).size > 128 * 1024 * 1024) + throw new Error('Project review exceeds artifact size limit') + const value: unknown = JSON.parse(await readFile(path, 'utf8')) + if ( + !isRecordLike(value) || + value.version !== 1 || + value.databaseId !== databaseId || + !Array.isArray(value.groupings) + ) + throw new Error('Project grouping review does not match this database') + const reviews: ProjectGroupingReview[] = [] + for (const item of value.groupings) { + if ( + !isRecordLike(item) || + !Array.isArray(item.members) || + item.members.length > MAX_FAMILY || + !item.members.every(isProjectBackfillWorkspace) || + typeof item.destinationProjectId !== 'string' || + typeof item.projectId !== 'string' || + typeof item.evidence !== 'string' || + !Array.isArray(item.roots) || + item.roots.length > MAX_FAMILY || + !item.roots.every((root): root is string => typeof root === 'string') || + !['pending', 'retain', 'detach'].includes(String(item.decision)) || + (item.detachRootId !== undefined && typeof item.detachRootId !== 'string') + ) + throw new Error('Invalid Project grouping review') + if (projectGroupingEvidence(item.members) !== item.evidence) + throw new Error('Project grouping evidence does not match the reviewed members') + if (reviews.some((review) => review.projectId === item.projectId)) + throw new Error('Duplicate Project grouping review') + reviews.push({ + projectId: item.projectId, + evidence: item.evidence, + members: item.members, + destinationProjectId: item.destinationProjectId, + roots: item.roots, + decision: item.decision as ProjectGroupingReview['decision'], + ...(typeof item.detachRootId === 'string' ? { detachRootId: item.detachRootId } : {}), + }) + } + return reviews +} + +/** Checks physical prerequisites without trusting a local migration journal or a CLI flag. */ +export async function assertProjectBackfillDatabase(sql: Sql, write: boolean): Promise { + const [state] = await sql` + SELECT pg_is_in_recovery() AS replica, current_setting('transaction_read_only') AS readonly, + current_setting('server_version_num')::int AS version, + EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema = 'public' + AND table_name = 'workspace' AND column_name = 'project_id') AS expanded + ` + if (!state?.expanded) throw new Error('Deploy #8830 before Project preparation') + if (state.version < 160000) throw new Error('Project preparation requires PostgreSQL 16+') + if (write && (state.replica || state.readonly === 'on')) + throw new Error('Project writes require the primary database') +} + +/** The contract locks workspace before retiring the connector; this read lock spans each query batch. */ +async function hasLegacyMemberships(tx: TransactionSql): Promise { + await tx`LOCK TABLE workspace IN ACCESS SHARE MODE` + const [state] = await tx`SELECT to_regclass('public.project_workspace') IS NOT NULL AS present` + return state.present +} + +function legacyAssignment(tx: TransactionSql, legacy: boolean) { + return legacy + ? tx`(SELECT pw.project_id FROM project_workspace pw WHERE pw.workspace_id = w.id)` + : tx`NULL::text` +} + +function effectiveProjectId(row: ProjectBackfillWorkspace): string | null { + return row.projectId ?? row.legacyProjectId +} + +/** Bounded discovery materializes only identity/scope metadata, never workflow content. */ +export async function discoverProjectBackfill( + sql: Sql, + databaseId: string, + reviews: ProjectGroupingReview[] = [] +): Promise { + const rows: ProjectBackfillWorkspace[] = [] + let legacyPresent = false + let after = '' + for (;;) { + const page = await sql.begin(async (tx) => { + const legacy = await hasLegacyMemberships(tx) + legacyPresent ||= legacy + return tx` + SELECT w.id, w.forked_from_workspace_id AS "parentId", w.owner_id AS "ownerId", + w.organization_id AS "organizationId", w.archived_at::text AS "archivedAt", + w.project_id AS "projectId", ${legacyAssignment(tx, legacy)} AS "legacyProjectId" + FROM workspace w WHERE w.id COLLATE "C" > ${after} COLLATE "C" + ORDER BY w.id COLLATE "C" LIMIT 1000 + ` + }) + if (!page.length) break + rows.push(...page) + if (rows.length > MAX_WORKSPACES) + throw new Error('Project discovery exceeds the workspace limit') + after = page[page.length - 1].id + } + const manifest: ProjectBackfillManifest = { + version: 1, + databaseId, + createdAt: new Date().toISOString(), + families: [], + repairs: [], + groupings: [], + conflicts: [], + } + const byId = new Map(rows.map((row) => [row.id, row])) + const rootById = new Map() + const groups = new Map() + for (const row of rows) { + let current: ProjectBackfillWorkspace | undefined = row + const path = new Set() + let root: string | null = null + while (current) { + if (rootById.has(current.id)) { + root = rootById.get(current.id) ?? null + break + } + if (path.has(current.id)) break + path.add(current.id) + if (!current.parentId) { + root = current.id + break + } + current = byId.get(current.parentId) + } + for (const id of path) rootById.set(id, root) + if (!root) { + manifest.conflicts.push({ id: row.id, reason: 'Fork cycle or missing parent' }) + continue + } + const group = groups.get(root) + if (group) group.push(row) + else groups.set(root, [row]) + } + const projects = await sql< + { id: string; ownerId: string; organizationId: string | null; archivedAt: string | null }[] + >` + SELECT id, owner_id AS "ownerId", organization_id AS "organizationId", archived_at::text AS "archivedAt" FROM project LIMIT 250001 + ` + if (projects.length > MAX_WORKSPACES) + throw new Error('Project discovery exceeds the Project limit') + const affectedProjects = new Set() + const projectMembers = new Map() + for (const row of rows) { + const id = effectiveProjectId(row) + if (!id) continue + const members = projectMembers.get(id) ?? [] + members.push(row) + projectMembers.set(id, members) + } + for (const project of projects) { + const members = projectMembers.get(project.id) ?? [] + if (!members.length) { + manifest.conflicts.push({ + id: project.id, + reason: 'Empty Project requires explicit dependency review and operator remediation', + }) + continue + } + const roots = new Set(members.map((row) => rootById.get(row.id))) + if ( + roots.size > 1 && + members.some((row) => row.parentId === null && row.legacyProjectId !== project.id) + ) { + if (legacyPresent) { + const evidence = projectGroupingEvidence(members) + const reviewed = reviews.find( + (review) => review.projectId === project.id && review.evidence === evidence + ) + const decision = reviewed?.decision ?? 'pending' + if (members.length > MAX_FAMILY) { + manifest.conflicts.push({ + id: project.id, + reason: 'Oversized ambiguous Project grouping requires operator remediation', + }) + continue + } + manifest.groupings.push({ + projectId: project.id, + evidence, + members, + destinationProjectId: reviewed?.destinationProjectId ?? generateId(), + roots: [...roots] + .filter((root): root is string => typeof root === 'string') + .sort(compareStrings), + decision, + ...(reviewed?.detachRootId ? { detachRootId: reviewed.detachRootId } : {}), + }) + if (decision !== 'retain') + manifest.conflicts.push({ + id: project.id, + reason: + 'Ambiguous multi-root Project grouping; review this manifest grouping or repair the known detached family before rediscovery', + }) + } + } + if ( + new Set(members.map((row) => row.organizationId)).size !== 1 || + (!members[0].organizationId && members.some((row) => row.ownerId !== project.ownerId)) + ) { + affectedProjects.add(project.id) + } + if ( + members.some((row) => row.organizationId !== project.organizationId) || + (project.archivedAt === null) !== members.some((row) => row.archivedAt === null) + ) + affectedProjects.add(project.id) + } + const plannedProjectMembers = new Map( + [...projectMembers].map(([id, members]) => [id, [...members]]) + ) + for (const members of groups.values()) { + const projectIds = [ + ...new Set(members.map(effectiveProjectId).filter((id): id is string => id !== null)), + ] + if (projectIds.length !== 1) continue + const planned = plannedProjectMembers.get(projectIds[0]) + if (planned) planned.push(...members.filter((row) => effectiveProjectId(row) === null)) + } + const scheduledMetadata = new Set() + for (const [rootId, members] of groups) { + const family = { rootId, members } + try { + const complete = + members.every((row) => row.projectId !== null) && + new Set(members.map((row) => row.projectId)).size === 1 + validateFamily(family, complete) + if ( + complete && + !members.some( + (row) => + row.projectId && + affectedProjects.has(row.projectId) && + !scheduledMetadata.has(row.projectId) + ) + ) + continue + validateFamily(family) + const projectId = members.map(effectiveProjectId).find((id) => id !== null) + if (projectId && (plannedProjectMembers.get(projectId)?.length ?? 0) > MAX_FAMILY) + throw new ProjectBackfillConflict('Oversized Project repair requires operator remediation') + if (projectId) scheduledMetadata.add(projectId) + manifest.families.push({ + ...family, + ...(projectId ? { projectMembers: plannedProjectMembers.get(projectId) } : {}), + }) + } catch (error) { + if (!(error instanceof ProjectBackfillConflict)) throw error + manifest.conflicts.push({ id: rootId, reason: error.message }) + } + } + manifest.families.sort((a, b) => compareStrings(a.rootId, b.rootId)) + let afterWorkflow = '' + const repairs = new Map() + for (;;) { + const page = await sql<{ id: string; workspaceId: string; archivedAt: string }[]>` + SELECT f.id, w.id AS "workspaceId", w.archived_at::text AS "archivedAt" + FROM workflow f JOIN workspace w ON w.id = f.workspace_id + WHERE w.archived_at IS NOT NULL AND f.archived_at IS NULL + AND f.id COLLATE "C" > ${afterWorkflow} COLLATE "C" + ORDER BY f.id COLLATE "C" LIMIT 1000 + ` + if (!page.length) break + for (const row of page) { + const repair = repairs.get(row.workspaceId) ?? { + workspaceId: row.workspaceId, + archivedAt: row.archivedAt, + workflowIds: [], + } + repair.workflowIds.push(row.id) + if (repair.workflowIds.length > MAX_FAMILY) + throw new Error(`Archive repair exceeds workflow limit: ${row.workspaceId}`) + repairs.set(row.workspaceId, repair) + } + afterWorkflow = page[page.length - 1].id + if (repairs.size > MAX_WORKSPACES) throw new Error('Too many archive repairs') + } + manifest.repairs = [...repairs.values()].sort((a, b) => + compareStrings(a.workspaceId, b.workspaceId) + ) + logger.info('Project discovery finished', { + workspaces: rows.length, + families: manifest.families.length, + repairs: manifest.repairs.length, + conflicts: manifest.conflicts.length, + }) + return manifest +} + +function validateFamily(family: ProjectBackfillFamily, complete = false): void { + const root = family.members.find((row) => row.id === family.rootId) + if (!root || root.parentId || (!complete && family.members.length > MAX_FAMILY)) + throw new ProjectBackfillConflict('Invalid or oversized fork family') + const members = new Map(family.members.map((row) => [row.id, row])) + if (members.size !== family.members.length) + throw new ProjectBackfillConflict('Duplicate family member') + const connected = new Set([root.id]) + for (const row of family.members) { + let current = row + const visited = new Set() + while (!connected.has(current.id)) { + if (visited.has(current.id)) throw new ProjectBackfillConflict('Cyclic family') + visited.add(current.id) + const parent = current.parentId ? members.get(current.parentId) : undefined + if (!parent) throw new ProjectBackfillConflict('Disconnected family member') + current = parent + } + for (const id of visited) connected.add(id) + } + if (family.members.some((row) => row.organizationId !== root.organizationId)) + throw new ProjectBackfillConflict('Fork family spans organizations') + if (!root.organizationId && family.members.some((row) => row.ownerId !== root.ownerId)) + throw new ProjectBackfillConflict( + 'Personal family has multiple lifecycle owners; select an owner before rediscovery' + ) + if (new Set(family.members.map(effectiveProjectId).filter(Boolean)).size > 1) + throw new ProjectBackfillConflict('Fork family spans Projects') +} + +/** All lock keys match deployed writers; nonblocking acquisition avoids lock-order inversions. */ +export async function tryProjectBackfillLocks(tx: TransactionSql, keys: string[]): Promise { + const [result] = await tx` + SELECT bool_and(pg_try_advisory_xact_lock(hashtextextended(key, 0))) AS acquired + FROM unnest(${keys}::text[]) AS key /*lock='project_backfill'*/ + ` + if (!result?.acquired) throw new ProjectBackfillBusy('Project backfill target is busy') +} + +/** Writes at most 50 independent families, or one complete fork family, in a bounded transaction. */ +export async function assignProjectBackfillBatch(sql: Sql, families: ProjectBackfillFamily[]) { + if ( + !families.length || + families.length > 50 || + (families.length > 1 && families.some((f) => f.members.length !== 1)) + ) + throw new Error('Expected 1–50 singletons or one complete fork family') + for (const family of families) validateFamily(family) + return sql.begin(async (tx) => { + await tx`SELECT set_config(CASE WHEN current_setting('transaction_timeout', true) IS NULL + THEN 'idle_in_transaction_session_timeout' ELSE 'transaction_timeout' END, '5s', true)` + await tx`SET LOCAL statement_timeout = '3s'` + await tx`SET LOCAL lock_timeout = '250ms'` + const legacy = await hasLegacyMemberships(tx) + await assertProjectColumnAuthority(tx) + const roots = families.map((f) => f.rootId).sort(compareStrings) + const expected = families.flatMap((f) => f.members).sort((a, b) => compareStrings(a.id, b.id)) + const ids = expected.map((row) => row.id) + if (new Set(ids).size !== ids.length) + throw new ProjectBackfillConflict('Duplicate workspace in batch') + await tryProjectBackfillLocks( + tx, + roots.map((id) => `fork-lineage:${id}`) + ) + await tryProjectBackfillLocks( + tx, + ids.map((id) => `project-backfill:${id}`) + ) + const before = await tx<{ projectId: string }[]>` + SELECT DISTINCT coalesce(w.project_id, ${legacyAssignment(tx, legacy)}) COLLATE "C" AS "projectId" + FROM workspace w WHERE w.id = ANY(${ids}) + AND coalesce(w.project_id, ${legacyAssignment(tx, legacy)}) IS NOT NULL + ORDER BY "projectId" + ` + if (before.length) + await tryProjectBackfillLocks( + tx, + before.map((row) => `project:${row.projectId}`) + ) + await tx`SELECT id FROM workspace WHERE id = ANY(${ids}) ORDER BY id COLLATE "C" FOR NO KEY UPDATE NOWAIT` + if (legacy) { + await tx`SELECT pw.workspace_id FROM project_workspace pw JOIN workspace w ON w.id = pw.workspace_id + WHERE w.id = ANY(${ids}) AND w.project_id IS NULL + ORDER BY pw.workspace_id COLLATE "C" FOR SHARE OF pw NOWAIT` + } + const current = await tx<(ProjectBackfillWorkspace & { name: string })[]>` + SELECT w.id, w.name, w.forked_from_workspace_id AS "parentId", w.owner_id AS "ownerId", + w.organization_id AS "organizationId", w.archived_at::text AS "archivedAt", + w.project_id AS "projectId", ${legacyAssignment(tx, legacy)} AS "legacyProjectId" + FROM workspace w WHERE w.id = ANY(${ids}) ORDER BY w.id COLLATE "C" + ` + const lockedProjects = new Set(before.map((row) => row.projectId)) + const newProjects = [...new Set(current.map(effectiveProjectId))] + .filter((id): id is string => id !== null && !lockedProjects.has(id)) + .sort(compareStrings) + if (newProjects.length) + await tryProjectBackfillLocks( + tx, + newProjects.map((id) => `project:${id}`) + ) + if (current.length !== expected.length) + throw new ProjectBackfillConflict('A reviewed workspace disappeared; rediscover') + const byId = new Map(current.map((row) => [row.id, row])) + for (const row of expected) { + const actual = byId.get(row.id) + if ( + !actual || + actual.parentId !== row.parentId || + actual.ownerId !== row.ownerId || + actual.organizationId !== row.organizationId || + actual.archivedAt !== row.archivedAt || + (row.projectId !== null && actual.projectId !== row.projectId) || + (effectiveProjectId(row) !== null && + effectiveProjectId(actual) !== effectiveProjectId(row)) || + (actual.projectId === null && actual.legacyProjectId !== row.legacyProjectId) + ) + throw new ProjectBackfillConflict( + 'Reviewed workspace scope, owner, archive state or assignment changed; rediscover' + ) + } + const children = + await tx`SELECT id FROM workspace WHERE forked_from_workspace_id = ANY(${ids}) AND NOT id = ANY(${ids}) LIMIT 1` + if (children.length) + throw new ProjectBackfillConflict('Reviewed fork family gained descendants; rediscover') + const invalidArchive = + await tx`SELECT f.id FROM workflow f JOIN workspace w ON w.id = f.workspace_id + WHERE w.id = ANY(${ids}) AND w.archived_at IS NOT NULL AND f.archived_at IS NULL LIMIT 1` + if (invalidArchive.length) + throw new ProjectBackfillConflict('Archive repair is required before assignment') + const inserts: { + id: string + name: string + owner_id: string + organization_id: string | null + archived_at: string | null + }[] = [] + const assignments: { id: string; project_id: string }[] = [] + let alreadyAssigned = 0 + for (const family of families) { + const root = byId.get(family.rootId) + if (!root) throw new ProjectBackfillConflict('Root disappeared') + const members = family.members + .map((row) => byId.get(row.id)) + .filter((row) => row !== undefined) + const projectIds = [ + ...new Set(members.map(effectiveProjectId).filter((id): id is string => id !== null)), + ] + if (projectIds.length > 1) throw new ProjectBackfillConflict('Family now spans Projects') + const archiveAt = members.every((row) => row.archivedAt !== null) + ? (members + .map((row) => row.archivedAt ?? '') + .sort(compareStrings) + .at(-1) ?? null) + : null + let projectId = projectIds[0] + if (projectId) { + const [existing] = + await tx`SELECT owner_id, organization_id, archived_at::text FROM project WHERE id = ${projectId} FOR NO KEY UPDATE NOWAIT` + const reviewedIds = family.projectMembers?.map((row) => row.id) ?? [] + const evidenceRows = await tx` + SELECT w.id, w.owner_id AS "ownerId", w.organization_id AS "organizationId", w.archived_at::text AS "archivedAt", + w.forked_from_workspace_id AS "parentId", w.project_id AS "projectId", ${legacyAssignment(tx, legacy)} AS "legacyProjectId" + FROM workspace w WHERE coalesce(w.project_id, ${legacyAssignment(tx, legacy)}) = ${projectId} + OR w.id = ANY(${reviewedIds}) + ORDER BY w.id COLLATE "C" LIMIT 1001 FOR NO KEY UPDATE NOWAIT + ` + const environments = evidenceRows.filter((row) => effectiveProjectId(row) === projectId) + if (!existing || evidenceRows.length > MAX_FAMILY) + throw new ProjectBackfillConflict( + 'Missing Project or oversized Project repair; operator remediation required' + ) + if (family.projectMembers) { + const reviewed = new Map( + [ + ...family.projectMembers, + ...family.members.filter((row) => effectiveProjectId(row) === null), + ].map((row) => [row.id, row]) + ) + const currentIds = new Set(evidenceRows.map((row) => row.id)) + if ( + family.projectMembers.some((row) => !currentIds.has(row.id)) || + evidenceRows.some((row) => { + const expected = reviewed.get(row.id) + return ( + !expected || + row.parentId !== expected.parentId || + row.ownerId !== expected.ownerId || + row.organizationId !== expected.organizationId || + row.archivedAt !== expected.archivedAt || + (effectiveProjectId(expected) !== null + ? effectiveProjectId(row) !== effectiveProjectId(expected) + : effectiveProjectId(row) !== null && effectiveProjectId(row) !== projectId) + ) + }) + ) + throw new ProjectBackfillConflict('Reviewed Project membership changed; rediscover') + } + const allMembers = [ + ...environments, + ...members.filter((row) => effectiveProjectId(row) === null), + ] + if ( + allMembers.some((row) => row.organizationId !== root.organizationId) || + (!root.organizationId && allMembers.some((row) => row.ownerId !== existing.owner_id)) || + (existing.organization_id !== root.organizationId && + allMembers.some((row) => row.ownerId !== existing.owner_id)) + ) + throw new ProjectBackfillConflict( + 'Existing Project has ambiguous ownership or scope; operator remediation required' + ) + const projectArchiveAt = allMembers.some((row) => row.archivedAt === null) + ? null + : (allMembers + .map((row) => row.archivedAt ?? '') + .sort(compareStrings) + .at(-1) ?? null) + if ( + existing.organization_id !== root.organizationId || + (existing.archived_at === null) !== (projectArchiveAt === null) + ) { + if (!family.projectMembers) + throw new ProjectBackfillConflict( + 'Project repair needs fresh Project-wide evidence; rediscover' + ) + await tx`UPDATE project SET organization_id = ${root.organizationId}, + archived_at = ${projectArchiveAt}::text::timestamp, updated_at = now() WHERE id = ${projectId}` + } + } else { + projectId = generateId() + inserts.push({ + id: projectId, + name: `${truncateAtCodePoint(root.name.trim() || 'Untitled', 90, '')} - Project`, + owner_id: root.ownerId, + organization_id: root.organizationId, + archived_at: archiveAt, + }) + } + for (const member of members) { + if (member.projectId) alreadyAssigned++ + else assignments.push({ id: member.id, project_id: projectId }) + } + } + if (inserts.length) + await tx`INSERT INTO project (id,name,owner_id,organization_id,archived_at) + SELECT id,name,owner_id,organization_id,archived_at::timestamp FROM jsonb_to_recordset(${JSON.stringify(inserts)}::text::jsonb) + AS p(id text,name text,owner_id text,organization_id text,archived_at text)` + let assigned = 0 + if (assignments.length) { + const result = await tx`UPDATE workspace w SET project_id = p.project_id + FROM jsonb_to_recordset(${JSON.stringify(assignments)}::text::jsonb) AS p(id text, project_id text) + WHERE w.id = p.id AND w.project_id IS NULL RETURNING w.id` + assigned = result.length + if (assigned !== assignments.length) + throw new ProjectBackfillConflict('Assignment changed while locked') + } + return { assigned, alreadyAssigned, projectsCreated: inserts.length } + }) +} + +/** Fresh whole-database validation is independent of manifest pagination and progress counters. */ +export async function verifyProjectBackfill(sql: Sql): Promise> { + const [counts] = await sql[]>` + SELECT + (SELECT count(*)::int FROM workspace WHERE project_id IS NULL) AS unassigned, + (SELECT count(*)::int FROM workspace w LEFT JOIN project p ON p.id = w.project_id + WHERE w.project_id IS NOT NULL AND (p.id IS NULL OR w.organization_id IS DISTINCT FROM p.organization_id)) AS scope, + (SELECT count(*)::int FROM workspace w LEFT JOIN workspace parent ON parent.id = w.forked_from_workspace_id + WHERE w.forked_from_workspace_id IS NOT NULL AND (parent.id IS NULL OR parent.project_id IS DISTINCT FROM w.project_id)) AS lineage, + (SELECT count(*)::int FROM project p WHERE NOT EXISTS (SELECT 1 FROM workspace w WHERE w.project_id = p.id) + OR (p.archived_at IS NULL AND NOT EXISTS (SELECT 1 FROM workspace w WHERE w.project_id = p.id AND w.archived_at IS NULL)) + OR (p.archived_at IS NOT NULL AND EXISTS (SELECT 1 FROM workspace w WHERE w.project_id = p.id AND w.archived_at IS NULL))) AS projects, + (SELECT count(*)::int FROM workflow f JOIN workspace w ON w.id = f.workspace_id + LEFT JOIN project p ON p.id = w.project_id WHERE f.archived_at IS NULL AND (w.archived_at IS NOT NULL OR p.archived_at IS NOT NULL)) AS archive, + (SELECT count(*)::int FROM project p LEFT JOIN "user" u ON u.id = p.owner_id + LEFT JOIN organization o ON o.id = p.organization_id WHERE u.id IS NULL OR (p.organization_id IS NOT NULL AND o.id IS NULL)) AS owners, + (WITH RECURSIVE reachable(id) AS (SELECT id FROM workspace WHERE forked_from_workspace_id IS NULL + UNION SELECT w.id FROM workspace w JOIN reachable r ON w.forked_from_workspace_id = r.id) + SELECT count(*)::int FROM workspace w LEFT JOIN reachable r ON r.id = w.id WHERE r.id IS NULL) AS unreachable + ` + counts.pendingCleanup = await countPendingProjectArchiveRepairs(sql) + return counts +} diff --git a/packages/db/maintenance/project-enforcement.ts b/packages/db/maintenance/project-enforcement.ts new file mode 100644 index 00000000000..174120432ef --- /dev/null +++ b/packages/db/maintenance/project-enforcement.ts @@ -0,0 +1,35 @@ +import { readFile } from 'node:fs/promises' +import { getPostgresErrorCode } from '@sim/utils/errors' +import type { Sql } from 'postgres' + +/** Installs the same bounded, replayable contract for migrations and fresh schema pushes. */ +export async function enforceProjectMembership(sql: Sql): Promise { + const source = await readFile(new URL('./project-membership.sql', import.meta.url), 'utf8') + const connection = await sql.reserve() + let previousSearchPath: string | undefined + try { + const [settings] = await connection<{ search_path: string }[]>`SHOW search_path` + previousSearchPath = settings.search_path + /** The SQL spans committed phases, so pin this reserved session until all phases finish. */ + await connection`SELECT set_config('search_path', 'public, pg_temp', false)` + for (const statement of source.split('--> statement-breakpoint')) { + if (statement.trim()) await connection.unsafe(statement) + } + } catch (error) { + if (!['25P03', '25P04', 'CONNECTION_CLOSED'].includes(getPostgresErrorCode(error) ?? '')) { + await connection.unsafe('ROLLBACK') + } + throw error + } finally { + await connection + .unsafe("SELECT pg_advisory_unlock(hashtextextended('sim:project-backfill-operator', 0))") + .catch(() => undefined) + try { + if (previousSearchPath !== undefined) { + await connection`SELECT set_config('search_path', ${previousSearchPath}, false)` + } + } finally { + connection.release() + } + } +} diff --git a/packages/db/maintenance/project-membership.sql b/packages/db/maintenance/project-membership.sql new file mode 100644 index 00000000000..0cf2160bf16 --- /dev/null +++ b/packages/db/maintenance/project-membership.sql @@ -0,0 +1,215 @@ +-- Installed by the registered Project migration after bounded backfill and verification. +COMMIT; +--> statement-breakpoint +-- Validation scans and concurrent index builds have a separate 60-second budget. +SET statement_timeout = '60s'; +--> statement-breakpoint +DO $$ BEGIN + IF to_regclass('public.project_membership_rollout') IS NULL THEN + IF to_regclass('public.project_workspace') IS NOT NULL + OR NOT EXISTS (SELECT 1 FROM pg_attribute WHERE attrelid = 'public.workspace'::regclass + AND attname = 'project_id' AND attnotnull AND NOT attisdropped) + OR (SELECT count(*) FROM pg_constraint + WHERE conrelid = 'public.workspace'::regclass AND contype = 'f' AND convalidated + AND conname IN ('workspace_project_id_project_id_fk', + 'workspace_project_organization_fk', 'workspace_fork_project_fk')) <> 3 THEN + RAISE EXCEPTION 'Project membership rollout marker is missing before completed contraction' USING ERRCODE = '55000'; + END IF; + ELSIF NOT EXISTS (SELECT 1 FROM public.project_membership_rollout WHERE id = 'membership' AND phase = 'column') THEN + RAISE EXCEPTION 'Project membership authority must switch before enforcement' USING ERRCODE = '55000'; + END IF; + IF NOT pg_try_advisory_lock(hashtextextended('sim:project-backfill-operator', 0)) THEN + RAISE EXCEPTION 'Project preparation is still running; finish it before enforcement' USING ERRCODE = '55000'; + END IF; +END $$; +--> statement-breakpoint +CREATE OR REPLACE FUNCTION pg_temp.validate_project_membership() RETURNS void LANGUAGE plpgsql SET search_path = pg_catalog, public, pg_temp AS $$ +BEGIN + IF EXISTS ( + WITH RECURSIVE reachable(id) AS ( + SELECT id FROM workspace WHERE forked_from_workspace_id IS NULL + UNION + SELECT w.id FROM workspace w JOIN reachable r ON w.forked_from_workspace_id = r.id + ) SELECT 1 FROM workspace w LEFT JOIN reachable r ON r.id = w.id WHERE r.id IS NULL + ) THEN + RAISE EXCEPTION 'Project backfill found a fork cycle or missing parent; reconcile before retrying' USING ERRCODE = '55000'; + END IF; + IF EXISTS (SELECT 1 FROM workspace w WHERE w.project_id IS NULL) THEN + RAISE EXCEPTION 'Project preparation is incomplete; rerun the Project script migration before enforcement' USING ERRCODE = '55000'; + END IF; + IF EXISTS ( + SELECT 1 FROM project p LEFT JOIN workspace w ON w.project_id = p.id + GROUP BY p.id HAVING count(w.id) = 0 + OR (p.archived_at IS NULL AND count(w.id) FILTER (WHERE w.archived_at IS NULL) = 0) + OR (p.archived_at IS NOT NULL AND count(w.id) FILTER (WHERE w.archived_at IS NULL) > 0) + ) THEN + RAISE EXCEPTION 'Project enforcement requires nonempty Projects with consistent archive state; reconcile and retry the migration' USING ERRCODE = '55000'; + END IF; + IF EXISTS ( + SELECT 1 FROM workspace w JOIN project p ON p.id = w.project_id + WHERE w.organization_id IS DISTINCT FROM p.organization_id + ) OR EXISTS ( + SELECT 1 FROM workspace w LEFT JOIN workspace parent ON parent.id = w.forked_from_workspace_id + WHERE w.forked_from_workspace_id IS NOT NULL AND parent.project_id IS DISTINCT FROM w.project_id + ) THEN + RAISE EXCEPTION 'Project enforcement requires reconciled organization scope and fork membership' USING ERRCODE = '55000'; + END IF; + IF EXISTS ( + SELECT 1 FROM project p JOIN workspace w ON w.project_id = p.id + JOIN workflow f ON f.workspace_id = w.id + WHERE (p.archived_at IS NOT NULL OR w.archived_at IS NOT NULL) AND f.archived_at IS NULL + ) THEN + RAISE EXCEPTION 'Project enforcement requires archived Projects to have no active workflows' USING ERRCODE = '55000'; + END IF; +END; +$$; +--> statement-breakpoint +SELECT pg_temp.validate_project_membership(); +--> statement-breakpoint +SET lock_timeout = '1s'; +--> statement-breakpoint +SET statement_timeout = '5s'; +--> statement-breakpoint +DO $$ BEGIN + IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conrelid = 'workspace'::regclass + AND conname = 'workspace_project_id_project_id_fk') THEN + ALTER TABLE workspace ADD CONSTRAINT workspace_project_id_project_id_fk + FOREIGN KEY (project_id) REFERENCES project(id) ON DELETE RESTRICT NOT VALID; + END IF; +END $$; +--> statement-breakpoint +DO $$ BEGIN + IF NOT EXISTS (SELECT 1 FROM pg_attribute WHERE attrelid = 'workspace'::regclass + AND attname = 'project_id' AND attnotnull) + AND NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conrelid = 'workspace'::regclass + AND conname = 'workspace_project_id_present') THEN + ALTER TABLE workspace ADD CONSTRAINT workspace_project_id_present CHECK (project_id IS NOT NULL) NOT VALID; + END IF; +END $$; +--> statement-breakpoint +SET statement_timeout = '60s'; +--> statement-breakpoint +ALTER TABLE workspace VALIDATE CONSTRAINT workspace_project_id_project_id_fk; +--> statement-breakpoint +DO $$ BEGIN + IF EXISTS (SELECT 1 FROM pg_constraint WHERE conrelid = 'workspace'::regclass + AND conname = 'workspace_project_id_present') THEN + ALTER TABLE workspace VALIDATE CONSTRAINT workspace_project_id_present; + END IF; +END $$; +--> statement-breakpoint +-- The generated expression always returns non-NULL, including personal scope; writers cannot override it. +-- Stored generated columns rewrite existing rows on PG16/17. Bound the rewrite and lock wait; +-- a timeout rolls this phase back and leaves the connector intact for a later retry. +BEGIN; +SET LOCAL lock_timeout = '1s'; +SET LOCAL statement_timeout = '5s'; +LOCK TABLE project, workspace IN ACCESS EXCLUSIVE MODE NOWAIT; +DO $$ BEGIN + IF to_regclass('project_workspace') IS NOT NULL THEN + LOCK TABLE project_workspace IN ACCESS EXCLUSIVE MODE NOWAIT; + END IF; +END $$; +ALTER TABLE project ADD COLUMN IF NOT EXISTS organization_scope_key text + GENERATED ALWAYS AS (CASE WHEN organization_id IS NULL THEN 'personal' ELSE 'organization:' || organization_id END) STORED; +ALTER TABLE workspace ADD COLUMN IF NOT EXISTS organization_scope_key text + GENERATED ALWAYS AS (CASE WHEN organization_id IS NULL THEN 'personal' ELSE 'organization:' || organization_id END) STORED; +COMMIT; +--> statement-breakpoint +SET statement_timeout = '5s'; +--> statement-breakpoint +-- Only invalid, unreferenced indexes from interrupted builds are removed. Valid indexes survive +-- replay, including when the final foreign keys already depend on them. +DO $$ DECLARE target_name text; BEGIN + FOR target_name IN SELECT c.relname FROM pg_index i JOIN pg_class c ON c.oid = i.indexrelid + WHERE c.relnamespace = 'public'::regnamespace AND NOT i.indisvalid + AND c.relname IN ('project_id_organization_scope_unique', 'workspace_id_project_unique') + LOOP + EXECUTE format('DROP INDEX %I', target_name); + END LOOP; +END $$; +--> statement-breakpoint +SET lock_timeout = 0; +SET statement_timeout = '60s'; +--> statement-breakpoint +CREATE UNIQUE INDEX CONCURRENTLY IF NOT EXISTS project_id_organization_scope_unique ON project(id, organization_scope_key); +--> statement-breakpoint +CREATE UNIQUE INDEX CONCURRENTLY IF NOT EXISTS workspace_id_project_unique ON workspace(id, project_id); +--> statement-breakpoint +SET lock_timeout = '1s'; +SET statement_timeout = '5s'; +--> statement-breakpoint +DO $$ BEGIN + IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conrelid = 'project'::regclass + AND conname = 'project_id_organization_scope_unique') THEN + ALTER TABLE project ADD CONSTRAINT project_id_organization_scope_unique + UNIQUE USING INDEX project_id_organization_scope_unique; + END IF; + IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conrelid = 'workspace'::regclass + AND conname = 'workspace_id_project_unique') THEN + ALTER TABLE workspace ADD CONSTRAINT workspace_id_project_unique + UNIQUE USING INDEX workspace_id_project_unique; + END IF; + IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conrelid = 'workspace'::regclass + AND conname = 'workspace_project_organization_fk') THEN + ALTER TABLE workspace ADD CONSTRAINT workspace_project_organization_fk + FOREIGN KEY (project_id, organization_scope_key) REFERENCES project(id, organization_scope_key) + DEFERRABLE INITIALLY DEFERRED NOT VALID; + END IF; + IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conrelid = 'workspace'::regclass + AND conname = 'workspace_fork_project_fk') THEN + ALTER TABLE workspace ADD CONSTRAINT workspace_fork_project_fk + FOREIGN KEY (forked_from_workspace_id, project_id) REFERENCES workspace(id, project_id) + DEFERRABLE INITIALLY DEFERRED NOT VALID; + END IF; +END $$; +--> statement-breakpoint +-- Drizzle represents the FK columns/actions; the shared finalizer owns deferred timing on push. +ALTER TABLE workspace ALTER CONSTRAINT workspace_project_organization_fk DEFERRABLE INITIALLY DEFERRED; +ALTER TABLE workspace ALTER CONSTRAINT workspace_fork_project_fk DEFERRABLE INITIALLY DEFERRED; +--> statement-breakpoint +SET statement_timeout = '60s'; +--> statement-breakpoint +ALTER TABLE workspace VALIDATE CONSTRAINT workspace_project_organization_fk; +--> statement-breakpoint +ALTER TABLE workspace VALIDATE CONSTRAINT workspace_fork_project_fk; +--> statement-breakpoint +BEGIN; +--> statement-breakpoint +-- PG17 caps the transaction; PG16 caps idle time, with each statement bounded below. +SELECT set_config(CASE WHEN current_setting('transaction_timeout', true) IS NULL + THEN 'idle_in_transaction_session_timeout' ELSE 'transaction_timeout' END, '5s', true); +--> statement-breakpoint +SET LOCAL lock_timeout = '1s'; +--> statement-breakpoint +SET LOCAL statement_timeout = '5s'; +--> statement-breakpoint +-- Keep enforcement and bridge retirement atomic; data scans run outside this transaction. +LOCK TABLE workspace, project IN ACCESS EXCLUSIVE MODE NOWAIT; +--> statement-breakpoint +DO $$ BEGIN + IF to_regclass('project_workspace') IS NOT NULL THEN + LOCK TABLE project_workspace IN ACCESS EXCLUSIVE MODE NOWAIT; + END IF; +END $$; +--> statement-breakpoint +-- migration-safe: contract of #8830, gated on its authority-aware release being fully deployed, incompatible app/worker versions having drained, and the column-authority switch being committed; the validated check proves existing rows. +ALTER TABLE workspace ALTER COLUMN project_id SET NOT NULL; +-- migration-safe: the required column now enforces the validated helper check's invariant. +ALTER TABLE workspace DROP CONSTRAINT IF EXISTS workspace_project_id_present; +--> statement-breakpoint +-- migration-safe: contract of #8830, gated on its authority-aware release being fully deployed, incompatible app/worker versions having drained, and the column-authority switch being committed; no supported application reader or writer then needs this connector. +DROP TABLE IF EXISTS project_workspace; +-- migration-safe: contract of #8830, whose deployed readers recognize the required column and validated foreign keys after both temporary tables disappear atomically under the workspace barrier. +DROP TABLE IF EXISTS project_membership_rollout; +--> statement-breakpoint +COMMIT; +--> statement-breakpoint +-- Composite foreign keys protect structural writes; compatible application transactions own lifecycle. +SELECT pg_temp.validate_project_membership(); +--> statement-breakpoint +DROP FUNCTION pg_temp.validate_project_membership(); +--> statement-breakpoint +SET statement_timeout = 0; +--> statement-breakpoint +SET lock_timeout = '5s'; diff --git a/packages/db/maintenance/project-repairs.ts b/packages/db/maintenance/project-repairs.ts new file mode 100644 index 00000000000..80262788134 --- /dev/null +++ b/packages/db/maintenance/project-repairs.ts @@ -0,0 +1,32 @@ +import type { ProjectArchiveRepair } from '@sim/db/maintenance/project-backfill' +import type { Sql } from 'postgres' + +/** Runner-managed recovery state, like script_migrations; never an application ownership table. */ +export async function ensureProjectArchiveRepairJournal(sql: Sql): Promise { + await sql`CREATE TABLE IF NOT EXISTS public.project_backfill_archive_repairs ( + workspace_id text PRIMARY KEY, + repair jsonb NOT NULL, + completed_at timestamptz + )` +} + +/** Written only after all external cleanup succeeds, so deployment cannot overlook a partial repair. */ +export async function completeProjectArchiveRepair( + sql: Sql, + repair: ProjectArchiveRepair +): Promise { + const rows = await sql`UPDATE public.project_backfill_archive_repairs SET completed_at = now() + WHERE workspace_id = ${repair.workspaceId} AND repair = ${JSON.stringify(repair)}::text::jsonb + RETURNING workspace_id` + if (!rows.length) throw new Error('Archive repair changed before cleanup completed') +} + +/** Read-only, including on databases that have never needed archive repair. */ +export async function countPendingProjectArchiveRepairs(sql: Sql): Promise { + const [state] = + await sql`SELECT to_regclass('public.project_backfill_archive_repairs') IS NOT NULL AS present` + if (!state.present) return 0 + const [row] = + await sql`SELECT count(*)::int AS count FROM public.project_backfill_archive_repairs WHERE completed_at IS NULL` + return row.count +} diff --git a/packages/db/maintenance/project-rollout.ts b/packages/db/maintenance/project-rollout.ts new file mode 100644 index 00000000000..f76a2be7fa4 --- /dev/null +++ b/packages/db/maintenance/project-rollout.ts @@ -0,0 +1,76 @@ +import { getTransientDatabaseFailure } from '@sim/utils/errors' +import { sleep } from '@sim/utils/helpers' +import { backoffWithJitter } from '@sim/utils/retry' +import type { Sql, TransactionSql } from 'postgres' + +type ProjectMembershipPhase = 'connector' | 'column' + +/** Reads durable authority without inferring it from partially copied membership. */ +export async function readProjectMembershipPhase( + sql: Sql | TransactionSql +): Promise { + const [table] = await sql`SELECT + to_regclass('public.project_membership_rollout') IS NOT NULL AS present, + to_regclass('public.project_workspace') IS NULL + AND EXISTS (SELECT 1 FROM pg_attribute WHERE attrelid = to_regclass('public.workspace') + AND attname = 'project_id' AND attnotnull AND NOT attisdropped) + AND (SELECT count(*) = 3 FROM pg_constraint + WHERE conrelid = to_regclass('public.workspace') AND contype = 'f' AND convalidated + AND conname IN ('workspace_project_id_project_id_fk', + 'workspace_project_organization_fk', 'workspace_fork_project_fk')) AS complete` + if (!table?.present) { + if (table?.complete) return 'column' + throw new Error('Project membership rollout marker is missing before completed contraction') + } + const rows = await sql< + { id: string; phase: string }[] + >`SELECT id, phase FROM public.project_membership_rollout` + const row = rows[0] + if (rows.length !== 1 || row?.id !== 'membership' || !['connector', 'column'].includes(row.phase)) + throw new Error('Project membership rollout marker is missing or invalid') + return row.phase as ProjectMembershipPhase +} + +/** Mutating maintenance never writes columns while compatible applications still use the connector. */ +export async function assertProjectColumnAuthority(sql: Sql | TransactionSql): Promise { + if ((await readProjectMembershipPhase(sql)) !== 'column') + throw new Error( + 'Project membership authority must switch through db:migrate after verified server/worker drainage before maintenance writes' + ) +} + +/** Commits the forward-only authority change before any discovery, backfill, or repair. */ +export async function switchProjectMembershipAuthority(sql: Sql): Promise { + for (let attempt = 0; ; attempt++) { + try { + await sql.begin(async (tx) => { + await tx`SET LOCAL statement_timeout = '3s'` + await tx`SELECT set_config(CASE WHEN current_setting('transaction_timeout', true) IS NULL + THEN 'idle_in_transaction_session_timeout' ELSE 'transaction_timeout' END, '5s', true)` + await tx`LOCK TABLE public.workspace IN ACCESS EXCLUSIVE MODE NOWAIT` + const [operator] = await tx`SELECT EXISTS (SELECT 1 FROM pg_locks + WHERE pid = pg_backend_pid() AND locktype = 'advisory' AND granted + AND classid = ((hashtextextended('sim:project-backfill-operator',0) >> 32) & 4294967295)::oid + AND objid = (hashtextextended('sim:project-backfill-operator',0) & 4294967295)::oid + AND objsubid = 1) AS held` + if (!operator?.held) + throw new Error('Project authority switch requires the operator session lock') + const phase = await readProjectMembershipPhase(tx) + if (phase === 'column') return + const [state] = await tx`SELECT + to_regclass('public.project_workspace') IS NOT NULL AS connector, + EXISTS (SELECT 1 FROM public.workspace WHERE project_id IS NOT NULL) AS populated` + if (!state?.connector || state.populated) + throw new Error( + 'Connector authority requires an intact connector and no populated Project columns; reconcile the rollout state before switching' + ) + await tx`UPDATE public.project_membership_rollout SET phase = 'column' WHERE id = 'membership'` + }) + return + } catch (error) { + const transient = getTransientDatabaseFailure(error) + if (attempt >= 2 || (transient !== 'capacity' && transient !== 'conflict')) throw error + await sleep(backoffWithJitter(attempt, null, { baseMs: 100, maxMs: 500 })) + } + } +} diff --git a/packages/db/maintenance/search-retirement.integration.ts b/packages/db/maintenance/search-retirement.integration.ts index 0ab85b505ef..6cc6bdba693 100644 --- a/packages/db/maintenance/search-retirement.integration.ts +++ b/packages/db/maintenance/search-retirement.integration.ts @@ -103,8 +103,11 @@ describe('operator-driven Search retirement in PostgreSQL', () => { writer = postgres(fixtureUrl.toString(), { ...options, prepare: true }) await sql`INSERT INTO "user" (id, name, email, email_verified, created_at, updated_at) VALUES ('reader', 'Synthetic reader', 'reader@example.test', true, now(), now())` - await sql`INSERT INTO workspace (id, name, owner_id, billed_account_user_id) - VALUES ('workspace', 'Synthetic workspace', 'reader', 'reader')` + await sql.begin(async (tx) => { + await tx`INSERT INTO project (id, name, owner_id) VALUES ('project', 'Synthetic project', 'reader')` + await tx`INSERT INTO workspace (id, project_id, name, owner_id, billed_account_user_id) + VALUES ('workspace', 'project', 'Synthetic workspace', 'reader', 'reader')` + }) await sql`CREATE TABLE search_embedding_cleanup_progress ( id integer PRIMARY KEY, knowledge_base_id text, phase text, after_id text)` await sql`INSERT INTO search_embedding_cleanup_progress VALUES (1, 'search', 'embeddings', '')` diff --git a/packages/db/migrations/0407_project_membership_enforcement.sql b/packages/db/migrations/0407_project_membership_enforcement.sql new file mode 100644 index 00000000000..3ab644c49a4 --- /dev/null +++ b/packages/db/migrations/0407_project_membership_enforcement.sql @@ -0,0 +1,3 @@ +-- The registered 0031_project_membership script commits the column-authority switch, then backfills and verifies before installing +-- the final constraints and retiring the connector. SQL migrations run before scripts. +SELECT 1; diff --git a/packages/db/migrations/meta/0407_snapshot.json b/packages/db/migrations/meta/0407_snapshot.json new file mode 100644 index 00000000000..4470b960990 --- /dev/null +++ b/packages/db/migrations/meta/0407_snapshot.json @@ -0,0 +1,30898 @@ +{ + "id": "7575a355-a608-4f08-b3bb-7f22ef176275", + "prevId": "155ddb55-55f3-4a4b-8681-9768740ef57a", + "version": "7", + "dialect": "postgresql", + "tables": { + "public.academy_certificate": { + "name": "academy_certificate", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "course_id": { + "name": "course_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "academy_cert_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'active'" + }, + "issued_at": { + "name": "issued_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "certificate_number": { + "name": "certificate_number", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "academy_certificate_user_id_idx": { + "name": "academy_certificate_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "academy_certificate_course_id_idx": { + "name": "academy_certificate_course_id_idx", + "columns": [ + { + "expression": "course_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "academy_certificate_user_course_unique": { + "name": "academy_certificate_user_course_unique", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "course_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "academy_certificate_status_idx": { + "name": "academy_certificate_status_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "academy_certificate_user_id_user_id_fk": { + "name": "academy_certificate_user_id_user_id_fk", + "tableFrom": "academy_certificate", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "academy_certificate_certificate_number_unique": { + "name": "academy_certificate_certificate_number_unique", + "nullsNotDistinct": false, + "columns": ["certificate_number"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.account": { + "name": "account", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "account_id": { + "name": "account_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider_id": { + "name": "provider_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "access_token": { + "name": "access_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "refresh_token": { + "name": "refresh_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "id_token": { + "name": "id_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "access_token_expires_at": { + "name": "access_token_expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "refresh_token_expires_at": { + "name": "refresh_token_expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "scope": { + "name": "scope", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "oauth_config": { + "name": "oauth_config", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "refresh_revoked_at": { + "name": "refresh_revoked_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "refresh_revoked_code": { + "name": "refresh_revoked_code", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "refresh_revoked_token_hash": { + "name": "refresh_revoked_token_hash", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "account_user_id_idx": { + "name": "account_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_account_on_account_id_provider_id": { + "name": "idx_account_on_account_id_provider_id", + "columns": [ + { + "expression": "account_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "provider_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "account_user_id_user_id_fk": { + "name": "account_user_id_user_id_fk", + "tableFrom": "account", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.agent_memory_turn": { + "name": "agent_memory_turn", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "memory_id": { + "name": "memory_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "execution_id": { + "name": "execution_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "block_id": { + "name": "block_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "node_id": { + "name": "node_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "execution_order": { + "name": "execution_order", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "encrypted_state": { + "name": "encrypted_state", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "revision": { + "name": "revision", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "agent_memory_turn_invocation_unique": { + "name": "agent_memory_turn_invocation_unique", + "columns": [ + { + "expression": "memory_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "execution_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "block_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "node_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "execution_order", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "agent_memory_turn_workflow_idx": { + "name": "agent_memory_turn_workflow_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "agent_memory_turn_memory_id_memory_id_fk": { + "name": "agent_memory_turn_memory_id_memory_id_fk", + "tableFrom": "agent_memory_turn", + "tableTo": "memory", + "columnsFrom": ["memory_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "agent_memory_turn_workflow_id_workflow_id_fk": { + "name": "agent_memory_turn_workflow_id_workflow_id_fk", + "tableFrom": "agent_memory_turn", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.api_key": { + "name": "api_key", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "key_hash": { + "name": "key_hash", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'personal'" + }, + "last_used": { + "name": "last_used", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "api_key_workspace_type_idx": { + "name": "api_key_workspace_type_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "api_key_user_type_idx": { + "name": "api_key_user_type_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "api_key_key_hash_idx": { + "name": "api_key_key_hash_idx", + "columns": [ + { + "expression": "key_hash", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "api_key_user_id_user_id_fk": { + "name": "api_key_user_id_user_id_fk", + "tableFrom": "api_key", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "api_key_workspace_id_workspace_id_fk": { + "name": "api_key_workspace_id_workspace_id_fk", + "tableFrom": "api_key", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "api_key_created_by_user_id_fk": { + "name": "api_key_created_by_user_id_fk", + "tableFrom": "api_key", + "tableTo": "user", + "columnsFrom": ["created_by"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "api_key_key_unique": { + "name": "api_key_key_unique", + "nullsNotDistinct": false, + "columns": ["key"] + } + }, + "policies": {}, + "checkConstraints": { + "workspace_type_check": { + "name": "workspace_type_check", + "value": "(type = 'workspace' AND workspace_id IS NOT NULL) OR (type = 'personal' AND workspace_id IS NULL)" + } + }, + "isRLSEnabled": false + }, + "public.async_jobs": { + "name": "async_jobs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "payload": { + "name": "payload", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "started_at": { + "name": "started_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "run_at": { + "name": "run_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "attempts": { + "name": "attempts", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "max_attempts": { + "name": "max_attempts", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 3 + }, + "error": { + "name": "error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "output": { + "name": "output", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "async_jobs_status_started_at_idx": { + "name": "async_jobs_status_started_at_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "started_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "async_jobs_status_completed_at_idx": { + "name": "async_jobs_status_completed_at_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "completed_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "async_jobs_schedule_pending_run_at_idx": { + "name": "async_jobs_schedule_pending_run_at_idx", + "columns": [ + { + "expression": "run_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"async_jobs\".\"type\" = 'schedule-execution' AND \"async_jobs\".\"status\" = 'pending'", + "concurrently": false, + "method": "btree", + "with": {} + }, + "async_jobs_schedule_processing_started_at_idx": { + "name": "async_jobs_schedule_processing_started_at_idx", + "columns": [ + { + "expression": "started_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"async_jobs\".\"type\" = 'schedule-execution' AND \"async_jobs\".\"status\" = 'processing'", + "concurrently": false, + "method": "btree", + "with": {} + }, + "async_jobs_schedule_unreconciled_terminal_idx": { + "name": "async_jobs_schedule_unreconciled_terminal_idx", + "columns": [ + { + "expression": "updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"async_jobs\".\"type\" = 'schedule-execution' AND \"async_jobs\".\"status\" IN ('completed', 'failed', 'cancelled') AND COALESCE(\"async_jobs\".\"metadata\" ->> 'scheduleReconciled', 'false') <> 'true'", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.audit_log": { + "name": "audit_log", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "actor_id": { + "name": "actor_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "action": { + "name": "action", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "resource_type": { + "name": "resource_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "resource_id": { + "name": "resource_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "actor_name": { + "name": "actor_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "actor_email": { + "name": "actor_email", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "resource_name": { + "name": "resource_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false, + "default": "'{}'" + }, + "ip_address": { + "name": "ip_address", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_agent": { + "name": "user_agent", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "surface": { + "name": "surface", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "audit_log_workspace_created_idx": { + "name": "audit_log_workspace_created_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "audit_log_workspace_created_at_id_idx": { + "name": "audit_log_workspace_created_at_id_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "date_trunc('milliseconds', \"created_at\")", + "asc": true, + "isExpression": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "audit_log_actor_created_idx": { + "name": "audit_log_actor_created_idx", + "columns": [ + { + "expression": "actor_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "audit_log_resource_idx": { + "name": "audit_log_resource_idx", + "columns": [ + { + "expression": "resource_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "resource_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "audit_log_action_idx": { + "name": "audit_log_action_idx", + "columns": [ + { + "expression": "action", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "audit_log_workspace_id_workspace_id_fk": { + "name": "audit_log_workspace_id_workspace_id_fk", + "tableFrom": "audit_log", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + }, + "audit_log_actor_id_user_id_fk": { + "name": "audit_log_actor_id_user_id_fk", + "tableFrom": "audit_log", + "tableTo": "user", + "columnsFrom": ["actor_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.background_work_status": { + "name": "background_work_status", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "kind": { + "name": "kind", + "type": "background_work_kind", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "background_work_status_value", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "message": { + "name": "message", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "error": { + "name": "error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "started_at": { + "name": "started_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "background_work_status_workspace_status_idx": { + "name": "background_work_status_workspace_status_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "background_work_status_workflow_status_idx": { + "name": "background_work_status_workflow_status_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "background_work_status_meta_child_ws_idx": { + "name": "background_work_status_meta_child_ws_idx", + "columns": [ + { + "expression": "(\"metadata\" ->> 'childWorkspaceId')", + "asc": true, + "isExpression": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "background_work_status_meta_other_ws_idx": { + "name": "background_work_status_meta_other_ws_idx", + "columns": [ + { + "expression": "(\"metadata\" ->> 'otherWorkspaceId')", + "asc": true, + "isExpression": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "background_work_status_workspace_id_workspace_id_fk": { + "name": "background_work_status_workspace_id_workspace_id_fk", + "tableFrom": "background_work_status", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "background_work_status_workflow_id_workflow_id_fk": { + "name": "background_work_status_workflow_id_workflow_id_fk", + "tableFrom": "background_work_status", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.changelog_change": { + "name": "changelog_change", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "release_id": { + "name": "release_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "position": { + "name": "position", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "text": { + "name": "text", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "deployment_version_id": { + "name": "deployment_version_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "chat_id": { + "name": "chat_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "changelog_change_release_position_unique": { + "name": "changelog_change_release_position_unique", + "columns": [ + { + "expression": "release_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "position", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "changelog_change_release_id_changelog_release_id_fk": { + "name": "changelog_change_release_id_changelog_release_id_fk", + "tableFrom": "changelog_change", + "tableTo": "changelog_release", + "columnsFrom": ["release_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "changelog_change_workflow_id_workflow_id_fk": { + "name": "changelog_change_workflow_id_workflow_id_fk", + "tableFrom": "changelog_change", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + }, + "changelog_change_deployment_version_id_workflow_deployment_version_id_fk": { + "name": "changelog_change_deployment_version_id_workflow_deployment_version_id_fk", + "tableFrom": "changelog_change", + "tableTo": "workflow_deployment_version", + "columnsFrom": ["deployment_version_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + }, + "changelog_change_chat_id_copilot_chats_id_fk": { + "name": "changelog_change_chat_id_copilot_chats_id_fk", + "tableFrom": "changelog_change", + "tableTo": "copilot_chats", + "columnsFrom": ["chat_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.changelog_release": { + "name": "changelog_release", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "version_major": { + "name": "version_major", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "version_minor": { + "name": "version_minor", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "version_patch": { + "name": "version_patch", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "bump_reason": { + "name": "bump_reason", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "body_file_id": { + "name": "body_file_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "revision": { + "name": "revision", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "published_at": { + "name": "published_at", + "type": "timestamp (3)", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "updated_by": { + "name": "updated_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "changelog_release_workspace_version_unique": { + "name": "changelog_release_workspace_version_unique", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "version_major", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "version_minor", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "version_patch", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "changelog_release_body_file_unique": { + "name": "changelog_release_body_file_unique", + "columns": [ + { + "expression": "body_file_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "changelog_release_workspace_published_idx": { + "name": "changelog_release_workspace_published_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "published_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "changelog_release_workspace_id_workspace_id_fk": { + "name": "changelog_release_workspace_id_workspace_id_fk", + "tableFrom": "changelog_release", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "changelog_release_body_file_id_workspace_files_id_fk": { + "name": "changelog_release_body_file_id_workspace_files_id_fk", + "tableFrom": "changelog_release", + "tableTo": "workspace_files", + "columnsFrom": ["body_file_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + }, + "changelog_release_created_by_user_id_fk": { + "name": "changelog_release_created_by_user_id_fk", + "tableFrom": "changelog_release", + "tableTo": "user", + "columnsFrom": ["created_by"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + }, + "changelog_release_updated_by_user_id_fk": { + "name": "changelog_release_updated_by_user_id_fk", + "tableFrom": "changelog_release", + "tableTo": "user", + "columnsFrom": ["updated_by"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "changelog_release_version_check": { + "name": "changelog_release_version_check", + "value": "\"changelog_release\".\"version_major\" >= 0 AND \"changelog_release\".\"version_minor\" >= 0 AND \"changelog_release\".\"version_patch\" >= 0" + } + }, + "isRLSEnabled": false + }, + "public.chat": { + "name": "chat", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "identifier": { + "name": "identifier", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "is_active": { + "name": "is_active", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "customizations": { + "name": "customizations", + "type": "json", + "primaryKey": false, + "notNull": false, + "default": "'{}'" + }, + "auth_type": { + "name": "auth_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'public'" + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "allowed_emails": { + "name": "allowed_emails", + "type": "json", + "primaryKey": false, + "notNull": false, + "default": "'[]'" + }, + "output_configs": { + "name": "output_configs", + "type": "json", + "primaryKey": false, + "notNull": false, + "default": "'[]'" + }, + "include_thinking": { + "name": "include_thinking", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "include_tool_calls": { + "name": "include_tool_calls", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "archived_at": { + "name": "archived_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "identifier_idx": { + "name": "identifier_idx", + "columns": [ + { + "expression": "identifier", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"chat\".\"archived_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "chat_archived_at_partial_idx": { + "name": "chat_archived_at_partial_idx", + "columns": [ + { + "expression": "archived_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"chat\".\"archived_at\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_chat_on_workflow_id_archived_at": { + "name": "idx_chat_on_workflow_id_archived_at", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "archived_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "chat_workflow_id_workflow_id_fk": { + "name": "chat_workflow_id_workflow_id_fk", + "tableFrom": "chat", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "chat_user_id_user_id_fk": { + "name": "chat_user_id_user_id_fk", + "tableFrom": "chat", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.client_credential_token": { + "name": "client_credential_token", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "encrypted_value": { + "name": "encrypted_value", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "access_token_digest": { + "name": "access_token_digest", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "client_credential_token_expires_at_idx": { + "name": "client_credential_token_expires_at_idx", + "columns": [ + { + "expression": "expires_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"client_credential_token\".\"access_token_digest\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "client_credential_token_access_token_digest_idx": { + "name": "client_credential_token_access_token_digest_idx", + "columns": [ + { + "expression": "access_token_digest", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"client_credential_token\".\"access_token_digest\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.copilot_async_tool_calls": { + "name": "copilot_async_tool_calls", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "run_id": { + "name": "run_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "checkpoint_id": { + "name": "checkpoint_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "tool_call_id": { + "name": "tool_call_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "tool_name": { + "name": "tool_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "args": { + "name": "args", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "status": { + "name": "status", + "type": "copilot_async_tool_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "result": { + "name": "result", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "error": { + "name": "error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "permission_decision": { + "name": "permission_decision", + "type": "copilot_tool_permission_decision", + "typeSchema": "public", + "primaryKey": false, + "notNull": false + }, + "permission_decided_at": { + "name": "permission_decided_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "permission_requested_at": { + "name": "permission_requested_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "pickup_deadline_at": { + "name": "pickup_deadline_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "persist_seq": { + "name": "persist_seq", + "type": "bigint", + "primaryKey": false, + "notNull": false, + "default": "nextval('copilot_async_tool_calls_persist_seq')" + }, + "claimed_at": { + "name": "claimed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "claimed_by": { + "name": "claimed_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "browser_download_started_at": { + "name": "browser_download_started_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "execution_started_at": { + "name": "execution_started_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "execution_settled_at": { + "name": "execution_settled_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "execution_owner_token": { + "name": "execution_owner_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "execution_lease_expires_at": { + "name": "execution_lease_expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "execution_revoked_at": { + "name": "execution_revoked_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "client_workflow_execution_id": { + "name": "client_workflow_execution_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "sandbox_processes": { + "name": "sandbox_processes", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "copilot_async_tool_calls_run_id_idx": { + "name": "copilot_async_tool_calls_run_id_idx", + "columns": [ + { + "expression": "run_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_async_tool_calls_checkpoint_id_idx": { + "name": "copilot_async_tool_calls_checkpoint_id_idx", + "columns": [ + { + "expression": "checkpoint_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_async_tool_calls_status_idx": { + "name": "copilot_async_tool_calls_status_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_async_tool_calls_run_status_idx": { + "name": "copilot_async_tool_calls_run_status_idx", + "columns": [ + { + "expression": "run_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_async_tool_calls_tool_call_id_unique": { + "name": "copilot_async_tool_calls_tool_call_id_unique", + "columns": [ + { + "expression": "tool_call_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "copilot_async_tool_calls_run_id_copilot_runs_id_fk": { + "name": "copilot_async_tool_calls_run_id_copilot_runs_id_fk", + "tableFrom": "copilot_async_tool_calls", + "tableTo": "copilot_runs", + "columnsFrom": ["run_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "copilot_async_tool_calls_checkpoint_id_copilot_run_checkpoints_id_fk": { + "name": "copilot_async_tool_calls_checkpoint_id_copilot_run_checkpoints_id_fk", + "tableFrom": "copilot_async_tool_calls", + "tableTo": "copilot_run_checkpoints", + "columnsFrom": ["checkpoint_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.copilot_chats": { + "name": "copilot_chats", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "type": { + "name": "type", + "type": "chat_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'copilot'" + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "model": { + "name": "model", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'claude-3-7-sonnet-latest'" + }, + "conversation_id": { + "name": "conversation_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "external_conversation_key": { + "name": "external_conversation_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "external_conversation_metadata": { + "name": "external_conversation_metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "preview_yaml": { + "name": "preview_yaml", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "plan_artifact": { + "name": "plan_artifact", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "config": { + "name": "config", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "resources": { + "name": "resources", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'" + }, + "auto_allowed_tools": { + "name": "auto_allowed_tools", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'" + }, + "last_seen_at": { + "name": "last_seen_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "pinned": { + "name": "pinned", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "copilot_chats_organization_id_idx": { + "name": "copilot_chats_organization_id_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_chats_external_conversation_unique": { + "name": "copilot_chats_external_conversation_unique", + "columns": [ + { + "expression": "external_conversation_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"copilot_chats\".\"external_conversation_key\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_chats_user_org_created_idx": { + "name": "copilot_chats_user_org_created_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_chats_user_id_idx": { + "name": "copilot_chats_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_chats_workflow_id_idx": { + "name": "copilot_chats_workflow_id_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_chats_user_workflow_idx": { + "name": "copilot_chats_user_workflow_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_chats_user_workspace_idx": { + "name": "copilot_chats_user_workspace_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_chats_created_at_idx": { + "name": "copilot_chats_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_chats_updated_at_idx": { + "name": "copilot_chats_updated_at_idx", + "columns": [ + { + "expression": "updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_chats_workspace_created_at_id_idx": { + "name": "copilot_chats_workspace_created_at_id_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "date_trunc('milliseconds', \"created_at\")", + "asc": true, + "isExpression": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_chats_user_workspace_deleted_partial_idx": { + "name": "copilot_chats_user_workspace_deleted_partial_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"copilot_chats\".\"deleted_at\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "copilot_chats_user_id_user_id_fk": { + "name": "copilot_chats_user_id_user_id_fk", + "tableFrom": "copilot_chats", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "copilot_chats_workflow_id_workflow_id_fk": { + "name": "copilot_chats_workflow_id_workflow_id_fk", + "tableFrom": "copilot_chats", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "copilot_chats_workspace_id_workspace_id_fk": { + "name": "copilot_chats_workspace_id_workspace_id_fk", + "tableFrom": "copilot_chats", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "copilot_chats_organization_id_organization_id_fk": { + "name": "copilot_chats_organization_id_organization_id_fk", + "tableFrom": "copilot_chats", + "tableTo": "organization", + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "copilot_chats_owner_check": { + "name": "copilot_chats_owner_check", + "value": "num_nonnulls(\"copilot_chats\".\"workspace_id\", \"copilot_chats\".\"organization_id\") <= 1" + }, + "copilot_chats_organization_workflow_check": { + "name": "copilot_chats_organization_workflow_check", + "value": "\"copilot_chats\".\"organization_id\" IS NULL OR \"copilot_chats\".\"workflow_id\" IS NULL" + } + }, + "isRLSEnabled": false + }, + "public.copilot_feedback": { + "name": "copilot_feedback", + "schema": "", + "columns": { + "feedback_id": { + "name": "feedback_id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "chat_id": { + "name": "chat_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "user_query": { + "name": "user_query", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "agent_response": { + "name": "agent_response", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "is_positive": { + "name": "is_positive", + "type": "boolean", + "primaryKey": false, + "notNull": true + }, + "feedback": { + "name": "feedback", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "workflow_yaml": { + "name": "workflow_yaml", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "copilot_feedback_user_id_idx": { + "name": "copilot_feedback_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_feedback_chat_id_idx": { + "name": "copilot_feedback_chat_id_idx", + "columns": [ + { + "expression": "chat_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_feedback_user_chat_idx": { + "name": "copilot_feedback_user_chat_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "chat_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_feedback_is_positive_idx": { + "name": "copilot_feedback_is_positive_idx", + "columns": [ + { + "expression": "is_positive", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_feedback_created_at_idx": { + "name": "copilot_feedback_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "copilot_feedback_user_id_user_id_fk": { + "name": "copilot_feedback_user_id_user_id_fk", + "tableFrom": "copilot_feedback", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "copilot_feedback_chat_id_copilot_chats_id_fk": { + "name": "copilot_feedback_chat_id_copilot_chats_id_fk", + "tableFrom": "copilot_feedback", + "tableTo": "copilot_chats", + "columnsFrom": ["chat_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.copilot_messages": { + "name": "copilot_messages", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "chat_id": { + "name": "chat_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "message_id": { + "name": "message_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "content": { + "name": "content", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "stream_id": { + "name": "stream_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "parent_message_id": { + "name": "parent_message_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "model": { + "name": "model", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tokens_in": { + "name": "tokens_in", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "tokens_out": { + "name": "tokens_out", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "seq": { + "name": "seq", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "copilot_messages_chat_message_unique": { + "name": "copilot_messages_chat_message_unique", + "columns": [ + { + "expression": "chat_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "message_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_messages_chat_created_at_idx": { + "name": "copilot_messages_chat_created_at_idx", + "columns": [ + { + "expression": "chat_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"copilot_messages\".\"deleted_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_messages_chat_seq_idx": { + "name": "copilot_messages_chat_seq_idx", + "columns": [ + { + "expression": "chat_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "seq", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"copilot_messages\".\"deleted_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_messages_chat_stream_idx": { + "name": "copilot_messages_chat_stream_idx", + "columns": [ + { + "expression": "chat_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "stream_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"copilot_messages\".\"stream_id\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_messages_user_created_at_idx": { + "name": "copilot_messages_user_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "chat_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "message_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"copilot_messages\".\"role\" = 'user' AND \"copilot_messages\".\"deleted_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "copilot_messages_chat_id_copilot_chats_id_fk": { + "name": "copilot_messages_chat_id_copilot_chats_id_fk", + "tableFrom": "copilot_messages", + "tableTo": "copilot_chats", + "columnsFrom": ["chat_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.copilot_organization_request_stops": { + "name": "copilot_organization_request_stops", + "schema": "", + "columns": { + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "stream_id": { + "name": "stream_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "stopped_at": { + "name": "stopped_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "copilot_organization_request_stops_user_id_user_id_fk": { + "name": "copilot_organization_request_stops_user_id_user_id_fk", + "tableFrom": "copilot_organization_request_stops", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "copilot_organization_request_stops_organization_id_organization_id_fk": { + "name": "copilot_organization_request_stops_organization_id_organization_id_fk", + "tableFrom": "copilot_organization_request_stops", + "tableTo": "organization", + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "copilot_organization_request_stops_user_id_organization_id_stream_id_pk": { + "name": "copilot_organization_request_stops_user_id_organization_id_stream_id_pk", + "columns": ["user_id", "organization_id", "stream_id"] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.copilot_request_stops": { + "name": "copilot_request_stops", + "schema": "", + "columns": { + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "stream_id": { + "name": "stream_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "stopped_at": { + "name": "stopped_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "copilot_request_stops_user_id_user_id_fk": { + "name": "copilot_request_stops_user_id_user_id_fk", + "tableFrom": "copilot_request_stops", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "copilot_request_stops_workspace_id_workspace_id_fk": { + "name": "copilot_request_stops_workspace_id_workspace_id_fk", + "tableFrom": "copilot_request_stops", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "copilot_request_stops_user_id_workspace_id_stream_id_pk": { + "name": "copilot_request_stops_user_id_workspace_id_stream_id_pk", + "columns": ["user_id", "workspace_id", "stream_id"] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.copilot_run_checkpoints": { + "name": "copilot_run_checkpoints", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "run_id": { + "name": "run_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "pending_tool_call_id": { + "name": "pending_tool_call_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "conversation_snapshot": { + "name": "conversation_snapshot", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "agent_state": { + "name": "agent_state", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "provider_request": { + "name": "provider_request", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "copilot_run_checkpoints_run_id_idx": { + "name": "copilot_run_checkpoints_run_id_idx", + "columns": [ + { + "expression": "run_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_run_checkpoints_pending_tool_call_id_idx": { + "name": "copilot_run_checkpoints_pending_tool_call_id_idx", + "columns": [ + { + "expression": "pending_tool_call_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_run_checkpoints_run_pending_tool_unique": { + "name": "copilot_run_checkpoints_run_pending_tool_unique", + "columns": [ + { + "expression": "run_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "pending_tool_call_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "copilot_run_checkpoints_run_id_copilot_runs_id_fk": { + "name": "copilot_run_checkpoints_run_id_copilot_runs_id_fk", + "tableFrom": "copilot_run_checkpoints", + "tableTo": "copilot_runs", + "columnsFrom": ["run_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.copilot_runs": { + "name": "copilot_runs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "execution_id": { + "name": "execution_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "tool_execution_version": { + "name": "tool_execution_version", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "tool_admission_closed_at": { + "name": "tool_admission_closed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "parent_run_id": { + "name": "parent_run_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "chat_id": { + "name": "chat_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "stream_id": { + "name": "stream_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "agent": { + "name": "agent", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "model": { + "name": "model", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "copilot_run_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'active'" + }, + "request_context": { + "name": "request_context", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "desktop_device_id": { + "name": "desktop_device_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "started_at": { + "name": "started_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "error": { + "name": "error", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "copilot_runs_parent_run_id_idx": { + "name": "copilot_runs_parent_run_id_idx", + "columns": [ + { + "expression": "parent_run_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_runs_chat_id_idx": { + "name": "copilot_runs_chat_id_idx", + "columns": [ + { + "expression": "chat_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_runs_chat_started_at_idx": { + "name": "copilot_runs_chat_started_at_idx", + "columns": [ + { + "expression": "chat_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "started_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_runs_user_id_idx": { + "name": "copilot_runs_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_runs_workflow_id_idx": { + "name": "copilot_runs_workflow_id_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_runs_workspace_id_idx": { + "name": "copilot_runs_workspace_id_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_runs_status_idx": { + "name": "copilot_runs_status_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_runs_chat_execution_idx": { + "name": "copilot_runs_chat_execution_idx", + "columns": [ + { + "expression": "chat_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "execution_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_runs_execution_started_at_idx": { + "name": "copilot_runs_execution_started_at_idx", + "columns": [ + { + "expression": "execution_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "started_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_runs_workspace_completed_at_id_idx": { + "name": "copilot_runs_workspace_completed_at_id_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "date_trunc('milliseconds', \"completed_at\")", + "asc": true, + "isExpression": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_runs_stream_id_unique": { + "name": "copilot_runs_stream_id_unique", + "columns": [ + { + "expression": "stream_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_runs_desktop_device_started_at_idx": { + "name": "copilot_runs_desktop_device_started_at_idx", + "columns": [ + { + "expression": "desktop_device_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "started_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"copilot_runs\".\"desktop_device_id\" IS NOT NULL", + "concurrently": true, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "copilot_runs_chat_id_copilot_chats_id_fk": { + "name": "copilot_runs_chat_id_copilot_chats_id_fk", + "tableFrom": "copilot_runs", + "tableTo": "copilot_chats", + "columnsFrom": ["chat_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "copilot_runs_user_id_user_id_fk": { + "name": "copilot_runs_user_id_user_id_fk", + "tableFrom": "copilot_runs", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "copilot_runs_workflow_id_workflow_id_fk": { + "name": "copilot_runs_workflow_id_workflow_id_fk", + "tableFrom": "copilot_runs", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "copilot_runs_workspace_id_workspace_id_fk": { + "name": "copilot_runs_workspace_id_workspace_id_fk", + "tableFrom": "copilot_runs", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "copilot_runs_organization_id_organization_id_fk": { + "name": "copilot_runs_organization_id_organization_id_fk", + "tableFrom": "copilot_runs", + "tableTo": "organization", + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "copilot_runs_desktop_device_id_desktop_devices_id_fk": { + "name": "copilot_runs_desktop_device_id_desktop_devices_id_fk", + "tableFrom": "copilot_runs", + "tableTo": "desktop_devices", + "columnsFrom": ["desktop_device_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.copilot_service_usage": { + "name": "copilot_service_usage", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "stream_id": { + "name": "stream_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "tool_call_id": { + "name": "tool_call_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "service": { + "name": "service", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "cost_usd": { + "name": "cost_usd", + "type": "numeric(12, 8)", + "primaryKey": false, + "notNull": false + }, + "worker_origin": { + "name": "worker_origin", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "next_attempt_at": { + "name": "next_attempt_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "attempts": { + "name": "attempts", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "delivered_at": { + "name": "delivered_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "last_error": { + "name": "last_error", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "copilot_service_usage_pending_idx": { + "name": "copilot_service_usage_pending_idx", + "columns": [ + { + "expression": "next_attempt_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "delivered_at IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.copilot_task_subscriptions": { + "name": "copilot_task_subscriptions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "task_id": { + "name": "task_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "execution_id": { + "name": "execution_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "chat_id": { + "name": "chat_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "copilot_task_subscriptions_execution_idx": { + "name": "copilot_task_subscriptions_execution_idx", + "columns": [ + { + "expression": "execution_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_task_subscriptions_task_idx": { + "name": "copilot_task_subscriptions_task_idx", + "columns": [ + { + "expression": "task_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "copilot_task_subscriptions_chat_id_copilot_chats_id_fk": { + "name": "copilot_task_subscriptions_chat_id_copilot_chats_id_fk", + "tableFrom": "copilot_task_subscriptions", + "tableTo": "copilot_chats", + "columnsFrom": ["chat_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "copilot_task_subscriptions_workspace_id_workspace_id_fk": { + "name": "copilot_task_subscriptions_workspace_id_workspace_id_fk", + "tableFrom": "copilot_task_subscriptions", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "copilot_task_subscriptions_user_id_user_id_fk": { + "name": "copilot_task_subscriptions_user_id_user_id_fk", + "tableFrom": "copilot_task_subscriptions", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.copilot_workflow_read_hashes": { + "name": "copilot_workflow_read_hashes", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "chat_id": { + "name": "chat_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "hash": { + "name": "hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "copilot_workflow_read_hashes_chat_id_idx": { + "name": "copilot_workflow_read_hashes_chat_id_idx", + "columns": [ + { + "expression": "chat_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_workflow_read_hashes_workflow_id_idx": { + "name": "copilot_workflow_read_hashes_workflow_id_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_workflow_read_hashes_chat_workflow_unique": { + "name": "copilot_workflow_read_hashes_chat_workflow_unique", + "columns": [ + { + "expression": "chat_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "copilot_workflow_read_hashes_chat_id_copilot_chats_id_fk": { + "name": "copilot_workflow_read_hashes_chat_id_copilot_chats_id_fk", + "tableFrom": "copilot_workflow_read_hashes", + "tableTo": "copilot_chats", + "columnsFrom": ["chat_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "copilot_workflow_read_hashes_workflow_id_workflow_id_fk": { + "name": "copilot_workflow_read_hashes_workflow_id_workflow_id_fk", + "tableFrom": "copilot_workflow_read_hashes", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.credential": { + "name": "credential", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "slack_app_id": { + "name": "slack_app_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "type": { + "name": "type", + "type": "credential_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "display_name": { + "name": "display_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "unredacted": { + "name": "unredacted", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "provider_id": { + "name": "provider_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "account_id": { + "name": "account_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "env_key": { + "name": "env_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "env_owner_user_id": { + "name": "env_owner_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "encrypted_service_account_key": { + "name": "encrypted_service_account_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "encrypted_personal_token": { + "name": "encrypted_personal_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "authorization_app_id": { + "name": "authorization_app_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "credential_group_enrollment_id": { + "name": "credential_group_enrollment_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "credential_group_option_id": { + "name": "credential_group_option_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "mcp_server_id": { + "name": "mcp_server_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "mcp_oauth_config_version": { + "name": "mcp_oauth_config_version", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "managed_oauth_scope_version": { + "name": "managed_oauth_scope_version", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "provider_subject_id": { + "name": "provider_subject_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "provider_tenant_id": { + "name": "provider_tenant_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "managed_oauth_status": { + "name": "managed_oauth_status", + "type": "managed_oauth_credential_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": false + }, + "granted_scopes": { + "name": "granted_scopes", + "type": "text[]", + "primaryKey": false, + "notNull": false + }, + "provider_metadata": { + "name": "provider_metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "encrypted_oauth_token_set": { + "name": "encrypted_oauth_token_set", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "mcp_tools": { + "name": "mcp_tools", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "mcp_tools_refreshed_at": { + "name": "mcp_tools_refreshed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "granted_at": { + "name": "granted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "revoked_at": { + "name": "revoked_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "access_token_expires_at": { + "name": "access_token_expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "refresh_token_expires_at": { + "name": "refresh_token_expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "last_refreshed_at": { + "name": "last_refreshed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "credential_organization_id_idx": { + "name": "credential_organization_id_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "credential_organization_account_unique": { + "name": "credential_organization_account_unique", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "account_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"credential\".\"account_id\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "credential_org_personal_token_unique": { + "name": "credential_org_personal_token_unique", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_by", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "provider_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "provider_tenant_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "provider_subject_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"credential\".\"type\" = 'personal_token'", + "concurrently": false, + "method": "btree", + "with": {} + }, + "credential_workspace_id_idx": { + "name": "credential_workspace_id_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "credential_type_idx": { + "name": "credential_type_idx", + "columns": [ + { + "expression": "type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "credential_provider_id_idx": { + "name": "credential_provider_id_idx", + "columns": [ + { + "expression": "provider_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "credential_account_id_idx": { + "name": "credential_account_id_idx", + "columns": [ + { + "expression": "account_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "credential_env_owner_user_id_idx": { + "name": "credential_env_owner_user_id_idx", + "columns": [ + { + "expression": "env_owner_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "credential_group_enrollment_idx": { + "name": "credential_group_enrollment_idx", + "columns": [ + { + "expression": "credential_group_enrollment_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "credential_mcp_server_idx": { + "name": "credential_mcp_server_idx", + "columns": [ + { + "expression": "mcp_server_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "credential_group_option_unique": { + "name": "credential_group_option_unique", + "columns": [ + { + "expression": "credential_group_enrollment_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "credential_group_option_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"credential\".\"type\" = 'managed_oauth'", + "concurrently": false, + "method": "btree", + "with": {} + }, + "credential_managed_mcp_enrollment_server_unique": { + "name": "credential_managed_mcp_enrollment_server_unique", + "columns": [ + { + "expression": "credential_group_enrollment_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "mcp_server_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"credential\".\"type\" = 'managed_mcp'", + "concurrently": false, + "method": "btree", + "with": {} + }, + "credential_workspace_account_unique": { + "name": "credential_workspace_account_unique", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "account_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "account_id IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "credential_workspace_env_unique": { + "name": "credential_workspace_env_unique", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "env_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "type = 'env_workspace'", + "concurrently": false, + "method": "btree", + "with": {} + }, + "credential_workspace_personal_env_unique": { + "name": "credential_workspace_personal_env_unique", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "env_key", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "env_owner_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "type = 'env_personal'", + "concurrently": false, + "method": "btree", + "with": {} + }, + "credential_personal_token_identity_unique": { + "name": "credential_personal_token_identity_unique", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_by", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "provider_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "provider_tenant_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "provider_subject_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "type = 'personal_token'", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "credential_workspace_id_workspace_id_fk": { + "name": "credential_workspace_id_workspace_id_fk", + "tableFrom": "credential", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "credential_organization_id_organization_id_fk": { + "name": "credential_organization_id_organization_id_fk", + "tableFrom": "credential", + "tableTo": "organization", + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "credential_slack_app_id_slack_app_id_fk": { + "name": "credential_slack_app_id_slack_app_id_fk", + "tableFrom": "credential", + "tableTo": "slack_app", + "columnsFrom": ["slack_app_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + }, + "credential_account_id_account_id_fk": { + "name": "credential_account_id_account_id_fk", + "tableFrom": "credential", + "tableTo": "account", + "columnsFrom": ["account_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "credential_env_owner_user_id_user_id_fk": { + "name": "credential_env_owner_user_id_user_id_fk", + "tableFrom": "credential", + "tableTo": "user", + "columnsFrom": ["env_owner_user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "credential_credential_group_enrollment_id_credential_group_enrollment_id_fk": { + "name": "credential_credential_group_enrollment_id_credential_group_enrollment_id_fk", + "tableFrom": "credential", + "tableTo": "credential_group_enrollment", + "columnsFrom": ["credential_group_enrollment_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "credential_mcp_server_id_mcp_servers_id_fk": { + "name": "credential_mcp_server_id_mcp_servers_id_fk", + "tableFrom": "credential", + "tableTo": "mcp_servers", + "columnsFrom": ["mcp_server_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "credential_created_by_user_id_fk": { + "name": "credential_created_by_user_id_fk", + "tableFrom": "credential", + "tableTo": "user", + "columnsFrom": ["created_by"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "credential_owner_check": { + "name": "credential_owner_check", + "value": "num_nonnulls(\"credential\".\"workspace_id\", \"credential\".\"organization_id\") = 1" + }, + "credential_organization_type_check": { + "name": "credential_organization_type_check", + "value": "\"credential\".\"organization_id\" IS NULL OR \"credential\".\"type\" IN ('oauth', 'managed_oauth', 'managed_mcp', 'service_account', 'personal_token')" + }, + "credential_personal_token_source_check": { + "name": "credential_personal_token_source_check", + "value": "(type::text <> 'personal_token') OR (\n created_by IS NOT NULL\n AND provider_id IS NOT NULL\n AND provider_id = 'gitlab'\n AND provider_subject_id IS NOT NULL\n AND provider_tenant_id IS NOT NULL\n AND encrypted_personal_token IS NOT NULL\n AND granted_scopes IS NOT NULL\n AND cardinality(granted_scopes) > 0\n AND account_id IS NULL\n AND env_key IS NULL\n AND env_owner_user_id IS NULL\n AND authorization_app_id IS NULL\n AND encrypted_oauth_token_set IS NULL\n AND encrypted_service_account_key IS NULL\n AND unredacted = false\n )" + }, + "credential_oauth_source_check": { + "name": "credential_oauth_source_check", + "value": "(type <> 'oauth') OR (account_id IS NOT NULL AND provider_id IS NOT NULL)" + }, + "credential_managed_oauth_source_check": { + "name": "credential_managed_oauth_source_check", + "value": "(type::text <> 'managed_oauth') OR (\n account_id IS NULL\n AND provider_id IS NOT NULL\n AND authorization_app_id IS NOT NULL\n AND provider_subject_id IS NOT NULL\n AND managed_oauth_status IS NOT NULL\n AND granted_scopes IS NOT NULL\n AND encrypted_oauth_token_set IS NOT NULL\n AND granted_at IS NOT NULL\n )" + }, + "credential_managed_oauth_group_binding_check": { + "name": "credential_managed_oauth_group_binding_check", + "value": "(type::text <> 'managed_oauth') OR (\n credential_group_enrollment_id IS NOT NULL\n AND credential_group_option_id IS NOT NULL\n AND managed_oauth_scope_version IS NOT NULL\n AND managed_oauth_scope_version > 0\n )" + }, + "credential_managed_mcp_source_check": { + "name": "credential_managed_mcp_source_check", + "value": "(type::text <> 'managed_mcp') OR (\n id LIKE 'mcp-cg-%'\n AND account_id IS NULL\n AND provider_id IS NULL\n AND authorization_app_id IS NULL\n AND credential_group_enrollment_id IS NOT NULL\n AND credential_group_option_id IS NULL\n AND mcp_server_id IS NOT NULL\n AND managed_oauth_status IS NOT NULL\n AND (managed_oauth_status <> 'active' OR (\n encrypted_oauth_token_set IS NOT NULL\n AND mcp_tools IS NOT NULL\n ))\n AND granted_at IS NOT NULL\n AND managed_oauth_scope_version IS NULL\n AND provider_subject_id IS NULL\n AND provider_tenant_id IS NULL\n AND granted_scopes IS NULL\n AND provider_metadata IS NULL\n AND created_by IS NULL\n AND env_key IS NULL\n AND env_owner_user_id IS NULL\n AND encrypted_service_account_key IS NULL\n AND unredacted = false\n )" + }, + "credential_creator_source_check": { + "name": "credential_creator_source_check", + "value": "(type::text = 'managed_mcp') OR created_by IS NOT NULL" + }, + "credential_workspace_env_source_check": { + "name": "credential_workspace_env_source_check", + "value": "(type <> 'env_workspace') OR (env_key IS NOT NULL AND env_owner_user_id IS NULL)" + }, + "credential_personal_env_source_check": { + "name": "credential_personal_env_source_check", + "value": "(type <> 'env_personal') OR (env_key IS NOT NULL AND env_owner_user_id IS NOT NULL)" + } + }, + "isRLSEnabled": false + }, + "public.credential_group": { + "name": "credential_group", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "public_id": { + "name": "public_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "options": { + "name": "options", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "encrypted_provider_configuration": { + "name": "encrypted_provider_configuration", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "credential_group_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'active'" + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "credential_group_organization_id_idx": { + "name": "credential_group_organization_id_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "credential_group_organization_unique": { + "name": "credential_group_organization_unique", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "credential_group_public_id_unique": { + "name": "credential_group_public_id_unique", + "columns": [ + { + "expression": "public_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "credential_group_workspace_unique": { + "name": "credential_group_workspace_unique", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "credential_group_workspace_id_workspace_id_fk": { + "name": "credential_group_workspace_id_workspace_id_fk", + "tableFrom": "credential_group", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "credential_group_organization_id_organization_id_fk": { + "name": "credential_group_organization_id_organization_id_fk", + "tableFrom": "credential_group", + "tableTo": "organization", + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "credential_group_created_by_user_id_fk": { + "name": "credential_group_created_by_user_id_fk", + "tableFrom": "credential_group", + "tableTo": "user", + "columnsFrom": ["created_by"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "credential_group_owner_check": { + "name": "credential_group_owner_check", + "value": "num_nonnulls(\"credential_group\".\"workspace_id\", \"credential_group\".\"organization_id\") = 1" + } + }, + "isRLSEnabled": false + }, + "public.credential_group_enrollment": { + "name": "credential_group_enrollment", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "credential_group_id": { + "name": "credential_group_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "credential_group_enrollment_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'invited'" + }, + "invitation_token_hash": { + "name": "invitation_token_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "invitation_expires_at": { + "name": "invitation_expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "invited_at": { + "name": "invited_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "sent_at": { + "name": "sent_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "revoked_at": { + "name": "revoked_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "last_delivery_error": { + "name": "last_delivery_error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "credential_group_enrollment_group_user_unique": { + "name": "credential_group_enrollment_group_user_unique", + "columns": [ + { + "expression": "credential_group_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"credential_group_enrollment\".\"user_id\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "credential_group_enrollment_user_id_idx": { + "name": "credential_group_enrollment_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "credential_group_enrollment_group_email_unique": { + "name": "credential_group_enrollment_group_email_unique", + "columns": [ + { + "expression": "credential_group_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "email", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "credential_group_enrollment_invitation_token_hash_unique": { + "name": "credential_group_enrollment_invitation_token_hash_unique", + "columns": [ + { + "expression": "invitation_token_hash", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "credential_group_enrollment_group_status_idx": { + "name": "credential_group_enrollment_group_status_idx", + "columns": [ + { + "expression": "credential_group_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "credential_group_enrollment_group_invited_at_id_idx": { + "name": "credential_group_enrollment_group_invited_at_id_idx", + "columns": [ + { + "expression": "credential_group_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "invited_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "credential_group_enrollment_credential_group_id_credential_group_id_fk": { + "name": "credential_group_enrollment_credential_group_id_credential_group_id_fk", + "tableFrom": "credential_group_enrollment", + "tableTo": "credential_group", + "columnsFrom": ["credential_group_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "credential_group_enrollment_user_id_user_id_fk": { + "name": "credential_group_enrollment_user_id_user_id_fk", + "tableFrom": "credential_group_enrollment", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "credential_group_enrollment_created_by_user_id_fk": { + "name": "credential_group_enrollment_created_by_user_id_fk", + "tableFrom": "credential_group_enrollment", + "tableTo": "user", + "columnsFrom": ["created_by"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "credential_group_enrollment_normalized_email_check": { + "name": "credential_group_enrollment_normalized_email_check", + "value": "\"credential_group_enrollment\".\"email\" = lower(btrim(\"credential_group_enrollment\".\"email\")) AND length(\"credential_group_enrollment\".\"email\") BETWEEN 3 AND 320" + }, + "credential_group_enrollment_invitation_token_hash_length_check": { + "name": "credential_group_enrollment_invitation_token_hash_length_check", + "value": "length(\"credential_group_enrollment\".\"invitation_token_hash\") = 64" + } + }, + "isRLSEnabled": false + }, + "public.credential_member": { + "name": "credential_member", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "credential_id": { + "name": "credential_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "role": { + "name": "role", + "type": "credential_member_role", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'member'" + }, + "status": { + "name": "status", + "type": "credential_member_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'active'" + }, + "joined_at": { + "name": "joined_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "invited_by": { + "name": "invited_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "credential_member_user_id_idx": { + "name": "credential_member_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "credential_member_role_idx": { + "name": "credential_member_role_idx", + "columns": [ + { + "expression": "role", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "credential_member_status_idx": { + "name": "credential_member_status_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "credential_member_unique": { + "name": "credential_member_unique", + "columns": [ + { + "expression": "credential_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "credential_member_credential_id_credential_id_fk": { + "name": "credential_member_credential_id_credential_id_fk", + "tableFrom": "credential_member", + "tableTo": "credential", + "columnsFrom": ["credential_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "credential_member_user_id_user_id_fk": { + "name": "credential_member_user_id_user_id_fk", + "tableFrom": "credential_member", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "credential_member_invited_by_user_id_fk": { + "name": "credential_member_invited_by_user_id_fk", + "tableFrom": "credential_member", + "tableTo": "user", + "columnsFrom": ["invited_by"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.custom_block": { + "name": "custom_block", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "icon_url": { + "name": "icon_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "inputs": { + "name": "inputs", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "outputs": { + "name": "outputs", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "trace_child_runs": { + "name": "trace_child_runs", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "custom_block_organization_id_idx": { + "name": "custom_block_organization_id_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "custom_block_workflow_id_idx": { + "name": "custom_block_workflow_id_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "custom_block_organization_type_unique": { + "name": "custom_block_organization_type_unique", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "custom_block_organization_id_organization_id_fk": { + "name": "custom_block_organization_id_organization_id_fk", + "tableFrom": "custom_block", + "tableTo": "organization", + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "custom_block_workflow_id_workflow_id_fk": { + "name": "custom_block_workflow_id_workflow_id_fk", + "tableFrom": "custom_block", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "custom_block_created_by_user_id_fk": { + "name": "custom_block_created_by_user_id_fk", + "tableFrom": "custom_block", + "tableTo": "user", + "columnsFrom": ["created_by"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.custom_tools": { + "name": "custom_tools", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "schema": { + "name": "schema", + "type": "json", + "primaryKey": false, + "notNull": true + }, + "code": { + "name": "code", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "custom_tools_workspace_id_idx": { + "name": "custom_tools_workspace_id_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "custom_tools_workspace_title_unique": { + "name": "custom_tools_workspace_title_unique", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "title", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "custom_tools_workspace_id_workspace_id_fk": { + "name": "custom_tools_workspace_id_workspace_id_fk", + "tableFrom": "custom_tools", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "custom_tools_user_id_user_id_fk": { + "name": "custom_tools_user_id_user_id_fk", + "tableFrom": "custom_tools", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.dashboard": { + "name": "dashboard", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "content": { + "name": "content", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "revision": { + "name": "revision", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "updated_by": { + "name": "updated_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "dashboard_workspace_id_unique": { + "name": "dashboard_workspace_id_unique", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "dashboard_workspace_id_workspace_id_fk": { + "name": "dashboard_workspace_id_workspace_id_fk", + "tableFrom": "dashboard", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "dashboard_created_by_user_id_fk": { + "name": "dashboard_created_by_user_id_fk", + "tableFrom": "dashboard", + "tableTo": "user", + "columnsFrom": ["created_by"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + }, + "dashboard_updated_by_user_id_fk": { + "name": "dashboard_updated_by_user_id_fk", + "tableFrom": "dashboard", + "tableTo": "user", + "columnsFrom": ["updated_by"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.data_drain_runs": { + "name": "data_drain_runs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "drain_id": { + "name": "drain_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "data_drain_run_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "trigger": { + "name": "trigger", + "type": "data_drain_run_trigger", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "started_at": { + "name": "started_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "finished_at": { + "name": "finished_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "rows_exported": { + "name": "rows_exported", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "bytes_written": { + "name": "bytes_written", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "cursor_before": { + "name": "cursor_before", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "cursor_after": { + "name": "cursor_after", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "error": { + "name": "error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "locators": { + "name": "locators", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + } + }, + "indexes": { + "data_drain_runs_drain_started_idx": { + "name": "data_drain_runs_drain_started_idx", + "columns": [ + { + "expression": "drain_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "started_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "data_drain_runs_drain_id_data_drains_id_fk": { + "name": "data_drain_runs_drain_id_data_drains_id_fk", + "tableFrom": "data_drain_runs", + "tableTo": "data_drains", + "columnsFrom": ["drain_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.data_drains": { + "name": "data_drains", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "source": { + "name": "source", + "type": "data_drain_source", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "destination_type": { + "name": "destination_type", + "type": "data_drain_destination", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "destination_config": { + "name": "destination_config", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "destination_credentials": { + "name": "destination_credentials", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "schedule_cadence": { + "name": "schedule_cadence", + "type": "data_drain_cadence", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "cursor": { + "name": "cursor", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "last_run_at": { + "name": "last_run_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "last_success_at": { + "name": "last_success_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "data_drains_org_idx": { + "name": "data_drains_org_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "data_drains_due_idx": { + "name": "data_drains_due_idx", + "columns": [ + { + "expression": "enabled", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "last_run_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "data_drains_org_name_unique": { + "name": "data_drains_org_name_unique", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "data_drains_organization_id_organization_id_fk": { + "name": "data_drains_organization_id_organization_id_fk", + "tableFrom": "data_drains", + "tableTo": "organization", + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "data_drains_created_by_user_id_fk": { + "name": "data_drains_created_by_user_id_fk", + "tableFrom": "data_drains", + "tableTo": "user", + "columnsFrom": ["created_by"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.desktop_devices": { + "name": "desktop_devices", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "app_version": { + "name": "app_version", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "platform": { + "name": "platform", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "capabilities": { + "name": "capabilities", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "last_seen_at": { + "name": "last_seen_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "revoked_at": { + "name": "revoked_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "desktop_devices_user_id_idx": { + "name": "desktop_devices_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "desktop_devices_session_id_idx": { + "name": "desktop_devices_session_id_idx", + "columns": [ + { + "expression": "session_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "desktop_devices_user_id_user_id_fk": { + "name": "desktop_devices_user_id_user_id_fk", + "tableFrom": "desktop_devices", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "desktop_devices_session_id_session_id_fk": { + "name": "desktop_devices_session_id_session_id_fk", + "tableFrom": "desktop_devices", + "tableTo": "session", + "columnsFrom": ["session_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.docs_embeddings": { + "name": "docs_embeddings", + "schema": "", + "columns": { + "chunk_id": { + "name": "chunk_id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "chunk_text": { + "name": "chunk_text", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "source_document": { + "name": "source_document", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "source_link": { + "name": "source_link", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "header_text": { + "name": "header_text", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "header_level": { + "name": "header_level", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "token_count": { + "name": "token_count", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "embedding": { + "name": "embedding", + "type": "vector(1536)", + "primaryKey": false, + "notNull": true + }, + "embedding_model": { + "name": "embedding_model", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'text-embedding-3-small'" + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "chunk_text_tsv": { + "name": "chunk_text_tsv", + "type": "tsvector", + "primaryKey": false, + "notNull": false, + "generated": { + "as": "to_tsvector('english', \"docs_embeddings\".\"chunk_text\")", + "type": "stored" + } + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "docs_emb_source_document_idx": { + "name": "docs_emb_source_document_idx", + "columns": [ + { + "expression": "source_document", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "docs_emb_header_level_idx": { + "name": "docs_emb_header_level_idx", + "columns": [ + { + "expression": "header_level", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "docs_emb_source_header_idx": { + "name": "docs_emb_source_header_idx", + "columns": [ + { + "expression": "source_document", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "header_level", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "docs_emb_model_idx": { + "name": "docs_emb_model_idx", + "columns": [ + { + "expression": "embedding_model", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "docs_emb_created_at_idx": { + "name": "docs_emb_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "docs_embedding_vector_hnsw_idx": { + "name": "docs_embedding_vector_hnsw_idx", + "columns": [ + { + "expression": "embedding", + "isExpression": false, + "asc": true, + "nulls": "last", + "opclass": "vector_cosine_ops" + } + ], + "isUnique": false, + "concurrently": false, + "method": "hnsw", + "with": { + "m": 16, + "ef_construction": 64 + } + }, + "docs_emb_metadata_gin_idx": { + "name": "docs_emb_metadata_gin_idx", + "columns": [ + { + "expression": "metadata", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "gin", + "with": {} + }, + "docs_emb_chunk_text_fts_idx": { + "name": "docs_emb_chunk_text_fts_idx", + "columns": [ + { + "expression": "chunk_text_tsv", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "gin", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "docs_embedding_not_null_check": { + "name": "docs_embedding_not_null_check", + "value": "\"embedding\" IS NOT NULL" + }, + "docs_header_level_check": { + "name": "docs_header_level_check", + "value": "\"header_level\" >= 1 AND \"header_level\" <= 6" + } + }, + "isRLSEnabled": false + }, + "public.document": { + "name": "document", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "knowledge_base_id": { + "name": "knowledge_base_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "filename": { + "name": "filename", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "file_url": { + "name": "file_url", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "storage_key": { + "name": "storage_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "file_size": { + "name": "file_size", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "mime_type": { + "name": "mime_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "chunk_count": { + "name": "chunk_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "token_count": { + "name": "token_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "character_count": { + "name": "character_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "processing_status": { + "name": "processing_status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "processing_attempts": { + "name": "processing_attempts", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "processing_queued_at": { + "name": "processing_queued_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "processing_queue_token": { + "name": "processing_queue_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "processing_started_at": { + "name": "processing_started_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "processing_deferred_until": { + "name": "processing_deferred_until", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "processing_completed_at": { + "name": "processing_completed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "processing_error": { + "name": "processing_error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "processing_recovery_after": { + "name": "processing_recovery_after", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "archived_at": { + "name": "archived_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "user_excluded": { + "name": "user_excluded", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "tag1": { + "name": "tag1", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tag2": { + "name": "tag2", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tag3": { + "name": "tag3", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tag4": { + "name": "tag4", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tag5": { + "name": "tag5", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tag6": { + "name": "tag6", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tag7": { + "name": "tag7", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "number1": { + "name": "number1", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "number2": { + "name": "number2", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "number3": { + "name": "number3", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "number4": { + "name": "number4", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "number5": { + "name": "number5", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "date1": { + "name": "date1", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "date2": { + "name": "date2", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "boolean1": { + "name": "boolean1", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "boolean2": { + "name": "boolean2", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "boolean3": { + "name": "boolean3", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "connector_id": { + "name": "connector_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "external_id": { + "name": "external_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "content_hash": { + "name": "content_hash", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "source_url": { + "name": "source_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "secret_provenance_version": { + "name": "secret_provenance_version", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "uploaded_by": { + "name": "uploaded_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "acl": { + "name": "acl", + "type": "text[]", + "primaryKey": false, + "notNull": true, + "default": "'{ws}'::text[]" + }, + "acl_requirements": { + "name": "acl_requirements", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "acl_verified_at": { + "name": "acl_verified_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "source_modified_at": { + "name": "source_modified_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "source_seen_at": { + "name": "source_seen_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "uploaded_at": { + "name": "uploaded_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "doc_kb_id_idx": { + "name": "doc_kb_id_idx", + "columns": [ + { + "expression": "knowledge_base_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_kb_source_modified_idx": { + "name": "doc_kb_source_modified_idx", + "columns": [ + { + "expression": "knowledge_base_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "source_modified_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"document\".\"deleted_at\" IS NULL", + "concurrently": true, + "method": "btree", + "with": {} + }, + "doc_acl_gin_idx": { + "name": "doc_acl_gin_idx", + "columns": [ + { + "expression": "acl", + "isExpression": false, + "asc": true, + "nulls": "last", + "opclass": "array_ops" + } + ], + "isUnique": false, + "where": "\"document\".\"deleted_at\" IS NULL", + "concurrently": false, + "method": "gin", + "with": {} + }, + "doc_filename_idx": { + "name": "doc_filename_idx", + "columns": [ + { + "expression": "filename", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_processing_status_idx": { + "name": "doc_processing_status_idx", + "columns": [ + { + "expression": "knowledge_base_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "processing_status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_processing_recovery_idx": { + "name": "doc_processing_recovery_idx", + "columns": [ + { + "expression": "uploaded_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"document\".\"processing_status\" IN ('pending', 'processing', 'failed') AND \"document\".\"connector_id\" IS NOT NULL AND \"document\".\"content_hash\" IS NOT NULL AND \"document\".\"storage_key\" IS NOT NULL AND \"document\".\"user_excluded\" = false AND \"document\".\"archived_at\" IS NULL AND \"document\".\"deleted_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_connector_processing_recovery_idx": { + "name": "doc_connector_processing_recovery_idx", + "columns": [ + { + "expression": "connector_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "uploaded_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"document\".\"processing_status\" IN ('pending', 'processing', 'failed') AND \"document\".\"connector_id\" IS NOT NULL AND \"document\".\"content_hash\" IS NOT NULL AND \"document\".\"storage_key\" IS NOT NULL AND \"document\".\"user_excluded\" = false AND \"document\".\"archived_at\" IS NULL AND \"document\".\"deleted_at\" IS NULL", + "concurrently": true, + "method": "btree", + "with": {} + }, + "doc_connector_processing_status_idx": { + "name": "doc_connector_processing_status_idx", + "columns": [ + { + "expression": "connector_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "processing_status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"document\".\"processing_status\" IN ('pending', 'processing', 'failed') AND \"document\".\"connector_id\" IS NOT NULL AND \"document\".\"user_excluded\" = false AND \"document\".\"archived_at\" IS NULL AND \"document\".\"deleted_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_connector_external_id_idx": { + "name": "doc_connector_external_id_idx", + "columns": [ + { + "expression": "connector_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "external_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"document\".\"deleted_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_connector_source_lookup_idx": { + "name": "doc_connector_source_lookup_idx", + "columns": [ + { + "expression": "connector_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "external_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_connector_reconciliation_idx": { + "name": "doc_connector_reconciliation_idx", + "columns": [ + { + "expression": "connector_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "COALESCE(\"source_seen_at\", '-infinity'::timestamp)", + "asc": true, + "isExpression": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"document\".\"user_excluded\" = false AND \"document\".\"archived_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_connector_reconciliation_v2_idx": { + "name": "doc_connector_reconciliation_v2_idx", + "columns": [ + { + "expression": "connector_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"document\".\"user_excluded\" = false AND \"document\".\"archived_at\" IS NULL", + "concurrently": true, + "method": "btree", + "with": {} + }, + "doc_active_kb_token_count_idx": { + "name": "doc_active_kb_token_count_idx", + "columns": [ + { + "expression": "knowledge_base_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "token_count", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"document\".\"user_excluded\" = false AND \"document\".\"archived_at\" IS NULL AND \"document\".\"deleted_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_storage_key_idx": { + "name": "doc_storage_key_idx", + "columns": [ + { + "expression": "storage_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"document\".\"storage_key\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_archived_at_partial_idx": { + "name": "doc_archived_at_partial_idx", + "columns": [ + { + "expression": "archived_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"document\".\"archived_at\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_deleted_at_partial_idx": { + "name": "doc_deleted_at_partial_idx", + "columns": [ + { + "expression": "deleted_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"document\".\"deleted_at\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_connector_tombstone_idx": { + "name": "doc_connector_tombstone_idx", + "columns": [ + { + "expression": "connector_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"document\".\"archived_at\" IS NULL AND (\"document\".\"deleted_at\" IS NOT NULL OR \"document\".\"content_hash\" IS NULL)", + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_connector_live_idx": { + "name": "doc_connector_live_idx", + "columns": [ + { + "expression": "connector_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"document\".\"user_excluded\" = false AND \"document\".\"archived_at\" IS NULL AND \"document\".\"deleted_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_kb_tag1_lower_idx": { + "name": "doc_kb_tag1_lower_idx", + "columns": [ + { + "expression": "knowledge_base_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "lower(\"tag1\")", + "asc": true, + "isExpression": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_kb_tag2_lower_idx": { + "name": "doc_kb_tag2_lower_idx", + "columns": [ + { + "expression": "knowledge_base_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "lower(\"tag2\")", + "asc": true, + "isExpression": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_kb_tag3_lower_idx": { + "name": "doc_kb_tag3_lower_idx", + "columns": [ + { + "expression": "knowledge_base_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "lower(\"tag3\")", + "asc": true, + "isExpression": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_kb_tag4_lower_idx": { + "name": "doc_kb_tag4_lower_idx", + "columns": [ + { + "expression": "knowledge_base_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "lower(\"tag4\")", + "asc": true, + "isExpression": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_kb_tag5_lower_idx": { + "name": "doc_kb_tag5_lower_idx", + "columns": [ + { + "expression": "knowledge_base_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "lower(\"tag5\")", + "asc": true, + "isExpression": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_kb_tag6_lower_idx": { + "name": "doc_kb_tag6_lower_idx", + "columns": [ + { + "expression": "knowledge_base_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "lower(\"tag6\")", + "asc": true, + "isExpression": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_kb_tag7_lower_idx": { + "name": "doc_kb_tag7_lower_idx", + "columns": [ + { + "expression": "knowledge_base_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "lower(\"tag7\")", + "asc": true, + "isExpression": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_number1_idx": { + "name": "doc_number1_idx", + "columns": [ + { + "expression": "number1", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_number2_idx": { + "name": "doc_number2_idx", + "columns": [ + { + "expression": "number2", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_number3_idx": { + "name": "doc_number3_idx", + "columns": [ + { + "expression": "number3", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_number4_idx": { + "name": "doc_number4_idx", + "columns": [ + { + "expression": "number4", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_number5_idx": { + "name": "doc_number5_idx", + "columns": [ + { + "expression": "number5", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_date1_idx": { + "name": "doc_date1_idx", + "columns": [ + { + "expression": "date1", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": true, + "method": "btree", + "with": {} + }, + "doc_date2_idx": { + "name": "doc_date2_idx", + "columns": [ + { + "expression": "date2", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": true, + "method": "btree", + "with": {} + }, + "doc_boolean1_idx": { + "name": "doc_boolean1_idx", + "columns": [ + { + "expression": "boolean1", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_boolean2_idx": { + "name": "doc_boolean2_idx", + "columns": [ + { + "expression": "boolean2", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_boolean3_idx": { + "name": "doc_boolean3_idx", + "columns": [ + { + "expression": "boolean3", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "document_knowledge_base_id_knowledge_base_id_fk": { + "name": "document_knowledge_base_id_knowledge_base_id_fk", + "tableFrom": "document", + "tableTo": "knowledge_base", + "columnsFrom": ["knowledge_base_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "document_connector_id_knowledge_connector_id_fk": { + "name": "document_connector_id_knowledge_connector_id_fk", + "tableFrom": "document", + "tableTo": "knowledge_connector", + "columnsFrom": ["connector_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + }, + "document_uploaded_by_user_id_fk": { + "name": "document_uploaded_by_user_id_fk", + "tableFrom": "document", + "tableTo": "user", + "columnsFrom": ["uploaded_by"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "doc_acl_token_shape_check": { + "name": "doc_acl_token_shape_check", + "value": "array_position(\"document\".\"acl\", NULL) IS NULL AND (cardinality(\"document\".\"acl\") = 0 OR (cardinality(\"document\".\"acl\") = array_length(string_to_array(array_to_string(\"document\".\"acl\", E'\\n'), E'\\n'), 1) AND array_to_string(\"document\".\"acl\", E'\\n') ~ '^((ws|pub|link|u:[^\\nA-Z]+@[^\\nA-Z]+|[gs]:[^\\n:]+:[^\\n:]+:[^\\n]+)(\\n(ws|pub|link|u:[^\\nA-Z]+@[^\\nA-Z]+|[gs]:[^\\n:]+:[^\\n:]+:[^\\n]+))*)$'))" + } + }, + "isRLSEnabled": false + }, + "public.document_secret_provenance": { + "name": "document_secret_provenance", + "schema": "", + "columns": { + "document_id": { + "name": "document_id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "source_hash": { + "name": "source_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "entries": { + "name": "entries", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "document_secret_provenance_document_id_document_id_fk": { + "name": "document_secret_provenance_document_id_document_id_fk", + "tableFrom": "document_secret_provenance", + "tableTo": "document", + "columnsFrom": ["document_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "document_secret_provenance_status_check": { + "name": "document_secret_provenance_status_check", + "value": "\"document_secret_provenance\".\"status\" IN ('exact', 'unknown')" + } + }, + "isRLSEnabled": false + }, + "public.embedding": { + "name": "embedding", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "knowledge_base_id": { + "name": "knowledge_base_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "document_id": { + "name": "document_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "chunk_index": { + "name": "chunk_index", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "chunk_hash": { + "name": "chunk_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "content": { + "name": "content", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "secret_provenance_version": { + "name": "secret_provenance_version", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "content_length": { + "name": "content_length", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "token_count": { + "name": "token_count", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "embedding": { + "name": "embedding", + "type": "vector(1536)", + "primaryKey": false, + "notNull": false + }, + "embedding_384": { + "name": "embedding_384", + "type": "vector(384)", + "primaryKey": false, + "notNull": false + }, + "embedding_768": { + "name": "embedding_768", + "type": "vector(768)", + "primaryKey": false, + "notNull": false + }, + "embedding_1024": { + "name": "embedding_1024", + "type": "vector(1024)", + "primaryKey": false, + "notNull": false + }, + "embedding_3072": { + "name": "embedding_3072", + "type": "vector(3072)", + "primaryKey": false, + "notNull": false + }, + "embedding_model": { + "name": "embedding_model", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'text-embedding-3-small'" + }, + "start_offset": { + "name": "start_offset", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "end_offset": { + "name": "end_offset", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "tag1": { + "name": "tag1", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tag2": { + "name": "tag2", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tag3": { + "name": "tag3", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tag4": { + "name": "tag4", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tag5": { + "name": "tag5", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tag6": { + "name": "tag6", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tag7": { + "name": "tag7", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "number1": { + "name": "number1", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "number2": { + "name": "number2", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "number3": { + "name": "number3", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "number4": { + "name": "number4", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "number5": { + "name": "number5", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "date1": { + "name": "date1", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "date2": { + "name": "date2", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "boolean1": { + "name": "boolean1", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "boolean2": { + "name": "boolean2", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "boolean3": { + "name": "boolean3", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "content_tsv": { + "name": "content_tsv", + "type": "tsvector", + "primaryKey": false, + "notNull": false, + "generated": { + "as": "to_tsvector('english', \"embedding\".\"content\")", + "type": "stored" + } + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "emb_doc_chunk_idx": { + "name": "emb_doc_chunk_idx", + "columns": [ + { + "expression": "document_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "chunk_index", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_kb_model_idx": { + "name": "emb_kb_model_idx", + "columns": [ + { + "expression": "knowledge_base_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "embedding_model", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_kb_enabled_idx": { + "name": "emb_kb_enabled_idx", + "columns": [ + { + "expression": "knowledge_base_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "enabled", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_doc_enabled_idx": { + "name": "emb_doc_enabled_idx", + "columns": [ + { + "expression": "document_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "enabled", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_kb_tag1_lower_idx": { + "name": "emb_kb_tag1_lower_idx", + "columns": [ + { + "expression": "knowledge_base_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "lower(\"tag1\")", + "asc": true, + "isExpression": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_kb_tag2_lower_idx": { + "name": "emb_kb_tag2_lower_idx", + "columns": [ + { + "expression": "knowledge_base_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "lower(\"tag2\")", + "asc": true, + "isExpression": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_kb_tag3_lower_idx": { + "name": "emb_kb_tag3_lower_idx", + "columns": [ + { + "expression": "knowledge_base_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "lower(\"tag3\")", + "asc": true, + "isExpression": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_kb_tag4_lower_idx": { + "name": "emb_kb_tag4_lower_idx", + "columns": [ + { + "expression": "knowledge_base_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "lower(\"tag4\")", + "asc": true, + "isExpression": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_kb_tag5_lower_idx": { + "name": "emb_kb_tag5_lower_idx", + "columns": [ + { + "expression": "knowledge_base_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "lower(\"tag5\")", + "asc": true, + "isExpression": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_kb_tag6_lower_idx": { + "name": "emb_kb_tag6_lower_idx", + "columns": [ + { + "expression": "knowledge_base_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "lower(\"tag6\")", + "asc": true, + "isExpression": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_kb_tag7_lower_idx": { + "name": "emb_kb_tag7_lower_idx", + "columns": [ + { + "expression": "knowledge_base_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "lower(\"tag7\")", + "asc": true, + "isExpression": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_number1_idx": { + "name": "emb_number1_idx", + "columns": [ + { + "expression": "number1", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_number2_idx": { + "name": "emb_number2_idx", + "columns": [ + { + "expression": "number2", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_number3_idx": { + "name": "emb_number3_idx", + "columns": [ + { + "expression": "number3", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_number4_idx": { + "name": "emb_number4_idx", + "columns": [ + { + "expression": "number4", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_number5_idx": { + "name": "emb_number5_idx", + "columns": [ + { + "expression": "number5", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_date1_idx": { + "name": "emb_date1_idx", + "columns": [ + { + "expression": "date1", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": true, + "method": "btree", + "with": {} + }, + "emb_date2_idx": { + "name": "emb_date2_idx", + "columns": [ + { + "expression": "date2", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": true, + "method": "btree", + "with": {} + }, + "emb_boolean1_idx": { + "name": "emb_boolean1_idx", + "columns": [ + { + "expression": "boolean1", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_boolean2_idx": { + "name": "emb_boolean2_idx", + "columns": [ + { + "expression": "boolean2", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_boolean3_idx": { + "name": "emb_boolean3_idx", + "columns": [ + { + "expression": "boolean3", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_content_fts_idx": { + "name": "emb_content_fts_idx", + "columns": [ + { + "expression": "content_tsv", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "gin", + "with": {} + } + }, + "foreignKeys": { + "embedding_knowledge_base_id_knowledge_base_id_fk": { + "name": "embedding_knowledge_base_id_knowledge_base_id_fk", + "tableFrom": "embedding", + "tableTo": "knowledge_base", + "columnsFrom": ["knowledge_base_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "embedding_document_id_document_id_fk": { + "name": "embedding_document_id_document_id_fk", + "tableFrom": "embedding", + "tableTo": "document", + "columnsFrom": ["document_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "embedding_width_check": { + "name": "embedding_width_check", + "value": "num_nonnulls(\"embedding\", \"embedding_384\", \"embedding_768\", \"embedding_1024\", \"embedding_3072\") = 1" + } + }, + "isRLSEnabled": false + }, + "public.embedding_keyword_tin": { + "name": "embedding_keyword_tin", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "knowledge_base_id": { + "name": "knowledge_base_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "document_id": { + "name": "document_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true + }, + "content": { + "name": "content", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "connector_id": { + "name": "connector_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "acl": { + "name": "acl", + "type": "text[]", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "embedding_keyword_tin_document_idx": { + "name": "embedding_keyword_tin_document_idx", + "columns": [ + { + "expression": "document_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "embedding_keyword_tin_id_embedding_id_fk": { + "name": "embedding_keyword_tin_id_embedding_id_fk", + "tableFrom": "embedding_keyword_tin", + "tableTo": "embedding", + "columnsFrom": ["id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.embedding_search": { + "name": "embedding_search", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "knowledge_base_id": { + "name": "knowledge_base_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "document_id": { + "name": "document_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true + }, + "connector_id": { + "name": "connector_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "acl": { + "name": "acl", + "type": "text[]", + "primaryKey": false, + "notNull": false + }, + "binary": { + "name": "binary", + "type": "bit(1536)", + "primaryKey": false, + "notNull": false + }, + "binary_384": { + "name": "binary_384", + "type": "bit(384)", + "primaryKey": false, + "notNull": false + }, + "binary_768": { + "name": "binary_768", + "type": "bit(768)", + "primaryKey": false, + "notNull": false + }, + "binary_1024": { + "name": "binary_1024", + "type": "bit(1024)", + "primaryKey": false, + "notNull": false + }, + "binary_3072": { + "name": "binary_3072", + "type": "bit(3072)", + "primaryKey": false, + "notNull": false + }, + "vector": { + "name": "vector", + "type": "halfvec(1536)", + "primaryKey": false, + "notNull": false + }, + "vector_384": { + "name": "vector_384", + "type": "halfvec(384)", + "primaryKey": false, + "notNull": false + }, + "vector_512": { + "name": "vector_512", + "type": "halfvec(512)", + "primaryKey": false, + "notNull": false + }, + "vector_768": { + "name": "vector_768", + "type": "halfvec(768)", + "primaryKey": false, + "notNull": false + }, + "vector_1024": { + "name": "vector_1024", + "type": "halfvec(1024)", + "primaryKey": false, + "notNull": false + }, + "vector_3072": { + "name": "vector_3072", + "type": "halfvec(3072)", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "embedding_search_kb_idx": { + "name": "embedding_search_kb_idx", + "columns": [ + { + "expression": "knowledge_base_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "embedding_search_document_lookup_idx": { + "name": "embedding_search_document_lookup_idx", + "columns": [ + { + "expression": "document_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "knowledge_base_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"embedding_search\".\"enabled\"", + "concurrently": true, + "method": "btree", + "with": {} + }, + "embedding_search_cosine_hnsw_idx": { + "name": "embedding_search_cosine_hnsw_idx", + "columns": [ + { + "expression": "vector", + "isExpression": false, + "asc": true, + "nulls": "last", + "opclass": "halfvec_cosine_ops" + } + ], + "isUnique": false, + "concurrently": false, + "method": "hnsw", + "with": { + "m": 16, + "ef_construction": 64 + } + }, + "embedding_search_512_cosine_hnsw_idx": { + "name": "embedding_search_512_cosine_hnsw_idx", + "columns": [ + { + "expression": "vector_512", + "isExpression": false, + "asc": true, + "nulls": "last", + "opclass": "halfvec_cosine_ops" + } + ], + "isUnique": false, + "concurrently": false, + "method": "hnsw", + "with": { + "m": 16, + "ef_construction": 64 + } + }, + "embedding_search_384_cosine_hnsw_idx": { + "name": "embedding_search_384_cosine_hnsw_idx", + "columns": [ + { + "expression": "vector_384", + "isExpression": false, + "asc": true, + "nulls": "last", + "opclass": "halfvec_cosine_ops" + } + ], + "isUnique": false, + "concurrently": false, + "method": "hnsw", + "with": { + "m": 16, + "ef_construction": 64 + } + }, + "embedding_search_768_cosine_hnsw_idx": { + "name": "embedding_search_768_cosine_hnsw_idx", + "columns": [ + { + "expression": "vector_768", + "isExpression": false, + "asc": true, + "nulls": "last", + "opclass": "halfvec_cosine_ops" + } + ], + "isUnique": false, + "concurrently": false, + "method": "hnsw", + "with": { + "m": 16, + "ef_construction": 64 + } + }, + "embedding_search_1024_cosine_hnsw_idx": { + "name": "embedding_search_1024_cosine_hnsw_idx", + "columns": [ + { + "expression": "vector_1024", + "isExpression": false, + "asc": true, + "nulls": "last", + "opclass": "halfvec_cosine_ops" + } + ], + "isUnique": false, + "concurrently": false, + "method": "hnsw", + "with": { + "m": 16, + "ef_construction": 64 + } + }, + "embedding_search_3072_cosine_hnsw_idx": { + "name": "embedding_search_3072_cosine_hnsw_idx", + "columns": [ + { + "expression": "vector_3072", + "isExpression": false, + "asc": true, + "nulls": "last", + "opclass": "halfvec_cosine_ops" + } + ], + "isUnique": false, + "concurrently": false, + "method": "hnsw", + "with": { + "m": 16, + "ef_construction": 64 + } + } + }, + "foreignKeys": { + "embedding_search_id_embedding_id_fk": { + "name": "embedding_search_id_embedding_id_fk", + "tableFrom": "embedding_search", + "tableTo": "embedding", + "columnsFrom": ["id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "embedding_search_width_check": { + "name": "embedding_search_width_check", + "value": "num_nonnulls(\"binary\", \"binary_384\", \"binary_768\", \"binary_1024\", \"binary_3072\") = 1" + } + }, + "isRLSEnabled": false + }, + "public.embedding_secret_provenance": { + "name": "embedding_secret_provenance", + "schema": "", + "columns": { + "embedding_id": { + "name": "embedding_id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "content_hash": { + "name": "content_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "entries": { + "name": "entries", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "embedding_secret_provenance_embedding_id_embedding_id_fk": { + "name": "embedding_secret_provenance_embedding_id_embedding_id_fk", + "tableFrom": "embedding_secret_provenance", + "tableTo": "embedding", + "columnsFrom": ["embedding_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "embedding_secret_provenance_status_check": { + "name": "embedding_secret_provenance_status_check", + "value": "\"embedding_secret_provenance\".\"status\" IN ('exact', 'unknown')" + } + }, + "isRLSEnabled": false + }, + "public.environment": { + "name": "environment", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "variables": { + "name": "variables", + "type": "json", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "environment_user_id_user_id_fk": { + "name": "environment_user_id_user_id_fk", + "tableFrom": "environment", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "environment_user_id_unique": { + "name": "environment_user_id_unique", + "nullsNotDistinct": false, + "columns": ["user_id"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.execution_large_value_dependencies": { + "name": "execution_large_value_dependencies", + "schema": "", + "columns": { + "parent_key": { + "name": "parent_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "child_key": { + "name": "child_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "execution_large_value_dependencies_workspace_parent_key_idx": { + "name": "execution_large_value_dependencies_workspace_parent_key_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "parent_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "execution_large_value_dependencies_workspace_child_key_idx": { + "name": "execution_large_value_dependencies_workspace_child_key_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "child_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "execution_large_value_dependencies_workspace_id_workspace_id_fk": { + "name": "execution_large_value_dependencies_workspace_id_workspace_id_fk", + "tableFrom": "execution_large_value_dependencies", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "execution_large_value_dependencies_parent_key_child_key_pk": { + "name": "execution_large_value_dependencies_parent_key_child_key_pk", + "columns": ["parent_key", "child_key"] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.execution_large_value_references": { + "name": "execution_large_value_references", + "schema": "", + "columns": { + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "execution_id": { + "name": "execution_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "source": { + "name": "source", + "type": "execution_large_value_reference_source", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "execution_large_value_references_workspace_execution_source_idx": { + "name": "execution_large_value_references_workspace_execution_source_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "execution_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "source", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "execution_large_value_references_workflow_id_idx": { + "name": "execution_large_value_references_workflow_id_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "execution_large_value_references_workspace_id_workspace_id_fk": { + "name": "execution_large_value_references_workspace_id_workspace_id_fk", + "tableFrom": "execution_large_value_references", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "execution_large_value_references_workflow_id_workflow_id_fk": { + "name": "execution_large_value_references_workflow_id_workflow_id_fk", + "tableFrom": "execution_large_value_references", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "execution_large_value_references_key_execution_id_source_pk": { + "name": "execution_large_value_references_key_execution_id_source_pk", + "columns": ["key", "execution_id", "source"] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.execution_large_values": { + "name": "execution_large_values", + "schema": "", + "columns": { + "key": { + "name": "key", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "owner_execution_id": { + "name": "owner_execution_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "size": { + "name": "size", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "execution_large_values_owner_execution_id_idx": { + "name": "execution_large_values_owner_execution_id_idx", + "columns": [ + { + "expression": "owner_execution_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "execution_large_values_cleanup_idx": { + "name": "execution_large_values_cleanup_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"execution_large_values\".\"deleted_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "execution_large_values_tombstone_cleanup_idx": { + "name": "execution_large_values_tombstone_cleanup_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "deleted_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"execution_large_values\".\"deleted_at\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "execution_large_values_workflow_id_idx": { + "name": "execution_large_values_workflow_id_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "execution_large_values_workspace_id_workspace_id_fk": { + "name": "execution_large_values_workspace_id_workspace_id_fk", + "tableFrom": "execution_large_values", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "execution_large_values_workflow_id_workflow_id_fk": { + "name": "execution_large_values_workflow_id_workflow_id_fk", + "tableFrom": "execution_large_values", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.folder": { + "name": "folder", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "resource_type": { + "name": "resource_type", + "type": "folder_resource_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "parent_id": { + "name": "parent_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "locked": { + "name": "locked", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "sort_order": { + "name": "sort_order", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "folder_user_idx": { + "name": "folder_user_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "folder_workspace_resource_parent_idx": { + "name": "folder_workspace_resource_parent_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "resource_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "parent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "folder_parent_sort_idx": { + "name": "folder_parent_sort_idx", + "columns": [ + { + "expression": "parent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "sort_order", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "folder_deleted_at_idx": { + "name": "folder_deleted_at_idx", + "columns": [ + { + "expression": "deleted_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "folder_workspace_deleted_partial_idx": { + "name": "folder_workspace_deleted_partial_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "deleted_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"folder\".\"deleted_at\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "folder_workspace_resource_parent_name_active_unique": { + "name": "folder_workspace_resource_parent_name_active_unique", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "resource_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "coalesce(\"parent_id\", '')", + "asc": true, + "isExpression": true, + "nulls": "last" + }, + { + "expression": "name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"folder\".\"deleted_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "folder_user_id_user_id_fk": { + "name": "folder_user_id_user_id_fk", + "tableFrom": "folder", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "folder_workspace_id_workspace_id_fk": { + "name": "folder_workspace_id_workspace_id_fk", + "tableFrom": "folder", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "folder_parent_id_folder_id_fk": { + "name": "folder_parent_id_folder_id_fk", + "tableFrom": "folder", + "tableTo": "folder", + "columnsFrom": ["parent_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.idempotency_key": { + "name": "idempotency_key", + "schema": "", + "columns": { + "key": { + "name": "key", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "result": { + "name": "result", + "type": "json", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "idempotency_key_created_at_idx": { + "name": "idempotency_key_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.invitation": { + "name": "invitation", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "kind": { + "name": "kind", + "type": "invitation_kind", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'organization'" + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "inviter_id": { + "name": "inviter_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "membership_intent": { + "name": "membership_intent", + "type": "invitation_membership_intent", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'internal'" + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "invitation_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "invitation_email_idx": { + "name": "invitation_email_idx", + "columns": [ + { + "expression": "email", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "invitation_organization_id_idx": { + "name": "invitation_organization_id_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "invitation_status_idx": { + "name": "invitation_status_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "invitation_pending_email_org_unique": { + "name": "invitation_pending_email_org_unique", + "columns": [ + { + "expression": "email", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"invitation\".\"status\" = 'pending' AND \"invitation\".\"organization_id\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "invitation_inviter_id_user_id_fk": { + "name": "invitation_inviter_id_user_id_fk", + "tableFrom": "invitation", + "tableTo": "user", + "columnsFrom": ["inviter_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "invitation_organization_id_organization_id_fk": { + "name": "invitation_organization_id_organization_id_fk", + "tableFrom": "invitation", + "tableTo": "organization", + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "invitation_token_unique": { + "name": "invitation_token_unique", + "nullsNotDistinct": false, + "columns": ["token"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.invitation_workspace_grant": { + "name": "invitation_workspace_grant", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "invitation_id": { + "name": "invitation_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "permission": { + "name": "permission", + "type": "permission_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "invitation_workspace_grant_unique": { + "name": "invitation_workspace_grant_unique", + "columns": [ + { + "expression": "invitation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "invitation_workspace_grant_workspace_id_idx": { + "name": "invitation_workspace_grant_workspace_id_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "invitation_workspace_grant_invitation_id_invitation_id_fk": { + "name": "invitation_workspace_grant_invitation_id_invitation_id_fk", + "tableFrom": "invitation_workspace_grant", + "tableTo": "invitation", + "columnsFrom": ["invitation_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "invitation_workspace_grant_workspace_id_workspace_id_fk": { + "name": "invitation_workspace_grant_workspace_id_workspace_id_fk", + "tableFrom": "invitation_workspace_grant", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.job_execution_logs": { + "name": "job_execution_logs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "schedule_id": { + "name": "schedule_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "execution_id": { + "name": "execution_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "level": { + "name": "level", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'running'" + }, + "trigger": { + "name": "trigger", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "started_at": { + "name": "started_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "ended_at": { + "name": "ended_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "total_duration_ms": { + "name": "total_duration_ms", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "execution_data": { + "name": "execution_data", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "cost": { + "name": "cost", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "job_execution_logs_schedule_id_idx": { + "name": "job_execution_logs_schedule_id_idx", + "columns": [ + { + "expression": "schedule_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "job_execution_logs_workspace_started_at_idx": { + "name": "job_execution_logs_workspace_started_at_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "started_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "job_execution_logs_workspace_ended_at_id_idx": { + "name": "job_execution_logs_workspace_ended_at_id_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "date_trunc('milliseconds', \"ended_at\")", + "asc": true, + "isExpression": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "job_execution_logs_execution_id_unique": { + "name": "job_execution_logs_execution_id_unique", + "columns": [ + { + "expression": "execution_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "job_execution_logs_trigger_idx": { + "name": "job_execution_logs_trigger_idx", + "columns": [ + { + "expression": "trigger", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "job_execution_logs_schedule_id_workflow_schedule_id_fk": { + "name": "job_execution_logs_schedule_id_workflow_schedule_id_fk", + "tableFrom": "job_execution_logs", + "tableTo": "workflow_schedule", + "columnsFrom": ["schedule_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + }, + "job_execution_logs_workspace_id_workspace_id_fk": { + "name": "job_execution_logs_workspace_id_workspace_id_fk", + "tableFrom": "job_execution_logs", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.knowledge_base": { + "name": "knowledge_base", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "folder_id": { + "name": "folder_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "is_search_index": { + "name": "is_search_index", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "token_count": { + "name": "token_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "embedding_model": { + "name": "embedding_model", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'text-embedding-3-small'" + }, + "embedding_dimension": { + "name": "embedding_dimension", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1536 + }, + "chunking_config": { + "name": "chunking_config", + "type": "json", + "primaryKey": false, + "notNull": true, + "default": "'{\"maxSize\": 1024, \"minSize\": 1, \"overlap\": 200}'" + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "kb_organization_id_idx": { + "name": "kb_organization_id_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "kb_organization_search_index_unique": { + "name": "kb_organization_search_index_unique", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"knowledge_base\".\"is_search_index\" = true AND \"knowledge_base\".\"deleted_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "kb_organization_name_active_unique": { + "name": "kb_organization_name_active_unique", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"knowledge_base\".\"deleted_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "kb_user_id_idx": { + "name": "kb_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "kb_workspace_id_idx": { + "name": "kb_workspace_id_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "kb_user_workspace_idx": { + "name": "kb_user_workspace_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "kb_folder_id_idx": { + "name": "kb_folder_id_idx", + "columns": [ + { + "expression": "folder_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "kb_deleted_at_idx": { + "name": "kb_deleted_at_idx", + "columns": [ + { + "expression": "deleted_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "kb_workspace_deleted_partial_idx": { + "name": "kb_workspace_deleted_partial_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "deleted_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"knowledge_base\".\"deleted_at\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "kb_workspace_name_active_unique": { + "name": "kb_workspace_name_active_unique", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"knowledge_base\".\"deleted_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "kb_workspace_search_index_unique": { + "name": "kb_workspace_search_index_unique", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"knowledge_base\".\"is_search_index\" = true AND \"knowledge_base\".\"deleted_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "knowledge_base_user_id_user_id_fk": { + "name": "knowledge_base_user_id_user_id_fk", + "tableFrom": "knowledge_base", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "knowledge_base_workspace_id_workspace_id_fk": { + "name": "knowledge_base_workspace_id_workspace_id_fk", + "tableFrom": "knowledge_base", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "knowledge_base_organization_id_organization_id_fk": { + "name": "knowledge_base_organization_id_organization_id_fk", + "tableFrom": "knowledge_base", + "tableTo": "organization", + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "knowledge_base_folder_id_folder_id_fk": { + "name": "knowledge_base_folder_id_folder_id_fk", + "tableFrom": "knowledge_base", + "tableTo": "folder", + "columnsFrom": ["folder_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "kb_owner_check": { + "name": "kb_owner_check", + "value": "num_nonnulls(\"knowledge_base\".\"workspace_id\", \"knowledge_base\".\"organization_id\") = 1" + }, + "kb_organization_search_index_check": { + "name": "kb_organization_search_index_check", + "value": "\"knowledge_base\".\"organization_id\" IS NULL OR \"knowledge_base\".\"is_search_index\"" + }, + "kb_organization_folder_check": { + "name": "kb_organization_folder_check", + "value": "\"knowledge_base\".\"organization_id\" IS NULL OR \"knowledge_base\".\"folder_id\" IS NULL" + } + }, + "isRLSEnabled": false + }, + "public.knowledge_base_tag_definitions": { + "name": "knowledge_base_tag_definitions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "knowledge_base_id": { + "name": "knowledge_base_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "tag_slot": { + "name": "tag_slot", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "display_name": { + "name": "display_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "field_type": { + "name": "field_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'text'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "kb_tag_definitions_kb_slot_idx": { + "name": "kb_tag_definitions_kb_slot_idx", + "columns": [ + { + "expression": "knowledge_base_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "tag_slot", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "kb_tag_definitions_kb_display_name_idx": { + "name": "kb_tag_definitions_kb_display_name_idx", + "columns": [ + { + "expression": "knowledge_base_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "display_name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "kb_tag_definitions_kb_id_idx": { + "name": "kb_tag_definitions_kb_id_idx", + "columns": [ + { + "expression": "knowledge_base_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "knowledge_base_tag_definitions_knowledge_base_id_knowledge_base_id_fk": { + "name": "knowledge_base_tag_definitions_knowledge_base_id_knowledge_base_id_fk", + "tableFrom": "knowledge_base_tag_definitions", + "tableTo": "knowledge_base", + "columnsFrom": ["knowledge_base_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.knowledge_connector": { + "name": "knowledge_connector", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "knowledge_base_id": { + "name": "knowledge_base_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "connector_type": { + "name": "connector_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "credential_id": { + "name": "credential_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "encrypted_api_key": { + "name": "encrypted_api_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "source_config": { + "name": "source_config", + "type": "json", + "primaryKey": false, + "notNull": true + }, + "sync_mode": { + "name": "sync_mode", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'full'" + }, + "sync_interval_minutes": { + "name": "sync_interval_minutes", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1440 + }, + "access_mode": { + "name": "access_mode", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'workspace'" + }, + "credential_group_id": { + "name": "credential_group_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "credential_group_option_id": { + "name": "credential_group_option_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "member_sync_status": { + "name": "member_sync_status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'idle'" + }, + "member_sync_lock_token": { + "name": "member_sync_lock_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "member_sync_lock_lease_at": { + "name": "member_sync_lock_lease_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "next_member_sync_at": { + "name": "next_member_sync_at", + "type": "timestamp (3)", + "primaryKey": false, + "notNull": false + }, + "last_member_sync_at": { + "name": "last_member_sync_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "last_member_sync_error": { + "name": "last_member_sync_error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "member_sync_consecutive_failures": { + "name": "member_sync_consecutive_failures", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "access_rewrite_pending": { + "name": "access_rewrite_pending", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "member_tombstone_cursor": { + "name": "member_tombstone_cursor", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "member_resurrection_cursor": { + "name": "member_resurrection_cursor", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'active'" + }, + "last_sync_at": { + "name": "last_sync_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "last_sync_error": { + "name": "last_sync_error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "last_sync_doc_count": { + "name": "last_sync_doc_count", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "listing_checkpoint": { + "name": "listing_checkpoint", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "directory_checkpoint": { + "name": "directory_checkpoint", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "next_sync_at": { + "name": "next_sync_at", + "type": "timestamp (3)", + "primaryKey": false, + "notNull": false + }, + "next_directory_sync_at": { + "name": "next_directory_sync_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "consecutive_failures": { + "name": "consecutive_failures", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "sync_lock_token": { + "name": "sync_lock_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "sync_lock_lease_at": { + "name": "sync_lock_lease_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "archived_at": { + "name": "archived_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "detached_at": { + "name": "detached_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "detach_reserved_bytes": { + "name": "detach_reserved_bytes", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "default": 0 + } + }, + "indexes": { + "kc_knowledge_base_id_idx": { + "name": "kc_knowledge_base_id_idx", + "columns": [ + { + "expression": "knowledge_base_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "kc_status_next_sync_idx": { + "name": "kc_status_next_sync_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "next_sync_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "kc_archived_at_partial_idx": { + "name": "kc_archived_at_partial_idx", + "columns": [ + { + "expression": "archived_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"knowledge_connector\".\"archived_at\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "kc_deleted_at_partial_idx": { + "name": "kc_deleted_at_partial_idx", + "columns": [ + { + "expression": "deleted_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"knowledge_connector\".\"deleted_at\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "kc_member_sync_due_idx": { + "name": "kc_member_sync_due_idx", + "columns": [ + { + "expression": "member_sync_status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "next_member_sync_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"knowledge_connector\".\"access_mode\" = 'members' AND \"knowledge_connector\".\"deleted_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "kc_directory_sync_due_idx": { + "name": "kc_directory_sync_due_idx", + "columns": [ + { + "expression": "next_directory_sync_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"knowledge_connector\".\"access_mode\" = 'admin' AND \"knowledge_connector\".\"deleted_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "knowledge_connector_knowledge_base_id_knowledge_base_id_fk": { + "name": "knowledge_connector_knowledge_base_id_knowledge_base_id_fk", + "tableFrom": "knowledge_connector", + "tableTo": "knowledge_base", + "columnsFrom": ["knowledge_base_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "knowledge_connector_credential_group_id_credential_group_id_fk": { + "name": "knowledge_connector_credential_group_id_credential_group_id_fk", + "tableFrom": "knowledge_connector", + "tableTo": "credential_group", + "columnsFrom": ["credential_group_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "kc_access_mode_check": { + "name": "kc_access_mode_check", + "value": "\"knowledge_connector\".\"access_mode\" IN ('workspace', 'members', 'admin')" + }, + "kc_member_sync_status_check": { + "name": "kc_member_sync_status_check", + "value": "\"knowledge_connector\".\"member_sync_status\" IN ('idle', 'pending', 'running', 'error', 'disabled')" + }, + "kc_sync_lock_exclusive_check": { + "name": "kc_sync_lock_exclusive_check", + "value": "NOT (\"knowledge_connector\".\"sync_lock_token\" IS NOT NULL AND \"knowledge_connector\".\"member_sync_lock_token\" IS NOT NULL)" + } + }, + "isRLSEnabled": false + }, + "public.knowledge_connector_member": { + "name": "knowledge_connector_member", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "connector_id": { + "name": "connector_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "credential_id": { + "name": "credential_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "subject_token": { + "name": "subject_token", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'active'" + }, + "consecutive_failures": { + "name": "consecutive_failures", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "next_attempt_at": { + "name": "next_attempt_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "last_started_at": { + "name": "last_started_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "last_complete_listing_at": { + "name": "last_complete_listing_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "last_listed_count": { + "name": "last_listed_count", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "last_error": { + "name": "last_error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "member_synced_through": { + "name": "member_synced_through", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "scope_renewed_at": { + "name": "scope_renewed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "scope_renewal_cursor": { + "name": "scope_renewal_cursor", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "scope_renewal_started_at": { + "name": "scope_renewal_started_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "change_cursor": { + "name": "change_cursor", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "listing_checkpoint": { + "name": "listing_checkpoint", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "suspended_at": { + "name": "suspended_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "kcm_organization_id_idx": { + "name": "kcm_organization_id_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "kcm_connector_credential_unique": { + "name": "kcm_connector_credential_unique", + "columns": [ + { + "expression": "connector_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "credential_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "kcm_connector_queue_idx": { + "name": "kcm_connector_queue_idx", + "columns": [ + { + "expression": "connector_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "next_attempt_at", + "isExpression": false, + "asc": true, + "nulls": "first" + }, + { + "expression": "last_started_at", + "isExpression": false, + "asc": true, + "nulls": "first" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "kcm_credential_idx": { + "name": "kcm_credential_idx", + "columns": [ + { + "expression": "credential_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "knowledge_connector_member_workspace_id_workspace_id_fk": { + "name": "knowledge_connector_member_workspace_id_workspace_id_fk", + "tableFrom": "knowledge_connector_member", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "knowledge_connector_member_organization_id_organization_id_fk": { + "name": "knowledge_connector_member_organization_id_organization_id_fk", + "tableFrom": "knowledge_connector_member", + "tableTo": "organization", + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "knowledge_connector_member_connector_id_knowledge_connector_id_fk": { + "name": "knowledge_connector_member_connector_id_knowledge_connector_id_fk", + "tableFrom": "knowledge_connector_member", + "tableTo": "knowledge_connector", + "columnsFrom": ["connector_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "knowledge_connector_member_credential_id_credential_id_fk": { + "name": "knowledge_connector_member_credential_id_credential_id_fk", + "tableFrom": "knowledge_connector_member", + "tableTo": "credential", + "columnsFrom": ["credential_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "kcm_owner_check": { + "name": "kcm_owner_check", + "value": "num_nonnulls(\"knowledge_connector_member\".\"workspace_id\", \"knowledge_connector_member\".\"organization_id\") = 1" + }, + "kcm_status_check": { + "name": "kcm_status_check", + "value": "\"knowledge_connector_member\".\"status\" IN ('active', 'suspended', 'disabled')" + }, + "kcm_subject_token_shape_check": { + "name": "kcm_subject_token_shape_check", + "value": "\"knowledge_connector_member\".\"subject_token\" ~ '^s:[^:]+:[^:]+:.+$'" + } + }, + "isRLSEnabled": false + }, + "public.knowledge_connector_member_sync_log": { + "name": "knowledge_connector_member_sync_log", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "connector_id": { + "name": "connector_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "started_at": { + "name": "started_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "members_claimed": { + "name": "members_claimed", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "members_completed": { + "name": "members_completed", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "members_incomplete": { + "name": "members_incomplete", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "members_failed": { + "name": "members_failed", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "docs_failed": { + "name": "docs_failed", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "processing_dispatch_failed": { + "name": "processing_dispatch_failed", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "docs_listed": { + "name": "docs_listed", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "docs_added": { + "name": "docs_added", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "docs_updated": { + "name": "docs_updated", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "docs_unchanged": { + "name": "docs_unchanged", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "docs_hydrated_once": { + "name": "docs_hydrated_once", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "observations_added": { + "name": "observations_added", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "observations_renewed": { + "name": "observations_renewed", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "observations_removed": { + "name": "observations_removed", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "docs_tombstoned": { + "name": "docs_tombstoned", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "docs_resurrected": { + "name": "docs_resurrected", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "docs_purged": { + "name": "docs_purged", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "credentials_audited": { + "name": "credentials_audited", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "error_message": { + "name": "error_message", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "database_failure_class": { + "name": "database_failure_class", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "kcmsl_connector_started_at_idx": { + "name": "kcmsl_connector_started_at_idx", + "columns": [ + { + "expression": "connector_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "\"started_at\" DESC", + "asc": true, + "isExpression": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "kcmsl_started_at_partial_idx": { + "name": "kcmsl_started_at_partial_idx", + "columns": [ + { + "expression": "started_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"knowledge_connector_member_sync_log\".\"status\" = 'started'", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "knowledge_connector_member_sync_log_connector_id_knowledge_connector_id_fk": { + "name": "knowledge_connector_member_sync_log_connector_id_knowledge_connector_id_fk", + "tableFrom": "knowledge_connector_member_sync_log", + "tableTo": "knowledge_connector", + "columnsFrom": ["connector_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "kcmsl_status_check": { + "name": "kcmsl_status_check", + "value": "\"knowledge_connector_member_sync_log\".\"status\" IN ('started', 'partial', 'completed', 'failed')" + }, + "kcmsl_database_failure_class_check": { + "name": "kcmsl_database_failure_class_check", + "value": "\"knowledge_connector_member_sync_log\".\"database_failure_class\" IN ('capacity', 'conflict', 'connection')" + } + }, + "isRLSEnabled": false + }, + "public.knowledge_connector_partition": { + "name": "knowledge_connector_partition", + "schema": "", + "columns": { + "connector_id": { + "name": "connector_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "partition_key": { + "name": "partition_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "generation_id": { + "name": "generation_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "context": { + "name": "context", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "cursor": { + "name": "cursor", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "attempts": { + "name": "attempts", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "retry_at": { + "name": "retry_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "last_served_at": { + "name": "last_served_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "failure": { + "name": "failure", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "permission_cursor": { + "name": "permission_cursor", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "permission_attempts": { + "name": "permission_attempts", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "permission_retry_at": { + "name": "permission_retry_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "permission_last_served_at": { + "name": "permission_last_served_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "permission_started_at": { + "name": "permission_started_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "permission_failure": { + "name": "permission_failure", + "type": "jsonb", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "kcp_content_due_idx": { + "name": "kcp_content_due_idx", + "columns": [ + { + "expression": "connector_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "generation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "retry_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "last_served_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "kcp_permission_due_idx": { + "name": "kcp_permission_due_idx", + "columns": [ + { + "expression": "connector_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "generation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "permission_retry_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "permission_last_served_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "knowledge_connector_partition_connector_id_knowledge_connector_id_fk": { + "name": "knowledge_connector_partition_connector_id_knowledge_connector_id_fk", + "tableFrom": "knowledge_connector_partition", + "tableTo": "knowledge_connector", + "columnsFrom": ["connector_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "kcp_pk": { + "name": "kcp_pk", + "columns": ["connector_id", "partition_key"] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "kcp_partition_key_check": { + "name": "kcp_partition_key_check", + "value": "octet_length(\"knowledge_connector_partition\".\"partition_key\") BETWEEN 1 AND 1024" + }, + "kcp_context_check": { + "name": "kcp_context_check", + "value": "jsonb_typeof(\"knowledge_connector_partition\".\"context\") = 'object' AND octet_length(\"knowledge_connector_partition\".\"context\"::text) <= 16384" + }, + "kcp_status_check": { + "name": "kcp_status_check", + "value": "\"knowledge_connector_partition\".\"status\" IN ('pending', 'complete', 'blocked')" + }, + "kcp_cursor_check": { + "name": "kcp_cursor_check", + "value": "(\"knowledge_connector_partition\".\"cursor\" IS NULL OR octet_length(\"knowledge_connector_partition\".\"cursor\") <= 393216) AND (\"knowledge_connector_partition\".\"permission_cursor\" IS NULL OR octet_length(\"knowledge_connector_partition\".\"permission_cursor\") <= 393216)" + }, + "kcp_attempts_check": { + "name": "kcp_attempts_check", + "value": "\"knowledge_connector_partition\".\"attempts\" >= 0 AND \"knowledge_connector_partition\".\"permission_attempts\" >= 0" + } + }, + "isRLSEnabled": false + }, + "public.knowledge_connector_permission_grant": { + "name": "knowledge_connector_permission_grant", + "schema": "", + "columns": { + "connector_id": { + "name": "connector_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "group_key": { + "name": "group_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "subject_token": { + "name": "subject_token", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "kcpg_subject_idx": { + "name": "kcpg_subject_idx", + "columns": [ + { + "expression": "subject_token", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "connector_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "group_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "kcpg_snapshot_fk": { + "name": "kcpg_snapshot_fk", + "tableFrom": "knowledge_connector_permission_grant", + "tableTo": "knowledge_connector_permission_snapshot", + "columnsFrom": ["connector_id"], + "columnsTo": ["connector_id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "kcpg_pk": { + "name": "kcpg_pk", + "columns": ["connector_id", "group_key", "subject_token"] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "kcpg_group_check": { + "name": "kcpg_group_check", + "value": "length(\"knowledge_connector_permission_grant\".\"group_key\") BETWEEN 1 AND 255" + }, + "kcpg_subject_check": { + "name": "kcpg_subject_check", + "value": "\"knowledge_connector_permission_grant\".\"subject_token\" ~ '^u:[^[:space:]A-Z]+@[^[:space:]A-Z]+$'" + } + }, + "isRLSEnabled": false + }, + "public.knowledge_connector_permission_snapshot": { + "name": "knowledge_connector_permission_snapshot", + "schema": "", + "columns": { + "connector_id": { + "name": "connector_id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "revision": { + "name": "revision", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "payload": { + "name": "payload", + "type": "jsonb", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "kcps_connector_fk": { + "name": "kcps_connector_fk", + "tableFrom": "knowledge_connector_permission_snapshot", + "tableTo": "knowledge_connector", + "columnsFrom": ["connector_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "kcps_revision_check": { + "name": "kcps_revision_check", + "value": "\"knowledge_connector_permission_snapshot\".\"revision\" > 0" + } + }, + "isRLSEnabled": false + }, + "public.knowledge_connector_sync_log": { + "name": "knowledge_connector_sync_log", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "connector_id": { + "name": "connector_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "started_at": { + "name": "started_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "docs_added": { + "name": "docs_added", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "docs_updated": { + "name": "docs_updated", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "docs_deleted": { + "name": "docs_deleted", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "docs_unchanged": { + "name": "docs_unchanged", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "docs_skipped": { + "name": "docs_skipped", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "docs_failed": { + "name": "docs_failed", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "listed_count": { + "name": "listed_count", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "error_message": { + "name": "error_message", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "database_failure_class": { + "name": "database_failure_class", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "kcsl_connector_started_at_idx": { + "name": "kcsl_connector_started_at_idx", + "columns": [ + { + "expression": "connector_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "\"started_at\" DESC", + "asc": true, + "isExpression": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "kcsl_started_at_partial_idx": { + "name": "kcsl_started_at_partial_idx", + "columns": [ + { + "expression": "started_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"knowledge_connector_sync_log\".\"status\" = 'started'", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "knowledge_connector_sync_log_connector_id_knowledge_connector_id_fk": { + "name": "knowledge_connector_sync_log_connector_id_knowledge_connector_id_fk", + "tableFrom": "knowledge_connector_sync_log", + "tableTo": "knowledge_connector", + "columnsFrom": ["connector_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "kcsl_database_failure_class_check": { + "name": "kcsl_database_failure_class_check", + "value": "\"knowledge_connector_sync_log\".\"database_failure_class\" IN ('capacity', 'conflict', 'connection')" + } + }, + "isRLSEnabled": false + }, + "public.knowledge_document_observation": { + "name": "knowledge_document_observation", + "schema": "", + "columns": { + "document_id": { + "name": "document_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "member_id": { + "name": "member_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "last_seen_at": { + "name": "last_seen_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "run_id": { + "name": "run_id", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "kdo_member_idx": { + "name": "kdo_member_idx", + "columns": [ + { + "expression": "member_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "knowledge_document_observation_document_id_document_id_fk": { + "name": "knowledge_document_observation_document_id_document_id_fk", + "tableFrom": "knowledge_document_observation", + "tableTo": "document", + "columnsFrom": ["document_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "knowledge_document_observation_member_id_knowledge_connector_member_id_fk": { + "name": "knowledge_document_observation_member_id_knowledge_connector_member_id_fk", + "tableFrom": "knowledge_document_observation", + "tableTo": "knowledge_connector_member", + "columnsFrom": ["member_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "knowledge_document_observation_document_id_member_id_pk": { + "name": "knowledge_document_observation_document_id_member_id_pk", + "columns": ["document_id", "member_id"] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.knowledge_external_directory": { + "name": "knowledge_external_directory", + "schema": "", + "columns": { + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "provider_id": { + "name": "provider_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "sync_lock_token": { + "name": "sync_lock_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "sync_lock_lease_at": { + "name": "sync_lock_lease_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "last_started_at": { + "name": "last_started_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "last_complete_sync_at": { + "name": "last_complete_sync_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "ked_organization_id_idx": { + "name": "ked_organization_id_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "ked_workspace_identity_unique": { + "name": "ked_workspace_identity_unique", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "provider_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "tenant_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "ked_organization_identity_unique": { + "name": "ked_organization_identity_unique", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "provider_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "tenant_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "knowledge_external_directory_workspace_id_workspace_id_fk": { + "name": "knowledge_external_directory_workspace_id_workspace_id_fk", + "tableFrom": "knowledge_external_directory", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "knowledge_external_directory_organization_id_organization_id_fk": { + "name": "knowledge_external_directory_organization_id_organization_id_fk", + "tableFrom": "knowledge_external_directory", + "tableTo": "organization", + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "ked_owner_check": { + "name": "ked_owner_check", + "value": "num_nonnulls(\"knowledge_external_directory\".\"workspace_id\", \"knowledge_external_directory\".\"organization_id\") = 1" + } + }, + "isRLSEnabled": false + }, + "public.knowledge_external_group": { + "name": "knowledge_external_group", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "provider_id": { + "name": "provider_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "external_group_id": { + "name": "external_group_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "last_synced_at": { + "name": "last_synced_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "keg_organization_id_idx": { + "name": "keg_organization_id_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "keg_organization_identity_unique": { + "name": "keg_organization_identity_unique", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "provider_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "tenant_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "external_group_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "keg_organization_synced_idx": { + "name": "keg_organization_synced_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "last_synced_at", + "isExpression": false, + "asc": true, + "nulls": "first" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "keg_identity_unique": { + "name": "keg_identity_unique", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "provider_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "tenant_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "external_group_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "keg_workspace_synced_idx": { + "name": "keg_workspace_synced_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "last_synced_at", + "isExpression": false, + "asc": true, + "nulls": "first" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "knowledge_external_group_organization_id_organization_id_fk": { + "name": "knowledge_external_group_organization_id_organization_id_fk", + "tableFrom": "knowledge_external_group", + "tableTo": "organization", + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "keg_workspace_fk": { + "name": "keg_workspace_fk", + "tableFrom": "knowledge_external_group", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "keg_owner_check": { + "name": "keg_owner_check", + "value": "num_nonnulls(\"knowledge_external_group\".\"workspace_id\", \"knowledge_external_group\".\"organization_id\") = 1" + } + }, + "isRLSEnabled": false + }, + "public.knowledge_external_group_member": { + "name": "knowledge_external_group_member", + "schema": "", + "columns": { + "group_id": { + "name": "group_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "subject_token": { + "name": "subject_token", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "kegm_subject_token_idx": { + "name": "kegm_subject_token_idx", + "columns": [ + { + "expression": "subject_token", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "kegm_group_fk": { + "name": "kegm_group_fk", + "tableFrom": "knowledge_external_group_member", + "tableTo": "knowledge_external_group", + "columnsFrom": ["group_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "knowledge_external_group_member_group_id_subject_token_pk": { + "name": "knowledge_external_group_member_group_id_subject_token_pk", + "columns": ["group_id", "subject_token"] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.knowledge_projection_dirty": { + "name": "knowledge_projection_dirty", + "schema": "", + "columns": { + "document_id": { + "name": "document_id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "generation": { + "name": "generation", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "content": { + "name": "content", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "marked_at": { + "name": "marked_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "knowledge_projection_dirty_marked_at_idx": { + "name": "knowledge_projection_dirty_marked_at_idx", + "columns": [ + { + "expression": "marked_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "knowledge_projection_dirty_document_id_document_id_fk": { + "name": "knowledge_projection_dirty_document_id_document_id_fk", + "tableFrom": "knowledge_projection_dirty", + "tableTo": "document", + "columnsFrom": ["document_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.mcp_server_oauth": { + "name": "mcp_server_oauth", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "mcp_server_id": { + "name": "mcp_server_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "client_information": { + "name": "client_information", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tokens": { + "name": "tokens", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "code_verifier": { + "name": "code_verifier", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "state": { + "name": "state", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "state_created_at": { + "name": "state_created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "last_refreshed_at": { + "name": "last_refreshed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "mcp_server_oauth_server_unique": { + "name": "mcp_server_oauth_server_unique", + "columns": [ + { + "expression": "mcp_server_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "mcp_server_oauth_state_idx": { + "name": "mcp_server_oauth_state_idx", + "columns": [ + { + "expression": "state", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "mcp_server_oauth_mcp_server_id_mcp_servers_id_fk": { + "name": "mcp_server_oauth_mcp_server_id_mcp_servers_id_fk", + "tableFrom": "mcp_server_oauth", + "tableTo": "mcp_servers", + "columnsFrom": ["mcp_server_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "mcp_server_oauth_user_id_user_id_fk": { + "name": "mcp_server_oauth_user_id_user_id_fk", + "tableFrom": "mcp_server_oauth", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + }, + "mcp_server_oauth_workspace_id_workspace_id_fk": { + "name": "mcp_server_oauth_workspace_id_workspace_id_fk", + "tableFrom": "mcp_server_oauth", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "mcp_server_oauth_organization_id_organization_id_fk": { + "name": "mcp_server_oauth_organization_id_organization_id_fk", + "tableFrom": "mcp_server_oauth", + "tableTo": "organization", + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "mcp_server_oauth_owner_check": { + "name": "mcp_server_oauth_owner_check", + "value": "num_nonnulls(\"mcp_server_oauth\".\"workspace_id\", \"mcp_server_oauth\".\"organization_id\") = 1" + } + }, + "isRLSEnabled": false + }, + "public.mcp_servers": { + "name": "mcp_servers", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "credential_group_id": { + "name": "credential_group_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "managed_connector_id": { + "name": "managed_connector_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "oauth_config_version": { + "name": "oauth_config_version", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "transport": { + "name": "transport", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "url": { + "name": "url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "auth_type": { + "name": "auth_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'headers'" + }, + "oauth_client_id": { + "name": "oauth_client_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "oauth_client_secret": { + "name": "oauth_client_secret", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "headers": { + "name": "headers", + "type": "json", + "primaryKey": false, + "notNull": false, + "default": "'{}'" + }, + "timeout": { + "name": "timeout", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 30000 + }, + "retries": { + "name": "retries", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 3 + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "last_connected": { + "name": "last_connected", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "connection_status": { + "name": "connection_status", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'disconnected'" + }, + "last_error": { + "name": "last_error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status_config": { + "name": "status_config", + "type": "jsonb", + "primaryKey": false, + "notNull": false, + "default": "'{}'" + }, + "tool_count": { + "name": "tool_count", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 0 + }, + "last_tools_refresh": { + "name": "last_tools_refresh", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "total_requests": { + "name": "total_requests", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 0 + }, + "last_used": { + "name": "last_used", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "mcp_servers_organization_id_idx": { + "name": "mcp_servers_organization_id_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "mcp_servers_workspace_enabled_idx": { + "name": "mcp_servers_workspace_enabled_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "enabled", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "mcp_servers_credential_group_idx": { + "name": "mcp_servers_credential_group_idx", + "columns": [ + { + "expression": "credential_group_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "mcp_servers_credential_group_managed_connector_unique": { + "name": "mcp_servers_credential_group_managed_connector_unique", + "columns": [ + { + "expression": "credential_group_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "managed_connector_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"mcp_servers\".\"credential_group_id\" IS NOT NULL AND \"mcp_servers\".\"managed_connector_id\" IS NOT NULL AND \"mcp_servers\".\"deleted_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "mcp_servers_workspace_deleted_partial_idx": { + "name": "mcp_servers_workspace_deleted_partial_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "deleted_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"mcp_servers\".\"deleted_at\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "mcp_servers_workspace_id_workspace_id_fk": { + "name": "mcp_servers_workspace_id_workspace_id_fk", + "tableFrom": "mcp_servers", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "mcp_servers_organization_id_organization_id_fk": { + "name": "mcp_servers_organization_id_organization_id_fk", + "tableFrom": "mcp_servers", + "tableTo": "organization", + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "mcp_servers_credential_group_id_credential_group_id_fk": { + "name": "mcp_servers_credential_group_id_credential_group_id_fk", + "tableFrom": "mcp_servers", + "tableTo": "credential_group", + "columnsFrom": ["credential_group_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + }, + "mcp_servers_created_by_user_id_fk": { + "name": "mcp_servers_created_by_user_id_fk", + "tableFrom": "mcp_servers", + "tableTo": "user", + "columnsFrom": ["created_by"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "mcp_servers_owner_check": { + "name": "mcp_servers_owner_check", + "value": "num_nonnulls(\"mcp_servers\".\"workspace_id\", \"mcp_servers\".\"organization_id\") = 1" + }, + "mcp_servers_organization_managed_check": { + "name": "mcp_servers_organization_managed_check", + "value": "\"mcp_servers\".\"organization_id\" IS NULL OR \"mcp_servers\".\"credential_group_id\" IS NOT NULL" + }, + "mcp_servers_credential_group_managed_connector_check": { + "name": "mcp_servers_credential_group_managed_connector_check", + "value": "\"mcp_servers\".\"credential_group_id\" IS NULL OR \"mcp_servers\".\"managed_connector_id\" IS NOT NULL" + }, + "mcp_servers_managed_connector_oauth_check": { + "name": "mcp_servers_managed_connector_oauth_check", + "value": "\"mcp_servers\".\"managed_connector_id\" IS NULL OR \"mcp_servers\".\"auth_type\" = 'oauth'" + } + }, + "isRLSEnabled": false + }, + "public.member": { + "name": "member", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "member_user_id_unique": { + "name": "member_user_id_unique", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "member_organization_id_idx": { + "name": "member_organization_id_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "member_user_id_user_id_fk": { + "name": "member_user_id_user_id_fk", + "tableFrom": "member", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "member_organization_id_organization_id_fk": { + "name": "member_organization_id_organization_id_fk", + "tableFrom": "member", + "tableTo": "organization", + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.memory": { + "name": "memory", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "data": { + "name": "data", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "storage_version": { + "name": "storage_version", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "encrypted_context_summary": { + "name": "encrypted_context_summary", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "secret_provenance_version": { + "name": "secret_provenance_version", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "memory_key_idx": { + "name": "memory_key_idx", + "columns": [ + { + "expression": "key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "memory_workspace_idx": { + "name": "memory_workspace_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "memory_workspace_key_idx": { + "name": "memory_workspace_key_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "memory_workspace_deleted_partial_idx": { + "name": "memory_workspace_deleted_partial_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "deleted_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"memory\".\"deleted_at\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "memory_workspace_id_workspace_id_fk": { + "name": "memory_workspace_id_workspace_id_fk", + "tableFrom": "memory", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.memory_artifact": { + "name": "memory_artifact", + "schema": "", + "columns": { + "memory_id": { + "name": "memory_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "memory_artifact_key_idx": { + "name": "memory_artifact_key_idx", + "columns": [ + { + "expression": "key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "memory_artifact_memory_id_memory_id_fk": { + "name": "memory_artifact_memory_id_memory_id_fk", + "tableFrom": "memory_artifact", + "tableTo": "memory", + "columnsFrom": ["memory_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "memory_artifact_key_execution_large_values_key_fk": { + "name": "memory_artifact_key_execution_large_values_key_fk", + "tableFrom": "memory_artifact", + "tableTo": "execution_large_values", + "columnsFrom": ["key"], + "columnsTo": ["key"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "memory_artifact_memory_id_key_pk": { + "name": "memory_artifact_memory_id_key_pk", + "columns": ["memory_id", "key"] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.memory_item": { + "name": "memory_item", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "memory_id": { + "name": "memory_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "sequence": { + "name": "sequence", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "identity": { + "type": "always", + "name": "memory_item_sequence_seq", + "schema": "public", + "increment": "1", + "startWith": "1", + "minValue": "1", + "maxValue": "9223372036854775807", + "cache": "1", + "cycle": false + } + }, + "append_key": { + "name": "append_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "turn_id": { + "name": "turn_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "kind": { + "name": "kind", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "data": { + "name": "data", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "content_hash": { + "name": "content_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provenance_status": { + "name": "provenance_status", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provenance_entries": { + "name": "provenance_entries", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "memory_item_append_unique": { + "name": "memory_item_append_unique", + "columns": [ + { + "expression": "memory_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "append_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "memory_item_sequence_idx": { + "name": "memory_item_sequence_idx", + "columns": [ + { + "expression": "memory_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "sequence", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "memory_item_memory_id_memory_id_fk": { + "name": "memory_item_memory_id_memory_id_fk", + "tableFrom": "memory_item", + "tableTo": "memory", + "columnsFrom": ["memory_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "memory_item_kind_check": { + "name": "memory_item_kind_check", + "value": "\"memory_item\".\"kind\" IN ('message', 'exchange')" + }, + "memory_item_provenance_status_check": { + "name": "memory_item_provenance_status_check", + "value": "\"memory_item\".\"provenance_status\" IN ('exact', 'unknown')" + } + }, + "isRLSEnabled": false + }, + "public.memory_secret_provenance": { + "name": "memory_secret_provenance", + "schema": "", + "columns": { + "memory_id": { + "name": "memory_id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "content_hash": { + "name": "content_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "entries": { + "name": "entries", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "memory_secret_provenance_memory_id_memory_id_fk": { + "name": "memory_secret_provenance_memory_id_memory_id_fk", + "tableFrom": "memory_secret_provenance", + "tableTo": "memory", + "columnsFrom": ["memory_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "memory_secret_provenance_status_check": { + "name": "memory_secret_provenance_status_check", + "value": "\"memory_secret_provenance\".\"status\" IN ('exact', 'unknown')" + } + }, + "isRLSEnabled": false + }, + "public.mothership_inbox_allowed_sender": { + "name": "mothership_inbox_allowed_sender", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "label": { + "name": "label", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "added_by": { + "name": "added_by", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "inbox_sender_ws_email_idx": { + "name": "inbox_sender_ws_email_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "email", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "mothership_inbox_allowed_sender_workspace_id_workspace_id_fk": { + "name": "mothership_inbox_allowed_sender_workspace_id_workspace_id_fk", + "tableFrom": "mothership_inbox_allowed_sender", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "mothership_inbox_allowed_sender_added_by_user_id_fk": { + "name": "mothership_inbox_allowed_sender_added_by_user_id_fk", + "tableFrom": "mothership_inbox_allowed_sender", + "tableTo": "user", + "columnsFrom": ["added_by"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.mothership_inbox_task": { + "name": "mothership_inbox_task", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "from_email": { + "name": "from_email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "from_name": { + "name": "from_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "subject": { + "name": "subject", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "body_preview": { + "name": "body_preview", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "body_text": { + "name": "body_text", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "body_html": { + "name": "body_html", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "email_message_id": { + "name": "email_message_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "in_reply_to": { + "name": "in_reply_to", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "response_message_id": { + "name": "response_message_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "agentmail_message_id": { + "name": "agentmail_message_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'received'" + }, + "chat_id": { + "name": "chat_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "trigger_job_id": { + "name": "trigger_job_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "result_summary": { + "name": "result_summary", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "error_message": { + "name": "error_message", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "rejection_reason": { + "name": "rejection_reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "has_attachments": { + "name": "has_attachments", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "cc_recipients": { + "name": "cc_recipients", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "processing_started_at": { + "name": "processing_started_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "inbox_task_ws_created_at_idx": { + "name": "inbox_task_ws_created_at_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "inbox_task_ws_status_idx": { + "name": "inbox_task_ws_status_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "inbox_task_response_msg_id_idx": { + "name": "inbox_task_response_msg_id_idx", + "columns": [ + { + "expression": "response_message_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "inbox_task_email_msg_id_idx": { + "name": "inbox_task_email_msg_id_idx", + "columns": [ + { + "expression": "email_message_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "mothership_inbox_task_workspace_id_workspace_id_fk": { + "name": "mothership_inbox_task_workspace_id_workspace_id_fk", + "tableFrom": "mothership_inbox_task", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "mothership_inbox_task_chat_id_copilot_chats_id_fk": { + "name": "mothership_inbox_task_chat_id_copilot_chats_id_fk", + "tableFrom": "mothership_inbox_task", + "tableTo": "copilot_chats", + "columnsFrom": ["chat_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.mothership_inbox_webhook": { + "name": "mothership_inbox_webhook", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "webhook_id": { + "name": "webhook_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "secret": { + "name": "secret", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "mothership_inbox_webhook_workspace_id_workspace_id_fk": { + "name": "mothership_inbox_webhook_workspace_id_workspace_id_fk", + "tableFrom": "mothership_inbox_webhook", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "mothership_inbox_webhook_workspace_id_unique": { + "name": "mothership_inbox_webhook_workspace_id_unique", + "nullsNotDistinct": false, + "columns": ["workspace_id"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.mothership_resource_effects": { + "name": "mothership_resource_effects", + "schema": "", + "columns": { + "chat_id": { + "name": "chat_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "effect_id": { + "name": "effect_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "mothership_resource_effects_chat_id_copilot_chats_id_fk": { + "name": "mothership_resource_effects_chat_id_copilot_chats_id_fk", + "tableFrom": "mothership_resource_effects", + "tableTo": "copilot_chats", + "columnsFrom": ["chat_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "mothership_resource_effects_chat_id_effect_id_pk": { + "name": "mothership_resource_effects_chat_id_effect_id_pk", + "columns": ["chat_id", "effect_id"] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.mothership_settings": { + "name": "mothership_settings", + "schema": "", + "columns": { + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "mcp_tool_refs": { + "name": "mcp_tool_refs", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "custom_tool_refs": { + "name": "custom_tool_refs", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "skill_refs": { + "name": "skill_refs", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "mothership_settings_workspace_id_workspace_id_fk": { + "name": "mothership_settings_workspace_id_workspace_id_fk", + "tableFrom": "mothership_settings", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.oauth_access_token": { + "name": "oauth_access_token", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "client_id": { + "name": "client_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "reference_id": { + "name": "reference_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "refresh_id": { + "name": "refresh_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "scopes": { + "name": "scopes", + "type": "text[]", + "primaryKey": false, + "notNull": true + }, + "resource": { + "name": "resource", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "oauth_access_token_client_id_idx": { + "name": "oauth_access_token_client_id_idx", + "columns": [ + { + "expression": "client_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "oauth_access_token_session_id_idx": { + "name": "oauth_access_token_session_id_idx", + "columns": [ + { + "expression": "session_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "oauth_access_token_refresh_id_idx": { + "name": "oauth_access_token_refresh_id_idx", + "columns": [ + { + "expression": "refresh_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "oauth_access_token_user_client_idx": { + "name": "oauth_access_token_user_client_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "client_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "oauth_access_token_expires_at_idx": { + "name": "oauth_access_token_expires_at_idx", + "columns": [ + { + "expression": "expires_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "oauth_access_token_client_id_oauth_client_client_id_fk": { + "name": "oauth_access_token_client_id_oauth_client_client_id_fk", + "tableFrom": "oauth_access_token", + "tableTo": "oauth_client", + "columnsFrom": ["client_id"], + "columnsTo": ["client_id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "oauth_access_token_session_id_session_id_fk": { + "name": "oauth_access_token_session_id_session_id_fk", + "tableFrom": "oauth_access_token", + "tableTo": "session", + "columnsFrom": ["session_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + }, + "oauth_access_token_user_id_user_id_fk": { + "name": "oauth_access_token_user_id_user_id_fk", + "tableFrom": "oauth_access_token", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "oauth_access_token_refresh_id_oauth_refresh_token_id_fk": { + "name": "oauth_access_token_refresh_id_oauth_refresh_token_id_fk", + "tableFrom": "oauth_access_token", + "tableTo": "oauth_refresh_token", + "columnsFrom": ["refresh_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "oauth_access_token_token_unique": { + "name": "oauth_access_token_token_unique", + "nullsNotDistinct": false, + "columns": ["token"] + } + }, + "policies": {}, + "checkConstraints": { + "oauth_access_token_search_resource_check": { + "name": "oauth_access_token_search_resource_check", + "value": "NOT ('search:read' = ANY(\"oauth_access_token\".\"scopes\")) OR \"oauth_access_token\".\"resource\" IS NOT NULL" + } + }, + "isRLSEnabled": false + }, + "public.oauth_client": { + "name": "oauth_client", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "client_id": { + "name": "client_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "client_secret": { + "name": "client_secret", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "disabled": { + "name": "disabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "skip_consent": { + "name": "skip_consent", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "enable_end_session": { + "name": "enable_end_session", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "subject_type": { + "name": "subject_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "scopes": { + "name": "scopes", + "type": "text[]", + "primaryKey": false, + "notNull": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "uri": { + "name": "uri", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "icon": { + "name": "icon", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "contacts": { + "name": "contacts", + "type": "text[]", + "primaryKey": false, + "notNull": false + }, + "tos": { + "name": "tos", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "policy": { + "name": "policy", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "software_id": { + "name": "software_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "software_version": { + "name": "software_version", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "software_statement": { + "name": "software_statement", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "redirect_uris": { + "name": "redirect_uris", + "type": "text[]", + "primaryKey": false, + "notNull": true + }, + "post_logout_redirect_uris": { + "name": "post_logout_redirect_uris", + "type": "text[]", + "primaryKey": false, + "notNull": false + }, + "token_endpoint_auth_method": { + "name": "token_endpoint_auth_method", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "grant_types": { + "name": "grant_types", + "type": "text[]", + "primaryKey": false, + "notNull": false + }, + "response_types": { + "name": "response_types", + "type": "text[]", + "primaryKey": false, + "notNull": false + }, + "public": { + "name": "public", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "require_pkce": { + "name": "require_pkce", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "reference_id": { + "name": "reference_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "oauth_client_user_id_idx": { + "name": "oauth_client_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "oauth_client_user_id_user_id_fk": { + "name": "oauth_client_user_id_user_id_fk", + "tableFrom": "oauth_client", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "oauth_client_client_id_unique": { + "name": "oauth_client_client_id_unique", + "nullsNotDistinct": false, + "columns": ["client_id"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.oauth_consent": { + "name": "oauth_consent", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "client_id": { + "name": "client_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "reference_id": { + "name": "reference_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "scopes": { + "name": "scopes", + "type": "text[]", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "oauth_consent_client_id_idx": { + "name": "oauth_consent_client_id_idx", + "columns": [ + { + "expression": "client_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "oauth_consent_client_id_oauth_client_client_id_fk": { + "name": "oauth_consent_client_id_oauth_client_client_id_fk", + "tableFrom": "oauth_consent", + "tableTo": "oauth_client", + "columnsFrom": ["client_id"], + "columnsTo": ["client_id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "oauth_consent_user_id_user_id_fk": { + "name": "oauth_consent_user_id_user_id_fk", + "tableFrom": "oauth_consent", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "oauth_consent_user_client_reference_unique": { + "name": "oauth_consent_user_client_reference_unique", + "nullsNotDistinct": true, + "columns": ["user_id", "client_id", "reference_id"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.oauth_refresh_token": { + "name": "oauth_refresh_token", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "client_id": { + "name": "client_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "reference_id": { + "name": "reference_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "revoked": { + "name": "revoked", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "auth_time": { + "name": "auth_time", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "scopes": { + "name": "scopes", + "type": "text[]", + "primaryKey": false, + "notNull": true + }, + "resource": { + "name": "resource", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "family_id": { + "name": "family_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "generation": { + "name": "generation", + "type": "integer", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "oauth_refresh_token_client_id_idx": { + "name": "oauth_refresh_token_client_id_idx", + "columns": [ + { + "expression": "client_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "oauth_refresh_token_session_id_idx": { + "name": "oauth_refresh_token_session_id_idx", + "columns": [ + { + "expression": "session_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "oauth_refresh_token_user_client_idx": { + "name": "oauth_refresh_token_user_client_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "client_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "oauth_refresh_token_expires_at_idx": { + "name": "oauth_refresh_token_expires_at_idx", + "columns": [ + { + "expression": "expires_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "oauth_refresh_token_client_id_oauth_client_client_id_fk": { + "name": "oauth_refresh_token_client_id_oauth_client_client_id_fk", + "tableFrom": "oauth_refresh_token", + "tableTo": "oauth_client", + "columnsFrom": ["client_id"], + "columnsTo": ["client_id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "oauth_refresh_token_session_id_session_id_fk": { + "name": "oauth_refresh_token_session_id_session_id_fk", + "tableFrom": "oauth_refresh_token", + "tableTo": "session", + "columnsFrom": ["session_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + }, + "oauth_refresh_token_user_id_user_id_fk": { + "name": "oauth_refresh_token_user_id_user_id_fk", + "tableFrom": "oauth_refresh_token", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "oauth_refresh_token_family_id_oauth_token_family_id_fk": { + "name": "oauth_refresh_token_family_id_oauth_token_family_id_fk", + "tableFrom": "oauth_refresh_token", + "tableTo": "oauth_token_family", + "columnsFrom": ["family_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "oauth_refresh_token_token_unique": { + "name": "oauth_refresh_token_token_unique", + "nullsNotDistinct": false, + "columns": ["token"] + }, + "oauth_refresh_token_family_generation_unique": { + "name": "oauth_refresh_token_family_generation_unique", + "nullsNotDistinct": false, + "columns": ["family_id", "generation"] + } + }, + "policies": {}, + "checkConstraints": { + "oauth_refresh_token_generation_check": { + "name": "oauth_refresh_token_generation_check", + "value": "\"oauth_refresh_token\".\"generation\" BETWEEN 0 AND 1000" + }, + "oauth_refresh_token_search_resource_check": { + "name": "oauth_refresh_token_search_resource_check", + "value": "NOT ('search:read' = ANY(\"oauth_refresh_token\".\"scopes\")) OR \"oauth_refresh_token\".\"resource\" IS NOT NULL" + } + }, + "isRLSEnabled": false + }, + "public.oauth_token_family": { + "name": "oauth_token_family", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "client_id": { + "name": "client_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "reference_id": { + "name": "reference_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "consent_id": { + "name": "consent_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "current_generation": { + "name": "current_generation", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "oauth_token_family_client_id_idx": { + "name": "oauth_token_family_client_id_idx", + "columns": [ + { + "expression": "client_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "oauth_token_family_session_id_idx": { + "name": "oauth_token_family_session_id_idx", + "columns": [ + { + "expression": "session_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "oauth_token_family_user_client_idx": { + "name": "oauth_token_family_user_client_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "client_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "oauth_token_family_consent_id_idx": { + "name": "oauth_token_family_consent_id_idx", + "columns": [ + { + "expression": "consent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "oauth_token_family_expires_at_idx": { + "name": "oauth_token_family_expires_at_idx", + "columns": [ + { + "expression": "expires_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "oauth_token_family_client_id_oauth_client_client_id_fk": { + "name": "oauth_token_family_client_id_oauth_client_client_id_fk", + "tableFrom": "oauth_token_family", + "tableTo": "oauth_client", + "columnsFrom": ["client_id"], + "columnsTo": ["client_id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "oauth_token_family_session_id_session_id_fk": { + "name": "oauth_token_family_session_id_session_id_fk", + "tableFrom": "oauth_token_family", + "tableTo": "session", + "columnsFrom": ["session_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + }, + "oauth_token_family_user_id_user_id_fk": { + "name": "oauth_token_family_user_id_user_id_fk", + "tableFrom": "oauth_token_family", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "oauth_token_family_consent_id_oauth_consent_id_fk": { + "name": "oauth_token_family_consent_id_oauth_consent_id_fk", + "tableFrom": "oauth_token_family", + "tableTo": "oauth_consent", + "columnsFrom": ["consent_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "oauth_token_family_generation_check": { + "name": "oauth_token_family_generation_check", + "value": "\"oauth_token_family\".\"current_generation\" BETWEEN 0 AND 1000" + } + }, + "isRLSEnabled": false + }, + "public.organization": { + "name": "organization", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "slug": { + "name": "slug", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "logo": { + "name": "logo", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "metadata": { + "name": "metadata", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "session_policy_settings": { + "name": "session_policy_settings", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "security_policy_version": { + "name": "security_policy_version", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "require_sso": { + "name": "require_sso", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "whitelabel_settings": { + "name": "whitelabel_settings", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "data_retention_settings": { + "name": "data_retention_settings", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "org_usage_limit": { + "name": "org_usage_limit", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "storage_used_bytes": { + "name": "storage_used_bytes", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "limit_notifications": { + "name": "limit_notifications", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "credit_balance": { + "name": "credit_balance", + "type": "numeric", + "primaryKey": false, + "notNull": true, + "default": "'0'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.organization_access_request_settings": { + "name": "organization_access_request_settings", + "schema": "", + "columns": { + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "allow_requests": { + "name": "allow_requests", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_by": { + "name": "updated_by", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "organization_access_request_settings_organization_id_organization_id_fk": { + "name": "organization_access_request_settings_organization_id_organization_id_fk", + "tableFrom": "organization_access_request_settings", + "tableTo": "organization", + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "organization_access_request_settings_updated_by_user_id_fk": { + "name": "organization_access_request_settings_updated_by_user_id_fk", + "tableFrom": "organization_access_request_settings", + "tableTo": "user", + "columnsFrom": ["updated_by"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.organization_byok_keys": { + "name": "organization_byok_keys", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider_id": { + "name": "provider_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "encrypted_api_key": { + "name": "encrypted_api_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "organization_byok_organization_provider_idx": { + "name": "organization_byok_organization_provider_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "provider_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "organization_byok_keys_organization_id_organization_id_fk": { + "name": "organization_byok_keys_organization_id_organization_id_fk", + "tableFrom": "organization_byok_keys", + "tableTo": "organization", + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "organization_byok_keys_created_by_user_id_fk": { + "name": "organization_byok_keys_created_by_user_id_fk", + "tableFrom": "organization_byok_keys", + "tableTo": "user", + "columnsFrom": ["created_by"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.organization_member_usage_limit": { + "name": "organization_member_usage_limit", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "usage_limit": { + "name": "usage_limit", + "type": "numeric", + "primaryKey": false, + "notNull": true + }, + "set_by": { + "name": "set_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "org_member_usage_limit_org_user_unique": { + "name": "org_member_usage_limit_org_user_unique", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "org_member_usage_limit_organization_id_idx": { + "name": "org_member_usage_limit_organization_id_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "organization_member_usage_limit_organization_id_organization_id_fk": { + "name": "organization_member_usage_limit_organization_id_organization_id_fk", + "tableFrom": "organization_member_usage_limit", + "tableTo": "organization", + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "organization_member_usage_limit_user_id_user_id_fk": { + "name": "organization_member_usage_limit_user_id_user_id_fk", + "tableFrom": "organization_member_usage_limit", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "organization_member_usage_limit_set_by_user_id_fk": { + "name": "organization_member_usage_limit_set_by_user_id_fk", + "tableFrom": "organization_member_usage_limit", + "tableTo": "user", + "columnsFrom": ["set_by"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.organization_search_history": { + "name": "organization_search_history", + "schema": "", + "columns": { + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "sources": { + "name": "sources", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "queries": { + "name": "queries", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + } + }, + "indexes": { + "organization_search_history_user_idx": { + "name": "organization_search_history_user_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "organization_search_history_organization_id_organization_id_fk": { + "name": "organization_search_history_organization_id_organization_id_fk", + "tableFrom": "organization_search_history", + "tableTo": "organization", + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "organization_search_history_user_id_user_id_fk": { + "name": "organization_search_history_user_id_user_id_fk", + "tableFrom": "organization_search_history", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "organization_search_history_organization_id_user_id_pk": { + "name": "organization_search_history_organization_id_user_id_pk", + "columns": ["organization_id", "user_id"] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.organization_search_integration": { + "name": "organization_search_integration", + "schema": "", + "columns": { + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "connector_type": { + "name": "connector_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "approved": { + "name": "approved", + "type": "boolean", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "organization_search_integration_organization_id_organization_id_fk": { + "name": "organization_search_integration_organization_id_organization_id_fk", + "tableFrom": "organization_search_integration", + "tableTo": "organization", + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "organization_search_integration_organization_id_connector_type_pk": { + "name": "organization_search_integration_organization_id_connector_type_pk", + "columns": ["organization_id", "connector_type"] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.organization_search_invocation": { + "name": "organization_search_invocation", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "surface": { + "name": "surface", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "source_types": { + "name": "source_types", + "type": "text[]", + "primaryKey": false, + "notNull": true + }, + "result_count": { + "name": "result_count", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "organization_search_invocation_org_created_idx": { + "name": "organization_search_invocation_org_created_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "organization_search_invocation_user_idx": { + "name": "organization_search_invocation_user_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "organization_search_invocation_organization_id_organization_id_fk": { + "name": "organization_search_invocation_organization_id_organization_id_fk", + "tableFrom": "organization_search_invocation", + "tableTo": "organization", + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "organization_search_invocation_user_id_user_id_fk": { + "name": "organization_search_invocation_user_id_user_id_fk", + "tableFrom": "organization_search_invocation", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "organization_search_invocation_result_count_bounds": { + "name": "organization_search_invocation_result_count_bounds", + "value": "\"organization_search_invocation\".\"result_count\" BETWEEN 0 AND 100" + }, + "organization_search_invocation_source_types_bounds": { + "name": "organization_search_invocation_source_types_bounds", + "value": "cardinality(\"organization_search_invocation\".\"source_types\") <= 100" + } + }, + "isRLSEnabled": false + }, + "public.organization_search_mcp_invocation": { + "name": "organization_search_mcp_invocation", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "auth_kind": { + "name": "auth_kind", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "oauth_client_id": { + "name": "oauth_client_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "client_name": { + "name": "client_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tool_name": { + "name": "tool_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "outcome": { + "name": "outcome", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "duration_ms": { + "name": "duration_ms", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "organization_search_mcp_invocation_org_created_idx": { + "name": "organization_search_mcp_invocation_org_created_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "organization_search_mcp_invocation_user_idx": { + "name": "organization_search_mcp_invocation_user_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "org_search_mcp_invocation_org_fk": { + "name": "org_search_mcp_invocation_org_fk", + "tableFrom": "organization_search_mcp_invocation", + "tableTo": "organization", + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "org_search_mcp_invocation_user_fk": { + "name": "org_search_mcp_invocation_user_fk", + "tableFrom": "organization_search_mcp_invocation", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "organization_search_mcp_invocation_tool_check": { + "name": "organization_search_mcp_invocation_tool_check", + "value": "\"organization_search_mcp_invocation\".\"tool_name\" IN ('search', 'read_document', 'chat')" + }, + "organization_search_mcp_invocation_outcome_check": { + "name": "organization_search_mcp_invocation_outcome_check", + "value": "\"organization_search_mcp_invocation\".\"outcome\" IN ('success', 'error', 'cancelled', 'rate_limited')" + }, + "organization_search_mcp_invocation_duration_check": { + "name": "organization_search_mcp_invocation_duration_check", + "value": "\"organization_search_mcp_invocation\".\"duration_ms\" >= 0" + }, + "organization_search_mcp_invocation_client_name_check": { + "name": "organization_search_mcp_invocation_client_name_check", + "value": "length(\"organization_search_mcp_invocation\".\"client_name\") <= 256" + }, + "organization_search_mcp_invocation_auth_check": { + "name": "organization_search_mcp_invocation_auth_check", + "value": "(\"organization_search_mcp_invocation\".\"auth_kind\" = 'oauth_access_token' AND \"organization_search_mcp_invocation\".\"oauth_client_id\" IS NOT NULL)\n OR (\"organization_search_mcp_invocation\".\"auth_kind\" IN ('personal_api_key', 'workspace_api_key') AND \"organization_search_mcp_invocation\".\"oauth_client_id\" IS NULL AND \"organization_search_mcp_invocation\".\"client_name\" IS NULL)" + } + }, + "isRLSEnabled": false + }, + "public.organization_secret": { + "name": "organization_secret", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "source_id": { + "name": "source_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "owner_user_id": { + "name": "owner_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "encrypted_value": { + "name": "encrypted_value", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "organization_secret_shared_unique": { + "name": "organization_secret_shared_unique", + "columns": [ + { + "expression": "source_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"organization_secret\".\"owner_user_id\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "organization_secret_member_unique": { + "name": "organization_secret_member_unique", + "columns": [ + { + "expression": "source_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "owner_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"organization_secret\".\"owner_user_id\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "organization_secret_owner_idx": { + "name": "organization_secret_owner_idx", + "columns": [ + { + "expression": "owner_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "organization_secret_source_id_organization_secret_source_id_fk": { + "name": "organization_secret_source_id_organization_secret_source_id_fk", + "tableFrom": "organization_secret", + "tableTo": "organization_secret_source", + "columnsFrom": ["source_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "organization_secret_owner_user_id_user_id_fk": { + "name": "organization_secret_owner_user_id_user_id_fk", + "tableFrom": "organization_secret", + "tableTo": "user", + "columnsFrom": ["owner_user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.organization_secret_source": { + "name": "organization_secret_source", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "mode": { + "name": "mode", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "organization_secret_source_org_unique": { + "name": "organization_secret_source_org_unique", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "organization_secret_source_organization_id_organization_id_fk": { + "name": "organization_secret_source_organization_id_organization_id_fk", + "tableFrom": "organization_secret_source", + "tableTo": "organization", + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "organization_secret_source_mode_check": { + "name": "organization_secret_source_mode_check", + "value": "\"organization_secret_source\".\"mode\" IN ('organization', 'member')" + } + }, + "isRLSEnabled": false + }, + "public.outbox_event": { + "name": "outbox_event", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "event_type": { + "name": "event_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "payload": { + "name": "payload", + "type": "json", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "attempts": { + "name": "attempts", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "max_attempts": { + "name": "max_attempts", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 10 + }, + "available_at": { + "name": "available_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "locked_at": { + "name": "locked_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "last_error": { + "name": "last_error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "processed_at": { + "name": "processed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "outbox_event_status_available_idx": { + "name": "outbox_event_status_available_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "available_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "outbox_event_pending_type_available_idx": { + "name": "outbox_event_pending_type_available_idx", + "columns": [ + { + "expression": "event_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "available_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"outbox_event\".\"status\" = 'pending'", + "concurrently": false, + "method": "btree", + "with": {} + }, + "outbox_event_locked_at_idx": { + "name": "outbox_event_locked_at_idx", + "columns": [ + { + "expression": "locked_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "outbox_event_type_created_idx": { + "name": "outbox_event_type_created_idx", + "columns": [ + { + "expression": "event_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.paused_executions": { + "name": "paused_executions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "execution_id": { + "name": "execution_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "execution_snapshot": { + "name": "execution_snapshot", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "pause_points": { + "name": "pause_points", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "total_pause_count": { + "name": "total_pause_count", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "resumed_count": { + "name": "resumed_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "automatic_resume_retry_count": { + "name": "automatic_resume_retry_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'paused'" + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "paused_at": { + "name": "paused_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "next_resume_at": { + "name": "next_resume_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "paused_executions_workflow_id_idx": { + "name": "paused_executions_workflow_id_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "paused_executions_status_idx": { + "name": "paused_executions_status_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "paused_executions_execution_id_unique": { + "name": "paused_executions_execution_id_unique", + "columns": [ + { + "expression": "execution_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "paused_executions_next_resume_at_idx": { + "name": "paused_executions_next_resume_at_idx", + "columns": [ + { + "expression": "next_resume_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "status = 'paused' AND next_resume_at IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "paused_executions_workflow_id_workflow_id_fk": { + "name": "paused_executions_workflow_id_workflow_id_fk", + "tableFrom": "paused_executions", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.pending_credential_draft": { + "name": "pending_credential_draft", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "provider_id": { + "name": "provider_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "display_name": { + "name": "display_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "credential_id": { + "name": "credential_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "oauth_config": { + "name": "oauth_config", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "pending_draft_organization_id_idx": { + "name": "pending_draft_organization_id_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "pending_draft_user_provider_org": { + "name": "pending_draft_user_provider_org", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "provider_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "pending_draft_user_provider_ws": { + "name": "pending_draft_user_provider_ws", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "provider_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "pending_credential_draft_user_id_user_id_fk": { + "name": "pending_credential_draft_user_id_user_id_fk", + "tableFrom": "pending_credential_draft", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "pending_credential_draft_workspace_id_workspace_id_fk": { + "name": "pending_credential_draft_workspace_id_workspace_id_fk", + "tableFrom": "pending_credential_draft", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "pending_credential_draft_organization_id_organization_id_fk": { + "name": "pending_credential_draft_organization_id_organization_id_fk", + "tableFrom": "pending_credential_draft", + "tableTo": "organization", + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "pending_credential_draft_credential_id_credential_id_fk": { + "name": "pending_credential_draft_credential_id_credential_id_fk", + "tableFrom": "pending_credential_draft", + "tableTo": "credential", + "columnsFrom": ["credential_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "pending_draft_owner_check": { + "name": "pending_draft_owner_check", + "value": "num_nonnulls(\"pending_credential_draft\".\"workspace_id\", \"pending_credential_draft\".\"organization_id\") = 1" + } + }, + "isRLSEnabled": false + }, + "public.permission_access_request": { + "name": "permission_access_request", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "requester_id": { + "name": "requester_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "scope_key": { + "name": "scope_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "target_key": { + "name": "target_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "target": { + "name": "target", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "target_label": { + "name": "target_label", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "membership_id": { + "name": "membership_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "group_id": { + "name": "group_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "group_name": { + "name": "group_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "reason": { + "name": "reason", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "decision_reason": { + "name": "decision_reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "decided_by": { + "name": "decided_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "decision": { + "name": "decision", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "decided_at": { + "name": "decided_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "permission_access_request_pending_unique": { + "name": "permission_access_request_pending_unique", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "requester_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "scope_key", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "target_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"permission_access_request\".\"status\" = 'pending'", + "concurrently": false, + "method": "btree", + "with": {} + }, + "permission_access_request_org_queue_idx": { + "name": "permission_access_request_org_queue_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "permission_access_request_requester_idx": { + "name": "permission_access_request_requester_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "requester_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "scope_key", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "permission_access_request_organization_id_organization_id_fk": { + "name": "permission_access_request_organization_id_organization_id_fk", + "tableFrom": "permission_access_request", + "tableTo": "organization", + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "permission_access_request_requester_id_user_id_fk": { + "name": "permission_access_request_requester_id_user_id_fk", + "tableFrom": "permission_access_request", + "tableTo": "user", + "columnsFrom": ["requester_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "permission_access_request_decided_by_user_id_fk": { + "name": "permission_access_request_decided_by_user_id_fk", + "tableFrom": "permission_access_request", + "tableTo": "user", + "columnsFrom": ["decided_by"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "permission_access_request_status_check": { + "name": "permission_access_request_status_check", + "value": "\"permission_access_request\".\"status\" in ('pending', 'fulfilled', 'declined', 'cancelled', 'closed')" + } + }, + "isRLSEnabled": false + }, + "public.permission_group": { + "name": "permission_group", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "config": { + "name": "config", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "is_default": { + "name": "is_default", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "membership_mode": { + "name": "membership_mode", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'inherit'" + } + }, + "indexes": { + "permission_group_created_by_idx": { + "name": "permission_group_created_by_idx", + "columns": [ + { + "expression": "created_by", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "permission_group_organization_name_unique": { + "name": "permission_group_organization_name_unique", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "permission_group_organization_default_unique": { + "name": "permission_group_organization_default_unique", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "is_default = true", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "permission_group_organization_id_organization_id_fk": { + "name": "permission_group_organization_id_organization_id_fk", + "tableFrom": "permission_group", + "tableTo": "organization", + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "permission_group_created_by_user_id_fk": { + "name": "permission_group_created_by_user_id_fk", + "tableFrom": "permission_group", + "tableTo": "user", + "columnsFrom": ["created_by"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.permission_group_member": { + "name": "permission_group_member", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "permission_group_id": { + "name": "permission_group_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "assigned_by": { + "name": "assigned_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "assigned_at": { + "name": "assigned_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "permission_group_member_group_id_idx": { + "name": "permission_group_member_group_id_idx", + "columns": [ + { + "expression": "permission_group_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "permission_group_member_group_user_unique": { + "name": "permission_group_member_group_user_unique", + "columns": [ + { + "expression": "permission_group_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "permission_group_member_organization_user_idx": { + "name": "permission_group_member_organization_user_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "permission_group_member_permission_group_id_permission_group_id_fk": { + "name": "permission_group_member_permission_group_id_permission_group_id_fk", + "tableFrom": "permission_group_member", + "tableTo": "permission_group", + "columnsFrom": ["permission_group_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "permission_group_member_organization_id_organization_id_fk": { + "name": "permission_group_member_organization_id_organization_id_fk", + "tableFrom": "permission_group_member", + "tableTo": "organization", + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "permission_group_member_user_id_user_id_fk": { + "name": "permission_group_member_user_id_user_id_fk", + "tableFrom": "permission_group_member", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "permission_group_member_assigned_by_user_id_fk": { + "name": "permission_group_member_assigned_by_user_id_fk", + "tableFrom": "permission_group_member", + "tableTo": "user", + "columnsFrom": ["assigned_by"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.permission_group_workspace": { + "name": "permission_group_workspace", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "permission_group_id": { + "name": "permission_group_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "permission_group_workspace_workspace_id_idx": { + "name": "permission_group_workspace_workspace_id_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "permission_group_workspace_group_workspace_unique": { + "name": "permission_group_workspace_group_workspace_unique", + "columns": [ + { + "expression": "permission_group_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "permission_group_workspace_permission_group_id_permission_group_id_fk": { + "name": "permission_group_workspace_permission_group_id_permission_group_id_fk", + "tableFrom": "permission_group_workspace", + "tableTo": "permission_group", + "columnsFrom": ["permission_group_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "permission_group_workspace_workspace_id_workspace_id_fk": { + "name": "permission_group_workspace_workspace_id_workspace_id_fk", + "tableFrom": "permission_group_workspace", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "permission_group_workspace_organization_id_organization_id_fk": { + "name": "permission_group_workspace_organization_id_organization_id_fk", + "tableFrom": "permission_group_workspace", + "tableTo": "organization", + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.permissions": { + "name": "permissions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "entity_type": { + "name": "entity_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "entity_id": { + "name": "entity_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "permission_type": { + "name": "permission_type", + "type": "permission_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "permissions_user_id_idx": { + "name": "permissions_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "permissions_entity_idx": { + "name": "permissions_entity_idx", + "columns": [ + { + "expression": "entity_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "entity_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "permissions_user_entity_type_idx": { + "name": "permissions_user_entity_type_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "entity_type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "permissions_user_entity_permission_idx": { + "name": "permissions_user_entity_permission_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "entity_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "permission_type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "permissions_unique_constraint": { + "name": "permissions_unique_constraint", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "entity_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "entity_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "permissions_user_id_user_id_fk": { + "name": "permissions_user_id_user_id_fk", + "tableFrom": "permissions", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.pinned_item": { + "name": "pinned_item", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "resource_type": { + "name": "resource_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "resource_id": { + "name": "resource_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "pinned_at": { + "name": "pinned_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "pinned_item_user_workspace_idx": { + "name": "pinned_item_user_workspace_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "pinned_item_resource_idx": { + "name": "pinned_item_resource_idx", + "columns": [ + { + "expression": "resource_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "resource_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "pinned_item_user_resource_unique": { + "name": "pinned_item_user_resource_unique", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "resource_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "resource_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "pinned_item_user_id_user_id_fk": { + "name": "pinned_item_user_id_user_id_fk", + "tableFrom": "pinned_item", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "pinned_item_workspace_id_workspace_id_fk": { + "name": "pinned_item_workspace_id_workspace_id_fk", + "tableFrom": "pinned_item", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.project": { + "name": "project", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "owner_id": { + "name": "owner_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "archived_at": { + "name": "archived_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "organization_scope_key": { + "name": "organization_scope_key", + "type": "text", + "primaryKey": false, + "notNull": false, + "generated": { + "as": "CASE WHEN organization_id IS NULL THEN 'personal' ELSE 'organization:' || organization_id END", + "type": "stored" + } + } + }, + "indexes": { + "project_organization_archive_id_idx": { + "name": "project_organization_archive_id_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "archived_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "project_owner_archive_id_idx": { + "name": "project_owner_archive_id_idx", + "columns": [ + { + "expression": "owner_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "archived_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "project_organization_id_organization_id_fk": { + "name": "project_organization_id_organization_id_fk", + "tableFrom": "project", + "tableTo": "organization", + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], + "onDelete": "restrict", + "onUpdate": "no action" + }, + "project_owner_id_user_id_fk": { + "name": "project_owner_id_user_id_fk", + "tableFrom": "project", + "tableTo": "user", + "columnsFrom": ["owner_id"], + "columnsTo": ["id"], + "onDelete": "restrict", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "project_id_organization_scope_unique": { + "name": "project_id_organization_scope_unique", + "nullsNotDistinct": false, + "columns": ["id", "organization_scope_key"] + } + }, + "policies": {}, + "checkConstraints": { + "project_name_length": { + "name": "project_name_length", + "value": "char_length(btrim(\"project\".\"name\")) BETWEEN 1 AND 100" + } + }, + "isRLSEnabled": false + }, + "public.public_share": { + "name": "public_share", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "resource_type": { + "name": "resource_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "resource_id": { + "name": "resource_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "is_active": { + "name": "is_active", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "auth_type": { + "name": "auth_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'public'" + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "allowed_emails": { + "name": "allowed_emails", + "type": "json", + "primaryKey": false, + "notNull": false, + "default": "'[]'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "public_share_token_unique": { + "name": "public_share_token_unique", + "columns": [ + { + "expression": "token", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "public_share_resource_unique": { + "name": "public_share_resource_unique", + "columns": [ + { + "expression": "resource_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "resource_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "public_share_resource_id_idx": { + "name": "public_share_resource_id_idx", + "columns": [ + { + "expression": "resource_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "public_share_workspace_id_idx": { + "name": "public_share_workspace_id_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "public_share_workspace_id_workspace_id_fk": { + "name": "public_share_workspace_id_workspace_id_fk", + "tableFrom": "public_share", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "public_share_created_by_user_id_fk": { + "name": "public_share_created_by_user_id_fk", + "tableFrom": "public_share", + "tableTo": "user", + "columnsFrom": ["created_by"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.rate_limit_bucket": { + "name": "rate_limit_bucket", + "schema": "", + "columns": { + "key": { + "name": "key", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "tokens": { + "name": "tokens", + "type": "numeric", + "primaryKey": false, + "notNull": true + }, + "last_refill_at": { + "name": "last_refill_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "blocked_until": { + "name": "blocked_until", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "capacity_state": { + "name": "capacity_state", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.resource_policy": { + "name": "resource_policy", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "resource_type": { + "name": "resource_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "resource_id": { + "name": "resource_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "revision": { + "name": "revision", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "document": { + "name": "document", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "updated_by": { + "name": "updated_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "resource_policy_organization_id_idx": { + "name": "resource_policy_organization_id_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "resource_policy_resource_unique": { + "name": "resource_policy_resource_unique", + "columns": [ + { + "expression": "resource_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "resource_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "resource_policy_workspace_id_idx": { + "name": "resource_policy_workspace_id_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "resource_policy_workspace_id_workspace_id_fk": { + "name": "resource_policy_workspace_id_workspace_id_fk", + "tableFrom": "resource_policy", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "resource_policy_organization_id_organization_id_fk": { + "name": "resource_policy_organization_id_organization_id_fk", + "tableFrom": "resource_policy", + "tableTo": "organization", + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "resource_policy_created_by_user_id_fk": { + "name": "resource_policy_created_by_user_id_fk", + "tableFrom": "resource_policy", + "tableTo": "user", + "columnsFrom": ["created_by"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + }, + "resource_policy_updated_by_user_id_fk": { + "name": "resource_policy_updated_by_user_id_fk", + "tableFrom": "resource_policy", + "tableTo": "user", + "columnsFrom": ["updated_by"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "resource_policy_owner_check": { + "name": "resource_policy_owner_check", + "value": "num_nonnulls(\"resource_policy\".\"workspace_id\", \"resource_policy\".\"organization_id\") = 1" + } + }, + "isRLSEnabled": false + }, + "public.resume_queue": { + "name": "resume_queue", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "paused_execution_id": { + "name": "paused_execution_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "parent_execution_id": { + "name": "parent_execution_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "new_execution_id": { + "name": "new_execution_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "context_id": { + "name": "context_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "resume_input": { + "name": "resume_input", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "queued_at": { + "name": "queued_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "claimed_at": { + "name": "claimed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "failure_reason": { + "name": "failure_reason", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "resume_queue_parent_status_idx": { + "name": "resume_queue_parent_status_idx", + "columns": [ + { + "expression": "parent_execution_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "queued_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "resume_queue_new_execution_idx": { + "name": "resume_queue_new_execution_idx", + "columns": [ + { + "expression": "new_execution_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "resume_queue_paused_execution_id_paused_executions_id_fk": { + "name": "resume_queue_paused_execution_id_paused_executions_id_fk", + "tableFrom": "resume_queue", + "tableTo": "paused_executions", + "columnsFrom": ["paused_execution_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.sandbox_image": { + "name": "sandbox_image", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "spec_hash": { + "name": "spec_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "spec": { + "name": "spec", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "sandbox_image_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "image_ref": { + "name": "image_ref", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "provider_image_id": { + "name": "provider_image_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "build_id": { + "name": "build_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "materialization_generation": { + "name": "materialization_generation", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "error_code": { + "name": "error_code", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "error_message": { + "name": "error_message", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "error_detail": { + "name": "error_detail", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "last_used_at": { + "name": "last_used_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "sandbox_image_provider_spec_unique": { + "name": "sandbox_image_provider_spec_unique", + "columns": [ + { + "expression": "provider", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "spec_hash", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "sandbox_image_status_idx": { + "name": "sandbox_image_status_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "sandbox_image_last_used_idx": { + "name": "sandbox_image_last_used_idx", + "columns": [ + { + "expression": "last_used_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.scim_connection": { + "name": "scim_connection", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'active'" + }, + "settings": { + "name": "settings", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "last_request_at": { + "name": "last_request_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "reconcile_lock_token": { + "name": "reconcile_lock_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "reconcile_lease_at": { + "name": "reconcile_lease_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "reconciled_at": { + "name": "reconciled_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "scim_connection_organization_unique": { + "name": "scim_connection_organization_unique", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "scim_connection_reconcile_due_idx": { + "name": "scim_connection_reconcile_due_idx", + "columns": [ + { + "expression": "reconciled_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "scim_connection_organization_id_organization_id_fk": { + "name": "scim_connection_organization_id_organization_id_fk", + "tableFrom": "scim_connection", + "tableTo": "organization", + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "scim_connection_created_by_user_id_fk": { + "name": "scim_connection_created_by_user_id_fk", + "tableFrom": "scim_connection", + "tableTo": "user", + "columnsFrom": ["created_by"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.scim_credential": { + "name": "scim_credential", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "connection_id": { + "name": "connection_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "token_hash": { + "name": "token_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "token_prefix": { + "name": "token_prefix", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "scopes": { + "name": "scopes", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "revoked_at": { + "name": "revoked_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "revoked_by": { + "name": "revoked_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "last_used_at": { + "name": "last_used_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "scim_credential_token_hash_unique": { + "name": "scim_credential_token_hash_unique", + "columns": [ + { + "expression": "token_hash", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "scim_credential_connection_idx": { + "name": "scim_credential_connection_idx", + "columns": [ + { + "expression": "connection_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "scim_credential_connection_id_scim_connection_id_fk": { + "name": "scim_credential_connection_id_scim_connection_id_fk", + "tableFrom": "scim_credential", + "tableTo": "scim_connection", + "columnsFrom": ["connection_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "scim_credential_revoked_by_user_id_fk": { + "name": "scim_credential_revoked_by_user_id_fk", + "tableFrom": "scim_credential", + "tableTo": "user", + "columnsFrom": ["revoked_by"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + }, + "scim_credential_created_by_user_id_fk": { + "name": "scim_credential_created_by_user_id_fk", + "tableFrom": "scim_credential", + "tableTo": "user", + "columnsFrom": ["created_by"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.scim_group": { + "name": "scim_group", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "connection_id": { + "name": "connection_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "external_id": { + "name": "external_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "display_name": { + "name": "display_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "display_name_key": { + "name": "display_name_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "order_key": { + "name": "order_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "scim_group_connection_display_name_unique": { + "name": "scim_group_connection_display_name_unique", + "columns": [ + { + "expression": "connection_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "display_name_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "scim_group_connection_external_id_unique": { + "name": "scim_group_connection_external_id_unique", + "columns": [ + { + "expression": "connection_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "external_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "external_id is not null", + "concurrently": false, + "method": "btree", + "with": {} + }, + "scim_group_connection_order_idx": { + "name": "scim_group_connection_order_idx", + "columns": [ + { + "expression": "connection_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "order_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "scim_group_connection_id_scim_connection_id_fk": { + "name": "scim_group_connection_id_scim_connection_id_fk", + "tableFrom": "scim_group", + "tableTo": "scim_connection", + "columnsFrom": ["connection_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.scim_group_mapping": { + "name": "scim_group_mapping", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "group_id": { + "name": "group_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "target_kind": { + "name": "target_kind", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "permission_group_id": { + "name": "permission_group_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "permission_type": { + "name": "permission_type", + "type": "permission_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": false + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "source": { + "name": "source", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'manual'" + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "scim_group_mapping_group_idx": { + "name": "scim_group_mapping_group_idx", + "columns": [ + { + "expression": "group_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "scim_group_mapping_permission_group_idx": { + "name": "scim_group_mapping_permission_group_idx", + "columns": [ + { + "expression": "permission_group_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "scim_group_mapping_workspace_idx": { + "name": "scim_group_mapping_workspace_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "scim_group_mapping_group_target_unique": { + "name": "scim_group_mapping_group_target_unique", + "columns": [ + { + "expression": "group_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "target_kind", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "coalesce(\"permission_group_id\", \"workspace_id\", \"role\")", + "asc": true, + "isExpression": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "scim_group_mapping_group_id_scim_group_id_fk": { + "name": "scim_group_mapping_group_id_scim_group_id_fk", + "tableFrom": "scim_group_mapping", + "tableTo": "scim_group", + "columnsFrom": ["group_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "scim_group_mapping_permission_group_id_permission_group_id_fk": { + "name": "scim_group_mapping_permission_group_id_permission_group_id_fk", + "tableFrom": "scim_group_mapping", + "tableTo": "permission_group", + "columnsFrom": ["permission_group_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "scim_group_mapping_workspace_id_workspace_id_fk": { + "name": "scim_group_mapping_workspace_id_workspace_id_fk", + "tableFrom": "scim_group_mapping", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "scim_group_mapping_created_by_user_id_fk": { + "name": "scim_group_mapping_created_by_user_id_fk", + "tableFrom": "scim_group_mapping", + "tableTo": "user", + "columnsFrom": ["created_by"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "scim_group_mapping_target_shape": { + "name": "scim_group_mapping_target_shape", + "value": "(\n (\"scim_group_mapping\".\"target_kind\" = 'permission_group' AND \"scim_group_mapping\".\"permission_group_id\" IS NOT NULL AND \"scim_group_mapping\".\"workspace_id\" IS NULL AND \"scim_group_mapping\".\"permission_type\" IS NULL AND \"scim_group_mapping\".\"role\" IS NULL)\n OR (\"scim_group_mapping\".\"target_kind\" = 'workspace' AND \"scim_group_mapping\".\"workspace_id\" IS NOT NULL AND \"scim_group_mapping\".\"permission_type\" IS NOT NULL AND \"scim_group_mapping\".\"permission_group_id\" IS NULL AND \"scim_group_mapping\".\"role\" IS NULL)\n OR (\"scim_group_mapping\".\"target_kind\" = 'org_role' AND \"scim_group_mapping\".\"role\" IS NOT NULL AND \"scim_group_mapping\".\"permission_group_id\" IS NULL AND \"scim_group_mapping\".\"workspace_id\" IS NULL AND \"scim_group_mapping\".\"permission_type\" IS NULL)\n )" + } + }, + "isRLSEnabled": false + }, + "public.scim_group_member": { + "name": "scim_group_member", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "group_id": { + "name": "group_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "scim_user_id": { + "name": "scim_user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "scim_group_member_group_user_unique": { + "name": "scim_group_member_group_user_unique", + "columns": [ + { + "expression": "group_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "scim_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "scim_group_member_scim_user_idx": { + "name": "scim_group_member_scim_user_idx", + "columns": [ + { + "expression": "scim_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "scim_group_member_group_id_scim_group_id_fk": { + "name": "scim_group_member_group_id_scim_group_id_fk", + "tableFrom": "scim_group_member", + "tableTo": "scim_group", + "columnsFrom": ["group_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "scim_group_member_scim_user_id_scim_user_id_fk": { + "name": "scim_group_member_scim_user_id_scim_user_id_fk", + "tableFrom": "scim_group_member", + "tableTo": "scim_user", + "columnsFrom": ["scim_user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.scim_projection_grant": { + "name": "scim_projection_grant", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "connection_id": { + "name": "connection_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "scim_user_id": { + "name": "scim_user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "target_kind": { + "name": "target_kind", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "target_id": { + "name": "target_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "permission_type": { + "name": "permission_type", + "type": "permission_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": false + }, + "baseline_permission": { + "name": "baseline_permission", + "type": "permission_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": false + }, + "origin": { + "name": "origin", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'directory'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "scim_projection_grant_user_target_unique": { + "name": "scim_projection_grant_user_target_unique", + "columns": [ + { + "expression": "scim_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "target_kind", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "target_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "scim_projection_grant_connection_idx": { + "name": "scim_projection_grant_connection_idx", + "columns": [ + { + "expression": "connection_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "scim_projection_grant_connection_id_scim_connection_id_fk": { + "name": "scim_projection_grant_connection_id_scim_connection_id_fk", + "tableFrom": "scim_projection_grant", + "tableTo": "scim_connection", + "columnsFrom": ["connection_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "scim_projection_grant_scim_user_id_scim_user_id_fk": { + "name": "scim_projection_grant_scim_user_id_scim_user_id_fk", + "tableFrom": "scim_projection_grant", + "tableTo": "scim_user", + "columnsFrom": ["scim_user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.scim_request_log": { + "name": "scim_request_log", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "connection_id": { + "name": "connection_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "credential_id": { + "name": "credential_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "method": { + "name": "method", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "path": { + "name": "path", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "scim_type": { + "name": "scim_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "detail": { + "name": "detail", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_agent": { + "name": "user_agent", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "duration_ms": { + "name": "duration_ms", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "scim_request_log_connection_created_idx": { + "name": "scim_request_log_connection_created_idx", + "columns": [ + { + "expression": "connection_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "scim_request_log_connection_id_scim_connection_id_fk": { + "name": "scim_request_log_connection_id_scim_connection_id_fk", + "tableFrom": "scim_request_log", + "tableTo": "scim_connection", + "columnsFrom": ["connection_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.scim_user": { + "name": "scim_user", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "connection_id": { + "name": "connection_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "external_id": { + "name": "external_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_name": { + "name": "user_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "active": { + "name": "active", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "attributes": { + "name": "attributes", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "order_key": { + "name": "order_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "scim_user_connection_user_unique": { + "name": "scim_user_connection_user_unique", + "columns": [ + { + "expression": "connection_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "scim_user_connection_user_name_unique": { + "name": "scim_user_connection_user_name_unique", + "columns": [ + { + "expression": "connection_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "user_name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "scim_user_connection_external_id_unique": { + "name": "scim_user_connection_external_id_unique", + "columns": [ + { + "expression": "connection_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "external_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "external_id is not null", + "concurrently": false, + "method": "btree", + "with": {} + }, + "scim_user_connection_order_idx": { + "name": "scim_user_connection_order_idx", + "columns": [ + { + "expression": "connection_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "order_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "scim_user_user_idx": { + "name": "scim_user_user_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "scim_user_connection_id_scim_connection_id_fk": { + "name": "scim_user_connection_id_scim_connection_id_fk", + "tableFrom": "scim_user", + "tableTo": "scim_connection", + "columnsFrom": ["connection_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "scim_user_user_id_user_id_fk": { + "name": "scim_user_user_id_user_id_fk", + "tableFrom": "scim_user", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.scim_user_tombstone": { + "name": "scim_user_tombstone", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "connection_id": { + "name": "connection_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "external_id": { + "name": "external_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "scim_user_tombstone_connection_external_id_unique": { + "name": "scim_user_tombstone_connection_external_id_unique", + "columns": [ + { + "expression": "connection_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "external_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "scim_user_tombstone_user_idx": { + "name": "scim_user_tombstone_user_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "scim_user_tombstone_connection_id_scim_connection_id_fk": { + "name": "scim_user_tombstone_connection_id_scim_connection_id_fk", + "tableFrom": "scim_user_tombstone", + "tableTo": "scim_connection", + "columnsFrom": ["connection_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "scim_user_tombstone_user_id_user_id_fk": { + "name": "scim_user_tombstone_user_id_user_id_fk", + "tableFrom": "scim_user_tombstone", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.secret_usage": { + "name": "secret_usage", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "secret_name": { + "name": "secret_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "secret_scope": { + "name": "secret_scope", + "type": "secret_usage_scope", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "secret_owner_user_id": { + "name": "secret_owner_user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "source": { + "name": "source", + "type": "secret_usage_source", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "actor_user_id": { + "name": "actor_user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "usage_date": { + "name": "usage_date", + "type": "date", + "primaryKey": false, + "notNull": true + }, + "use_count": { + "name": "use_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "last_used_at": { + "name": "last_used_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "last_execution_id": { + "name": "last_execution_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "last_trigger": { + "name": "last_trigger", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "secret_usage_bucket_unique": { + "name": "secret_usage_bucket_unique", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "secret_name", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "secret_scope", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "secret_owner_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "source", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "actor_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "usage_date", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "secret_usage_secret_recent_idx": { + "name": "secret_usage_secret_recent_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "secret_name", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "secret_scope", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "secret_owner_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "last_used_at", + "isExpression": false, + "asc": false, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "secret_usage_workspace_id_workspace_id_fk": { + "name": "secret_usage_workspace_id_workspace_id_fk", + "tableFrom": "secret_usage", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.session": { + "name": "session", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "ip_address": { + "name": "ip_address", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_agent": { + "name": "user_agent", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "active_organization_id": { + "name": "active_organization_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "impersonated_by": { + "name": "impersonated_by", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "session_user_id_idx": { + "name": "session_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "session_user_id_user_id_fk": { + "name": "session_user_id_user_id_fk", + "tableFrom": "session", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "session_active_organization_id_organization_id_fk": { + "name": "session_active_organization_id_organization_id_fk", + "tableFrom": "session", + "tableTo": "organization", + "columnsFrom": ["active_organization_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "session_token_unique": { + "name": "session_token_unique", + "nullsNotDistinct": false, + "columns": ["token"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.settings": { + "name": "settings", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "theme": { + "name": "theme", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'system'" + }, + "auto_connect": { + "name": "auto_connect", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "telemetry_enabled": { + "name": "telemetry_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "email_preferences": { + "name": "email_preferences", + "type": "json", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "billing_usage_notifications_enabled": { + "name": "billing_usage_notifications_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "show_training_controls": { + "name": "show_training_controls", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "super_user_mode_enabled": { + "name": "super_user_mode_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "mothership_environment": { + "name": "mothership_environment", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'default'" + }, + "error_notifications_enabled": { + "name": "error_notifications_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "snap_to_grid_size": { + "name": "snap_to_grid_size", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "show_action_bar": { + "name": "show_action_bar", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "auto_focus_on_click": { + "name": "auto_focus_on_click", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "timezone": { + "name": "timezone", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "copilot_enabled_models": { + "name": "copilot_enabled_models", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "copilot_auto_allowed_tools": { + "name": "copilot_auto_allowed_tools", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'" + }, + "last_active_workspace_id": { + "name": "last_active_workspace_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "settings_user_id_user_id_fk": { + "name": "settings_user_id_user_id_fk", + "tableFrom": "settings", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "settings_user_id_unique": { + "name": "settings_user_id_unique", + "nullsNotDistinct": false, + "columns": ["user_id"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.sim_trigger_state": { + "name": "sim_trigger_state", + "schema": "", + "columns": { + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "block_id": { + "name": "block_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "scope_key": { + "name": "scope_key", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "last_fired_at": { + "name": "last_fired_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "sim_trigger_state_workflow_id_workflow_id_fk": { + "name": "sim_trigger_state_workflow_id_workflow_id_fk", + "tableFrom": "sim_trigger_state", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "sim_trigger_state_workflow_id_block_id_scope_key_pk": { + "name": "sim_trigger_state_workflow_id_block_id_scope_key_pk", + "columns": ["workflow_id", "block_id", "scope_key"] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.skill": { + "name": "skill", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "content": { + "name": "content", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "skill_workspace_name_unique": { + "name": "skill_workspace_name_unique", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "skill_workspace_id_workspace_id_fk": { + "name": "skill_workspace_id_workspace_id_fk", + "tableFrom": "skill", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "skill_user_id_user_id_fk": { + "name": "skill_user_id_user_id_fk", + "tableFrom": "skill", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.skill_member": { + "name": "skill_member", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "skill_id": { + "name": "skill_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "invited_by": { + "name": "invited_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "skill_member_user_id_idx": { + "name": "skill_member_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "skill_member_unique": { + "name": "skill_member_unique", + "columns": [ + { + "expression": "skill_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "skill_member_skill_id_skill_id_fk": { + "name": "skill_member_skill_id_skill_id_fk", + "tableFrom": "skill_member", + "tableTo": "skill", + "columnsFrom": ["skill_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "skill_member_user_id_user_id_fk": { + "name": "skill_member_user_id_user_id_fk", + "tableFrom": "skill_member", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "skill_member_invited_by_user_id_fk": { + "name": "skill_member_invited_by_user_id_fk", + "tableFrom": "skill_member", + "tableTo": "user", + "columnsFrom": ["invited_by"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.slack_app": { + "name": "slack_app", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "kind": { + "name": "kind", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "client_id": { + "name": "client_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "encrypted_client_secret": { + "name": "encrypted_client_secret", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "encrypted_signing_secret": { + "name": "encrypted_signing_secret", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "revision": { + "name": "revision", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "slack_app_organization_id_organization_id_fk": { + "name": "slack_app_organization_id_organization_id_fk", + "tableFrom": "slack_app", + "tableTo": "organization", + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "slack_app_owner_check": { + "name": "slack_app_owner_check", + "value": "(\"slack_app\".\"kind\" = 'custom' AND \"slack_app\".\"organization_id\" IS NOT NULL) OR (\"slack_app\".\"kind\" = 'shared' AND \"slack_app\".\"organization_id\" IS NULL)" + }, + "slack_app_custom_credentials_check": { + "name": "slack_app_custom_credentials_check", + "value": "\"slack_app\".\"kind\" = 'shared' OR (\"slack_app\".\"client_id\" IS NOT NULL AND \"slack_app\".\"encrypted_client_secret\" IS NOT NULL AND \"slack_app\".\"encrypted_signing_secret\" IS NOT NULL)" + } + }, + "isRLSEnabled": false + }, + "public.slack_search_installation": { + "name": "slack_search_installation", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "credential_id": { + "name": "credential_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "app_id": { + "name": "app_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "slack_app_id": { + "name": "slack_app_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "team_id": { + "name": "team_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "team_name": { + "name": "team_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "bot_user_id": { + "name": "bot_user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "enterprise_id": { + "name": "enterprise_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "credential_version": { + "name": "credential_version", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "revision": { + "name": "revision", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "last_outcome": { + "name": "last_outcome", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "last_event_at": { + "name": "last_event_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "slack_search_installation_organization_idx": { + "name": "slack_search_installation_organization_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "slack_search_installation_credential_unique": { + "name": "slack_search_installation_credential_unique", + "columns": [ + { + "expression": "credential_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "slack_search_installation_app_team_unique": { + "name": "slack_search_installation_app_team_unique", + "columns": [ + { + "expression": "app_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "team_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "slack_search_installation_active_team_unique": { + "name": "slack_search_installation_active_team_unique", + "columns": [ + { + "expression": "team_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"slack_search_installation\".\"enabled\" = true", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "slack_search_installation_organization_id_organization_id_fk": { + "name": "slack_search_installation_organization_id_organization_id_fk", + "tableFrom": "slack_search_installation", + "tableTo": "organization", + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "slack_search_installation_credential_id_credential_id_fk": { + "name": "slack_search_installation_credential_id_credential_id_fk", + "tableFrom": "slack_search_installation", + "tableTo": "credential", + "columnsFrom": ["credential_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "slack_search_installation_slack_app_id_slack_app_id_fk": { + "name": "slack_search_installation_slack_app_id_slack_app_id_fk", + "tableFrom": "slack_search_installation", + "tableTo": "slack_app", + "columnsFrom": ["slack_app_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.slack_search_turn": { + "name": "slack_search_turn", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "ordinal": { + "name": "ordinal", + "type": "integer", + "primaryKey": false, + "notNull": true, + "identity": { + "type": "always", + "name": "slack_search_turn_ordinal_seq", + "schema": "public", + "increment": "1", + "startWith": "1", + "minValue": "1", + "maxValue": "2147483647", + "cache": "1", + "cycle": false + } + }, + "installation_id": { + "name": "installation_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "conversation_key": { + "name": "conversation_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "event_id": { + "name": "event_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "payload": { + "name": "payload", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "lease_id": { + "name": "lease_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "lease_expires_at": { + "name": "lease_expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "outcome": { + "name": "outcome", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "slack_search_turn_event_unique": { + "name": "slack_search_turn_event_unique", + "columns": [ + { + "expression": "installation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "event_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "slack_search_turn_pending_idx": { + "name": "slack_search_turn_pending_idx", + "columns": [ + { + "expression": "installation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "slack_search_turn_thread_idx": { + "name": "slack_search_turn_thread_idx", + "columns": [ + { + "expression": "conversation_key", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "slack_search_turn_active_thread_unique": { + "name": "slack_search_turn_active_thread_unique", + "columns": [ + { + "expression": "conversation_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"slack_search_turn\".\"status\" = 'running'", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "slack_search_turn_installation_id_slack_search_installation_id_fk": { + "name": "slack_search_turn_installation_id_slack_search_installation_id_fk", + "tableFrom": "slack_search_turn", + "tableTo": "slack_search_installation", + "columnsFrom": ["installation_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.sso_domain": { + "name": "sso_domain", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "domain": { + "name": "domain", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "verification_token": { + "name": "verification_token", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "verified_at": { + "name": "verified_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "primary_provider_id": { + "name": "primary_provider_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "sso_domain_organization_id_idx": { + "name": "sso_domain_organization_id_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "sso_domain_domain_idx": { + "name": "sso_domain_domain_idx", + "columns": [ + { + "expression": "domain", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "sso_domain_org_domain_unique": { + "name": "sso_domain_org_domain_unique", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "domain", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "sso_domain_verified_unique": { + "name": "sso_domain_verified_unique", + "columns": [ + { + "expression": "domain", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "status = 'verified'", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "sso_domain_organization_id_organization_id_fk": { + "name": "sso_domain_organization_id_organization_id_fk", + "tableFrom": "sso_domain", + "tableTo": "organization", + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "sso_domain_created_by_user_id_fk": { + "name": "sso_domain_created_by_user_id_fk", + "tableFrom": "sso_domain", + "tableTo": "user", + "columnsFrom": ["created_by"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.sso_provider": { + "name": "sso_provider", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "issuer": { + "name": "issuer", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "domain": { + "name": "domain", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "oidc_config": { + "name": "oidc_config", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "saml_config": { + "name": "saml_config", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider_id": { + "name": "provider_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "domain_verified": { + "name": "domain_verified", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "jit_provisioning_enabled": { + "name": "jit_provisioning_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + } + }, + "indexes": { + "sso_provider_provider_id_unique": { + "name": "sso_provider_provider_id_unique", + "columns": [ + { + "expression": "provider_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "sso_provider_domain_idx": { + "name": "sso_provider_domain_idx", + "columns": [ + { + "expression": "domain", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "sso_provider_user_id_idx": { + "name": "sso_provider_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "sso_provider_organization_id_idx": { + "name": "sso_provider_organization_id_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "sso_provider_user_id_user_id_fk": { + "name": "sso_provider_user_id_user_id_fk", + "tableFrom": "sso_provider", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "sso_provider_organization_id_organization_id_fk": { + "name": "sso_provider_organization_id_organization_id_fk", + "tableFrom": "sso_provider", + "tableTo": "organization", + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.subscription": { + "name": "subscription", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "plan": { + "name": "plan", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "reference_id": { + "name": "reference_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "stripe_customer_id": { + "name": "stripe_customer_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "stripe_subscription_id": { + "name": "stripe_subscription_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "period_start": { + "name": "period_start", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "period_end": { + "name": "period_end", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "cancel_at_period_end": { + "name": "cancel_at_period_end", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "cancel_at": { + "name": "cancel_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "canceled_at": { + "name": "canceled_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "ended_at": { + "name": "ended_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "seats": { + "name": "seats", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "trial_start": { + "name": "trial_start", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "trial_end": { + "name": "trial_end", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "billing_interval": { + "name": "billing_interval", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "stripe_schedule_id": { + "name": "stripe_schedule_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "metadata": { + "name": "metadata", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "last_closed_period_start": { + "name": "last_closed_period_start", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "subscription_reference_status_idx": { + "name": "subscription_reference_status_idx", + "columns": [ + { + "expression": "reference_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "subscription_cycle_close_lagging_idx": { + "name": "subscription_cycle_close_lagging_idx", + "columns": [ + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"subscription\".\"status\" in ('active', 'past_due') and \"subscription\".\"period_start\" is not null and (\"subscription\".\"last_closed_period_start\" is null or \"subscription\".\"last_closed_period_start\" < \"subscription\".\"period_start\")", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "check_enterprise_metadata": { + "name": "check_enterprise_metadata", + "value": "plan != 'enterprise' OR metadata IS NOT NULL" + } + }, + "isRLSEnabled": false + }, + "public.table_jobs": { + "name": "table_jobs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "table_id": { + "name": "table_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'running'" + }, + "payload": { + "name": "payload", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "rows_processed": { + "name": "rows_processed", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "error": { + "name": "error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "started_at": { + "name": "started_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "table_jobs_one_active_per_table": { + "name": "table_jobs_one_active_per_table", + "columns": [ + { + "expression": "table_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"table_jobs\".\"status\" = 'running' AND \"table_jobs\".\"type\" <> 'export'", + "concurrently": false, + "method": "btree", + "with": {} + }, + "table_jobs_watchdog_idx": { + "name": "table_jobs_watchdog_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "table_jobs_table_started_idx": { + "name": "table_jobs_table_started_idx", + "columns": [ + { + "expression": "table_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "started_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "table_jobs_table_id_user_table_definitions_id_fk": { + "name": "table_jobs_table_id_user_table_definitions_id_fk", + "tableFrom": "table_jobs", + "tableTo": "user_table_definitions", + "columnsFrom": ["table_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "table_jobs_workspace_id_workspace_id_fk": { + "name": "table_jobs_workspace_id_workspace_id_fk", + "tableFrom": "table_jobs", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.table_row_executions": { + "name": "table_row_executions", + "schema": "", + "columns": { + "table_id": { + "name": "table_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "row_id": { + "name": "row_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "group_id": { + "name": "group_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "execution_id": { + "name": "execution_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "job_id": { + "name": "job_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "error": { + "name": "error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "running_block_ids": { + "name": "running_block_ids", + "type": "text[]", + "primaryKey": false, + "notNull": true, + "default": "'{}'::text[]" + }, + "block_errors": { + "name": "block_errors", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "cancelled_at": { + "name": "cancelled_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "capability_governed_user_id": { + "name": "capability_governed_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "enrichment_details": { + "name": "enrichment_details", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "table_row_executions_table_status_idx": { + "name": "table_row_executions_table_status_idx", + "columns": [ + { + "expression": "table_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"table_row_executions\".\"status\" IN ('queued', 'running', 'pending')", + "concurrently": false, + "method": "btree", + "with": {} + }, + "table_row_executions_execution_id_idx": { + "name": "table_row_executions_execution_id_idx", + "columns": [ + { + "expression": "execution_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"table_row_executions\".\"execution_id\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "table_row_executions_table_group_idx": { + "name": "table_row_executions_table_group_idx", + "columns": [ + { + "expression": "table_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "group_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "table_row_executions_table_id_user_table_definitions_id_fk": { + "name": "table_row_executions_table_id_user_table_definitions_id_fk", + "tableFrom": "table_row_executions", + "tableTo": "user_table_definitions", + "columnsFrom": ["table_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "table_row_executions_row_id_user_table_rows_id_fk": { + "name": "table_row_executions_row_id_user_table_rows_id_fk", + "tableFrom": "table_row_executions", + "tableTo": "user_table_rows", + "columnsFrom": ["row_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "table_row_executions_capability_governed_user_id_user_id_fk": { + "name": "table_row_executions_capability_governed_user_id_user_id_fk", + "tableFrom": "table_row_executions", + "tableTo": "user", + "columnsFrom": ["capability_governed_user_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "table_row_executions_row_id_group_id_pk": { + "name": "table_row_executions_row_id_group_id_pk", + "columns": ["row_id", "group_id"] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.table_run_dispatches": { + "name": "table_run_dispatches", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "table_id": { + "name": "table_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "request_id": { + "name": "request_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "mode": { + "name": "mode", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "scope": { + "name": "scope", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "cursor": { + "name": "cursor", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "limit": { + "name": "limit", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "processed_count": { + "name": "processed_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "is_manual_run": { + "name": "is_manual_run", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "triggered_by_user_id": { + "name": "triggered_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "capability_governed_user_id": { + "name": "capability_governed_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "requested_at": { + "name": "requested_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "heartbeat_at": { + "name": "heartbeat_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "cancelled_at": { + "name": "cancelled_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "table_run_dispatches_active_idx": { + "name": "table_run_dispatches_active_idx", + "columns": [ + { + "expression": "table_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "table_run_dispatches_watchdog_idx": { + "name": "table_run_dispatches_watchdog_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "requested_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "table_run_dispatches_governed_active_idx": { + "name": "table_run_dispatches_governed_active_idx", + "columns": [ + { + "expression": "capability_governed_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"table_run_dispatches\".\"status\" IN ('pending', 'dispatching')", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "table_run_dispatches_table_id_user_table_definitions_id_fk": { + "name": "table_run_dispatches_table_id_user_table_definitions_id_fk", + "tableFrom": "table_run_dispatches", + "tableTo": "user_table_definitions", + "columnsFrom": ["table_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "table_run_dispatches_workspace_id_workspace_id_fk": { + "name": "table_run_dispatches_workspace_id_workspace_id_fk", + "tableFrom": "table_run_dispatches", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "table_run_dispatches_triggered_by_user_id_user_id_fk": { + "name": "table_run_dispatches_triggered_by_user_id_user_id_fk", + "tableFrom": "table_run_dispatches", + "tableTo": "user", + "columnsFrom": ["triggered_by_user_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + }, + "table_run_dispatches_capability_governed_user_id_user_id_fk": { + "name": "table_run_dispatches_capability_governed_user_id_user_id_fk", + "tableFrom": "table_run_dispatches", + "tableTo": "user", + "columnsFrom": ["capability_governed_user_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.table_views": { + "name": "table_views", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "table_id": { + "name": "table_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "config": { + "name": "config", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "is_default": { + "name": "is_default", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "table_views_table_created_idx": { + "name": "table_views_table_created_idx", + "columns": [ + { + "expression": "table_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "table_views_workspace_created_idx": { + "name": "table_views_workspace_created_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "table_views_table_default_unique": { + "name": "table_views_table_default_unique", + "columns": [ + { + "expression": "table_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "is_default = true", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "table_views_table_id_user_table_definitions_id_fk": { + "name": "table_views_table_id_user_table_definitions_id_fk", + "tableFrom": "table_views", + "tableTo": "user_table_definitions", + "columnsFrom": ["table_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "table_views_workspace_id_workspace_id_fk": { + "name": "table_views_workspace_id_workspace_id_fk", + "tableFrom": "table_views", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "table_views_created_by_user_id_fk": { + "name": "table_views_created_by_user_id_fk", + "tableFrom": "table_views", + "tableTo": "user", + "columnsFrom": ["created_by"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.upload_session": { + "name": "upload_session", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "token_hash": { + "name": "token_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "knowledge_base_id": { + "name": "knowledge_base_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "execution_id": { + "name": "execution_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "purpose": { + "name": "purpose", + "type": "upload_session_purpose", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "method": { + "name": "method", + "type": "upload_session_method", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "storage_context": { + "name": "storage_context", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "final_key": { + "name": "final_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "storage_provider": { + "name": "storage_provider", + "type": "upload_session_provider", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "provider_upload_id": { + "name": "provider_upload_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "provider_object_version": { + "name": "provider_object_version", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "file_name": { + "name": "file_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "content_type": { + "name": "content_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "file_size": { + "name": "file_size", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "part_size": { + "name": "part_size", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "part_count": { + "name": "part_count", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "upload_session_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'uploading'" + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "processing_lease_id": { + "name": "processing_lease_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "processing_lease_expires_at": { + "name": "processing_lease_expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "completed_file_id": { + "name": "completed_file_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "error": { + "name": "error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "upload_session_token_hash_unique": { + "name": "upload_session_token_hash_unique", + "columns": [ + { + "expression": "token_hash", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "upload_session_final_key_unique": { + "name": "upload_session_final_key_unique", + "columns": [ + { + "expression": "final_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "upload_session_status_expires_at_idx": { + "name": "upload_session_status_expires_at_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "expires_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.usage_log": { + "name": "usage_log", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "category": { + "name": "category", + "type": "usage_log_category", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "source": { + "name": "source", + "type": "usage_log_source", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "cost": { + "name": "cost", + "type": "numeric", + "primaryKey": false, + "notNull": true + }, + "event_key": { + "name": "event_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "billing_entity_type": { + "name": "billing_entity_type", + "type": "billing_entity_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": false + }, + "billing_entity_id": { + "name": "billing_entity_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "billing_period_start": { + "name": "billing_period_start", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "billing_period_end": { + "name": "billing_period_end", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "execution_id": { + "name": "execution_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "usage_log_user_created_at_idx": { + "name": "usage_log_user_created_at_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "usage_log_source_idx": { + "name": "usage_log_source_idx", + "columns": [ + { + "expression": "source", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "usage_log_workflow_id_idx": { + "name": "usage_log_workflow_id_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "usage_log_event_key_unique": { + "name": "usage_log_event_key_unique", + "columns": [ + { + "expression": "event_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"usage_log\".\"event_key\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "usage_log_billing_entity_period_idx": { + "name": "usage_log_billing_entity_period_idx", + "columns": [ + { + "expression": "billing_entity_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "billing_entity_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "billing_period_start", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "billing_period_end", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"usage_log\".\"billing_entity_type\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "usage_log_billing_period_cost_idx": { + "name": "usage_log_billing_period_cost_idx", + "columns": [ + { + "expression": "billing_entity_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "billing_entity_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "billing_period_start", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "billing_period_end", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "source", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "cost", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"usage_log\".\"billing_entity_type\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "usage_log_billing_entity_created_at_cost_idx": { + "name": "usage_log_billing_entity_created_at_cost_idx", + "columns": [ + { + "expression": "billing_entity_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "billing_entity_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "source", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "cost", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"usage_log\".\"billing_entity_type\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "usage_log_workspace_created_at_idx": { + "name": "usage_log_workspace_created_at_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "usage_log_execution_id_idx": { + "name": "usage_log_execution_id_idx", + "columns": [ + { + "expression": "execution_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "usage_log_user_id_user_id_fk": { + "name": "usage_log_user_id_user_id_fk", + "tableFrom": "usage_log", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "usage_log_workspace_id_workspace_id_fk": { + "name": "usage_log_workspace_id_workspace_id_fk", + "tableFrom": "usage_log", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + }, + "usage_log_workflow_id_workflow_id_fk": { + "name": "usage_log_workflow_id_workflow_id_fk", + "tableFrom": "usage_log", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "usage_log_billing_scope_all_or_none": { + "name": "usage_log_billing_scope_all_or_none", + "value": "(\n (\"usage_log\".\"billing_entity_type\" IS NULL AND \"usage_log\".\"billing_entity_id\" IS NULL AND \"usage_log\".\"billing_period_start\" IS NULL AND \"usage_log\".\"billing_period_end\" IS NULL)\n OR\n (\"usage_log\".\"billing_entity_type\" IS NOT NULL AND \"usage_log\".\"billing_entity_id\" IS NOT NULL AND \"usage_log\".\"billing_period_start\" IS NOT NULL AND \"usage_log\".\"billing_period_end\" IS NOT NULL AND \"usage_log\".\"billing_period_start\" < \"usage_log\".\"billing_period_end\")\n )" + } + }, + "isRLSEnabled": false + }, + "public.user": { + "name": "user", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "normalized_email": { + "name": "normalized_email", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "email_verified": { + "name": "email_verified", + "type": "boolean", + "primaryKey": false, + "notNull": true + }, + "image": { + "name": "image", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "stripe_customer_id": { + "name": "stripe_customer_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'user'" + }, + "banned": { + "name": "banned", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "ban_reason": { + "name": "ban_reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "ban_expires": { + "name": "ban_expires", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "suspended_at": { + "name": "suspended_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "suspension_source": { + "name": "suspension_source", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "user_email_lower_idx": { + "name": "user_email_lower_idx", + "columns": [ + { + "expression": "lower(btrim(\"email\"))", + "asc": true, + "isExpression": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "user_email_unique": { + "name": "user_email_unique", + "nullsNotDistinct": false, + "columns": ["email"] + }, + "user_normalized_email_unique": { + "name": "user_normalized_email_unique", + "nullsNotDistinct": false, + "columns": ["normalized_email"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user_stats": { + "name": "user_stats", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "current_usage_limit": { + "name": "current_usage_limit", + "type": "numeric", + "primaryKey": false, + "notNull": false, + "default": "'5'" + }, + "usage_limit_updated_at": { + "name": "usage_limit_updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "last_period_cost": { + "name": "last_period_cost", + "type": "numeric", + "primaryKey": false, + "notNull": false, + "default": "'0'" + }, + "billed_overage_this_period": { + "name": "billed_overage_this_period", + "type": "numeric", + "primaryKey": false, + "notNull": true, + "default": "'0'" + }, + "credit_balance": { + "name": "credit_balance", + "type": "numeric", + "primaryKey": false, + "notNull": true, + "default": "'0'" + }, + "last_period_copilot_cost": { + "name": "last_period_copilot_cost", + "type": "numeric", + "primaryKey": false, + "notNull": false, + "default": "'0'" + }, + "storage_used_bytes": { + "name": "storage_used_bytes", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "billing_blocked": { + "name": "billing_blocked", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "billing_blocked_reason": { + "name": "billing_blocked_reason", + "type": "billing_blocked_reason", + "typeSchema": "public", + "primaryKey": false, + "notNull": false + }, + "limit_notifications": { + "name": "limit_notifications", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + } + }, + "indexes": {}, + "foreignKeys": { + "user_stats_user_id_user_id_fk": { + "name": "user_stats_user_id_user_id_fk", + "tableFrom": "user_stats", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "user_stats_user_id_unique": { + "name": "user_stats_user_id_unique", + "nullsNotDistinct": false, + "columns": ["user_id"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user_table_definitions": { + "name": "user_table_definitions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "folder_id": { + "name": "folder_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "schema": { + "name": "schema", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "max_rows": { + "name": "max_rows", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 10000 + }, + "row_count": { + "name": "row_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "rows_version": { + "name": "rows_version", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "schema_locked": { + "name": "schema_locked", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "insert_locked": { + "name": "insert_locked", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "update_locked": { + "name": "update_locked", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "delete_locked": { + "name": "delete_locked", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "archived_at": { + "name": "archived_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "user_table_def_workspace_id_idx": { + "name": "user_table_def_workspace_id_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "user_table_def_folder_id_idx": { + "name": "user_table_def_folder_id_idx", + "columns": [ + { + "expression": "folder_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "user_table_def_workspace_name_unique": { + "name": "user_table_def_workspace_name_unique", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"user_table_definitions\".\"archived_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "user_table_def_archived_at_idx": { + "name": "user_table_def_archived_at_idx", + "columns": [ + { + "expression": "archived_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "user_table_def_workspace_archived_partial_idx": { + "name": "user_table_def_workspace_archived_partial_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "archived_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"user_table_definitions\".\"archived_at\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "user_table_definitions_workspace_id_workspace_id_fk": { + "name": "user_table_definitions_workspace_id_workspace_id_fk", + "tableFrom": "user_table_definitions", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "user_table_definitions_folder_id_folder_id_fk": { + "name": "user_table_definitions_folder_id_folder_id_fk", + "tableFrom": "user_table_definitions", + "tableTo": "folder", + "columnsFrom": ["folder_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + }, + "user_table_definitions_created_by_user_id_fk": { + "name": "user_table_definitions_created_by_user_id_fk", + "tableFrom": "user_table_definitions", + "tableTo": "user", + "columnsFrom": ["created_by"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user_table_row_changes": { + "name": "user_table_row_changes", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "bigint", + "primaryKey": true, + "notNull": true, + "identity": { + "type": "always", + "name": "user_table_row_changes_id_seq", + "schema": "public", + "increment": "1", + "startWith": "1", + "minValue": "1", + "maxValue": "9223372036854775807", + "cache": "1", + "cycle": false + } + }, + "table_id": { + "name": "table_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "row_delta": { + "name": "row_delta", + "type": "integer", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "user_table_row_changes_table_id_idx": { + "name": "user_table_row_changes_table_id_idx", + "columns": [ + { + "expression": "table_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "user_table_row_changes_table_id_user_table_definitions_id_fk": { + "name": "user_table_row_changes_table_id_user_table_definitions_id_fk", + "tableFrom": "user_table_row_changes", + "tableTo": "user_table_definitions", + "columnsFrom": ["table_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user_table_row_secret_provenance": { + "name": "user_table_row_secret_provenance", + "schema": "", + "columns": { + "row_id": { + "name": "row_id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "content_updated_at": { + "name": "content_updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "entries": { + "name": "entries", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "user_table_row_secret_provenance_row_id_user_table_rows_id_fk": { + "name": "user_table_row_secret_provenance_row_id_user_table_rows_id_fk", + "tableFrom": "user_table_row_secret_provenance", + "tableTo": "user_table_rows", + "columnsFrom": ["row_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "user_table_row_secret_provenance_status_check": { + "name": "user_table_row_secret_provenance_status_check", + "value": "\"user_table_row_secret_provenance\".\"status\" IN ('exact', 'unknown')" + } + }, + "isRLSEnabled": false + }, + "public.user_table_rows": { + "name": "user_table_rows", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "table_id": { + "name": "table_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "data": { + "name": "data", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "position": { + "name": "position", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "order_key": { + "name": "order_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "secret_provenance_version": { + "name": "secret_provenance_version", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "user_table_rows_tenant_data_gin_idx": { + "name": "user_table_rows_tenant_data_gin_idx", + "columns": [ + { + "expression": "table_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "\"data\" jsonb_path_ops", + "asc": true, + "isExpression": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "gin", + "with": {} + }, + "user_table_rows_workspace_table_idx": { + "name": "user_table_rows_workspace_table_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "table_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "user_table_rows_table_position_idx": { + "name": "user_table_rows_table_position_idx", + "columns": [ + { + "expression": "table_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "position", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "user_table_rows_table_order_key_idx": { + "name": "user_table_rows_table_order_key_idx", + "columns": [ + { + "expression": "table_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "order_key", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "user_table_rows_table_created_id_idx": { + "name": "user_table_rows_table_created_id_idx", + "columns": [ + { + "expression": "table_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "user_table_rows_table_id_id_idx": { + "name": "user_table_rows_table_id_id_idx", + "columns": [ + { + "expression": "table_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "user_table_rows_table_id_user_table_definitions_id_fk": { + "name": "user_table_rows_table_id_user_table_definitions_id_fk", + "tableFrom": "user_table_rows", + "tableTo": "user_table_definitions", + "columnsFrom": ["table_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "user_table_rows_workspace_id_workspace_id_fk": { + "name": "user_table_rows_workspace_id_workspace_id_fk", + "tableFrom": "user_table_rows", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "user_table_rows_created_by_user_id_fk": { + "name": "user_table_rows_created_by_user_id_fk", + "tableFrom": "user_table_rows", + "tableTo": "user", + "columnsFrom": ["created_by"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.verification": { + "name": "verification", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "identifier": { + "name": "identifier", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "verification_identifier_idx": { + "name": "verification_identifier_idx", + "columns": [ + { + "expression": "identifier", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "verification_expires_at_idx": { + "name": "verification_expires_at_idx", + "columns": [ + { + "expression": "expires_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.waitlist": { + "name": "waitlist", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "waitlist_email_unique": { + "name": "waitlist_email_unique", + "nullsNotDistinct": false, + "columns": ["email"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.webhook": { + "name": "webhook", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "deployment_version_id": { + "name": "deployment_version_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "registration_status": { + "name": "registration_status", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "registration_generation": { + "name": "registration_generation", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "config_fingerprint": { + "name": "config_fingerprint", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "prepared_at": { + "name": "prepared_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "block_id": { + "name": "block_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "path": { + "name": "path", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "routing_key": { + "name": "routing_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "provider_config": { + "name": "provider_config", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "is_active": { + "name": "is_active", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "failed_count": { + "name": "failed_count", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 0 + }, + "last_failed_at": { + "name": "last_failed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "archived_at": { + "name": "archived_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "path_deployment_unique": { + "name": "path_deployment_unique", + "columns": [ + { + "expression": "path", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "deployment_version_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"webhook\".\"archived_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "webhook_workflow_deployment_idx": { + "name": "webhook_workflow_deployment_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "deployment_version_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "webhook_routing_key_active_idx": { + "name": "webhook_routing_key_active_idx", + "columns": [ + { + "expression": "routing_key", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "provider", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"webhook\".\"archived_at\" IS NULL AND \"webhook\".\"routing_key\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "webhook_archived_at_partial_idx": { + "name": "webhook_archived_at_partial_idx", + "columns": [ + { + "expression": "archived_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"webhook\".\"archived_at\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_webhook_on_provider_is_active_workflow_id_deploym_bdeed5468": { + "name": "idx_webhook_on_provider_is_active_workflow_id_deploym_bdeed5468", + "columns": [ + { + "expression": "provider", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "is_active", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "deployment_version_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_webhook_on_workflow_id_block_id_updated_at_desc": { + "name": "idx_webhook_on_workflow_id_block_id_updated_at_desc", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "block_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "updated_at", + "isExpression": false, + "asc": false, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "webhook_active_registration_unique": { + "name": "webhook_active_registration_unique", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "block_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"webhook\".\"registration_status\" = 'active' AND \"webhook\".\"block_id\" IS NOT NULL AND \"webhook\".\"archived_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "webhook_candidate_registration_unique": { + "name": "webhook_candidate_registration_unique", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "block_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"webhook\".\"registration_status\" = 'candidate' AND \"webhook\".\"block_id\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "webhook_registration_status_generation_idx": { + "name": "webhook_registration_status_generation_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "registration_status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "registration_generation", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "webhook_workflow_id_workflow_id_fk": { + "name": "webhook_workflow_id_workflow_id_fk", + "tableFrom": "webhook", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "webhook_deployment_version_id_workflow_deployment_version_id_fk": { + "name": "webhook_deployment_version_id_workflow_deployment_version_id_fk", + "tableFrom": "webhook", + "tableTo": "workflow_deployment_version", + "columnsFrom": ["deployment_version_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "webhook_registration_status_check": { + "name": "webhook_registration_status_check", + "value": "\"webhook\".\"registration_status\" IS NULL OR \"webhook\".\"registration_status\" IN ('active', 'candidate', 'retired', 'orphaned')" + }, + "webhook_registration_generation_check": { + "name": "webhook_registration_generation_check", + "value": "\"webhook\".\"registration_generation\" IS NULL OR \"webhook\".\"registration_generation\" >= 0" + } + }, + "isRLSEnabled": false + }, + "public.webhook_path_claim": { + "name": "webhook_path_claim", + "schema": "", + "columns": { + "path": { + "name": "path", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "generation": { + "name": "generation", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "webhook_path_claim_workflow_idx": { + "name": "webhook_path_claim_workflow_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "webhook_path_claim_workflow_id_workflow_id_fk": { + "name": "webhook_path_claim_workflow_id_workflow_id_fk", + "tableFrom": "webhook_path_claim", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "webhook_path_claim_generation_check": { + "name": "webhook_path_claim_generation_check", + "value": "\"webhook_path_claim\".\"generation\" >= 0" + } + }, + "isRLSEnabled": false + }, + "public.workflow": { + "name": "workflow", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "folder_id": { + "name": "folder_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "sort_order": { + "name": "sort_order", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "last_synced": { + "name": "last_synced", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "is_deployed": { + "name": "is_deployed", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "deployed_at": { + "name": "deployed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "is_public_api": { + "name": "is_public_api", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "locked": { + "name": "locked", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "fork_sync_excluded": { + "name": "fork_sync_excluded", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "run_count": { + "name": "run_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "last_run_at": { + "name": "last_run_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "variables": { + "name": "variables", + "type": "json", + "primaryKey": false, + "notNull": false, + "default": "'{}'" + }, + "archived_at": { + "name": "archived_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "workflow_user_id_idx": { + "name": "workflow_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_workspace_id_idx": { + "name": "workflow_workspace_id_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_user_workspace_idx": { + "name": "workflow_user_workspace_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_workspace_folder_name_active_unique": { + "name": "workflow_workspace_folder_name_active_unique", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "coalesce(\"folder_id\", '')", + "asc": true, + "isExpression": true, + "nulls": "last" + }, + { + "expression": "name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"workflow\".\"archived_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_folder_sort_idx": { + "name": "workflow_folder_sort_idx", + "columns": [ + { + "expression": "folder_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "sort_order", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_active_workspace_sort_idx": { + "name": "workflow_active_workspace_sort_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "sort_order", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"workflow\".\"archived_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_archived_at_idx": { + "name": "workflow_archived_at_idx", + "columns": [ + { + "expression": "archived_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_workspace_archived_partial_idx": { + "name": "workflow_workspace_archived_partial_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "archived_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"workflow\".\"archived_at\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workflow_user_id_user_id_fk": { + "name": "workflow_user_id_user_id_fk", + "tableFrom": "workflow", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workflow_workspace_id_workspace_id_fk": { + "name": "workflow_workspace_id_workspace_id_fk", + "tableFrom": "workflow", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workflow_folder_id_folder_id_fk": { + "name": "workflow_folder_id_folder_id_fk", + "tableFrom": "workflow", + "tableTo": "folder", + "columnsFrom": ["folder_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workflow_blocks": { + "name": "workflow_blocks", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "position_x": { + "name": "position_x", + "type": "numeric", + "primaryKey": false, + "notNull": true + }, + "position_y": { + "name": "position_y", + "type": "numeric", + "primaryKey": false, + "notNull": true + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "horizontal_handles": { + "name": "horizontal_handles", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "is_wide": { + "name": "is_wide", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "advanced_mode": { + "name": "advanced_mode", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "trigger_mode": { + "name": "trigger_mode", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "error_enabled": { + "name": "error_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "retry": { + "name": "retry", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "locked": { + "name": "locked", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "height": { + "name": "height", + "type": "numeric", + "primaryKey": false, + "notNull": true, + "default": "'0'" + }, + "sub_blocks": { + "name": "sub_blocks", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "outputs": { + "name": "outputs", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "data": { + "name": "data", + "type": "jsonb", + "primaryKey": false, + "notNull": false, + "default": "'{}'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workflow_blocks_workflow_id_idx": { + "name": "workflow_blocks_workflow_id_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_blocks_type_idx": { + "name": "workflow_blocks_type_idx", + "columns": [ + { + "expression": "type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workflow_blocks_workflow_id_workflow_id_fk": { + "name": "workflow_blocks_workflow_id_workflow_id_fk", + "tableFrom": "workflow_blocks", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workflow_checkpoints": { + "name": "workflow_checkpoints", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "chat_id": { + "name": "chat_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "message_id": { + "name": "message_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "workflow_state": { + "name": "workflow_state", + "type": "json", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workflow_checkpoints_user_id_idx": { + "name": "workflow_checkpoints_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_checkpoints_workflow_id_idx": { + "name": "workflow_checkpoints_workflow_id_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_checkpoints_chat_id_idx": { + "name": "workflow_checkpoints_chat_id_idx", + "columns": [ + { + "expression": "chat_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_checkpoints_message_id_idx": { + "name": "workflow_checkpoints_message_id_idx", + "columns": [ + { + "expression": "message_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_checkpoints_user_workflow_idx": { + "name": "workflow_checkpoints_user_workflow_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_checkpoints_workflow_chat_idx": { + "name": "workflow_checkpoints_workflow_chat_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "chat_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_checkpoints_created_at_idx": { + "name": "workflow_checkpoints_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_checkpoints_chat_created_at_idx": { + "name": "workflow_checkpoints_chat_created_at_idx", + "columns": [ + { + "expression": "chat_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workflow_checkpoints_user_id_user_id_fk": { + "name": "workflow_checkpoints_user_id_user_id_fk", + "tableFrom": "workflow_checkpoints", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workflow_checkpoints_workflow_id_workflow_id_fk": { + "name": "workflow_checkpoints_workflow_id_workflow_id_fk", + "tableFrom": "workflow_checkpoints", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workflow_checkpoints_chat_id_copilot_chats_id_fk": { + "name": "workflow_checkpoints_chat_id_copilot_chats_id_fk", + "tableFrom": "workflow_checkpoints", + "tableTo": "copilot_chats", + "columnsFrom": ["chat_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workflow_deployment_operation": { + "name": "workflow_deployment_operation", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "deployment_version_id": { + "name": "deployment_version_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "version": { + "name": "version", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "previous_active_version_id": { + "name": "previous_active_version_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "action": { + "name": "action", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "protocol_version": { + "name": "protocol_version", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "generation": { + "name": "generation", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'preparing'" + }, + "component_readiness": { + "name": "component_readiness", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "error_code": { + "name": "error_code", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "error_message": { + "name": "error_message", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "idempotency_key": { + "name": "idempotency_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "request_hash": { + "name": "request_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "actor_id": { + "name": "actor_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workflow_deployment_operation_workflow_generation_unique": { + "name": "workflow_deployment_operation_workflow_generation_unique", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "generation", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_deployment_operation_workflow_idempotency_unique": { + "name": "workflow_deployment_operation_workflow_idempotency_unique", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "idempotency_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"workflow_deployment_operation\".\"idempotency_key\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_deployment_operation_workflow_in_flight_unique": { + "name": "workflow_deployment_operation_workflow_in_flight_unique", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"workflow_deployment_operation\".\"status\" IN ('preparing', 'activating')", + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_deployment_operation_workflow_status_idx": { + "name": "workflow_deployment_operation_workflow_status_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_deployment_operation_deployment_version_idx": { + "name": "workflow_deployment_operation_deployment_version_idx", + "columns": [ + { + "expression": "deployment_version_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_deployment_operation_workflow_version_generation_idx": { + "name": "workflow_deployment_operation_workflow_version_generation_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "deployment_version_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "generation", + "isExpression": false, + "asc": false, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workflow_deployment_operation_workflow_id_workflow_id_fk": { + "name": "workflow_deployment_operation_workflow_id_workflow_id_fk", + "tableFrom": "workflow_deployment_operation", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workflow_deployment_operation_deployment_version_id_workflow_deployment_version_id_fk": { + "name": "workflow_deployment_operation_deployment_version_id_workflow_deployment_version_id_fk", + "tableFrom": "workflow_deployment_operation", + "tableTo": "workflow_deployment_version", + "columnsFrom": ["deployment_version_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workflow_deployment_operation_previous_active_version_id_workflow_deployment_version_id_fk": { + "name": "workflow_deployment_operation_previous_active_version_id_workflow_deployment_version_id_fk", + "tableFrom": "workflow_deployment_operation", + "tableTo": "workflow_deployment_version", + "columnsFrom": ["previous_active_version_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "workflow_deployment_operation_action_check": { + "name": "workflow_deployment_operation_action_check", + "value": "\"workflow_deployment_operation\".\"action\" IN ('deploy', 'activate')" + }, + "workflow_deployment_operation_status_check": { + "name": "workflow_deployment_operation_status_check", + "value": "\"workflow_deployment_operation\".\"status\" IN ('preparing', 'activating', 'active', 'failed', 'superseded')" + }, + "workflow_deployment_operation_generation_check": { + "name": "workflow_deployment_operation_generation_check", + "value": "\"workflow_deployment_operation\".\"generation\" > 0" + }, + "workflow_deployment_operation_protocol_version_check": { + "name": "workflow_deployment_operation_protocol_version_check", + "value": "\"workflow_deployment_operation\".\"protocol_version\" > 0" + } + }, + "isRLSEnabled": false + }, + "public.workflow_deployment_version": { + "name": "workflow_deployment_version", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "version": { + "name": "version", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "state": { + "name": "state", + "type": "json", + "primaryKey": false, + "notNull": true + }, + "is_active": { + "name": "is_active", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "workflow_deployment_version_workflow_version_unique": { + "name": "workflow_deployment_version_workflow_version_unique", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "version", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_deployment_version_workflow_active_idx": { + "name": "workflow_deployment_version_workflow_active_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "is_active", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_deployment_version_created_at_idx": { + "name": "workflow_deployment_version_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workflow_deployment_version_workflow_id_workflow_id_fk": { + "name": "workflow_deployment_version_workflow_id_workflow_id_fk", + "tableFrom": "workflow_deployment_version", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workflow_edges": { + "name": "workflow_edges", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "source_block_id": { + "name": "source_block_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "target_block_id": { + "name": "target_block_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "source_handle": { + "name": "source_handle", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "target_handle": { + "name": "target_handle", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workflow_edges_workflow_id_idx": { + "name": "workflow_edges_workflow_id_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_edges_workflow_source_idx": { + "name": "workflow_edges_workflow_source_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "source_block_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_edges_workflow_target_idx": { + "name": "workflow_edges_workflow_target_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "target_block_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workflow_edges_workflow_id_workflow_id_fk": { + "name": "workflow_edges_workflow_id_workflow_id_fk", + "tableFrom": "workflow_edges", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workflow_edges_source_block_id_workflow_blocks_id_fk": { + "name": "workflow_edges_source_block_id_workflow_blocks_id_fk", + "tableFrom": "workflow_edges", + "tableTo": "workflow_blocks", + "columnsFrom": ["source_block_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workflow_edges_target_block_id_workflow_blocks_id_fk": { + "name": "workflow_edges_target_block_id_workflow_blocks_id_fk", + "tableFrom": "workflow_edges", + "tableTo": "workflow_blocks", + "columnsFrom": ["target_block_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workflow_execution_logs": { + "name": "workflow_execution_logs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "execution_id": { + "name": "execution_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "state_snapshot_id": { + "name": "state_snapshot_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "deployment_version_id": { + "name": "deployment_version_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "level": { + "name": "level", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'running'" + }, + "trigger": { + "name": "trigger", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "started_at": { + "name": "started_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "execution_deadline_at": { + "name": "execution_deadline_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "ended_at": { + "name": "ended_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "total_duration_ms": { + "name": "total_duration_ms", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "execution_data": { + "name": "execution_data", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "cost_total": { + "name": "cost_total", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "models_used": { + "name": "models_used", + "type": "text[]", + "primaryKey": false, + "notNull": false + }, + "files": { + "name": "files", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workflow_execution_logs_workflow_id_idx": { + "name": "workflow_execution_logs_workflow_id_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_execution_logs_state_snapshot_id_idx": { + "name": "workflow_execution_logs_state_snapshot_id_idx", + "columns": [ + { + "expression": "state_snapshot_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_execution_logs_deployment_version_id_idx": { + "name": "workflow_execution_logs_deployment_version_id_idx", + "columns": [ + { + "expression": "deployment_version_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_execution_logs_trigger_idx": { + "name": "workflow_execution_logs_trigger_idx", + "columns": [ + { + "expression": "trigger", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_execution_logs_level_idx": { + "name": "workflow_execution_logs_level_idx", + "columns": [ + { + "expression": "level", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_execution_logs_started_at_idx": { + "name": "workflow_execution_logs_started_at_idx", + "columns": [ + { + "expression": "started_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_execution_logs_execution_id_unique": { + "name": "workflow_execution_logs_execution_id_unique", + "columns": [ + { + "expression": "execution_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_execution_logs_workflow_started_at_idx": { + "name": "workflow_execution_logs_workflow_started_at_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "started_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_execution_logs_workspace_started_at_idx": { + "name": "workflow_execution_logs_workspace_started_at_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "started_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_execution_logs_workspace_activity_idx": { + "name": "workflow_execution_logs_workspace_activity_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "started_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "total_duration_ms", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "trigger", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": true, + "method": "btree", + "with": {} + }, + "workflow_execution_logs_workspace_started_at_id_desc_idx": { + "name": "workflow_execution_logs_workspace_started_at_id_desc_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "\"started_at\" DESC NULLS LAST", + "asc": true, + "isExpression": true, + "nulls": "last" + }, + { + "expression": "\"id\" DESC", + "asc": true, + "isExpression": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_execution_logs_workspace_cost_total_idx": { + "name": "workflow_execution_logs_workspace_cost_total_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "cost_total", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_execution_logs_models_used_idx": { + "name": "workflow_execution_logs_models_used_idx", + "columns": [ + { + "expression": "models_used", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "gin", + "with": {} + }, + "workflow_execution_logs_workspace_ended_at_id_idx": { + "name": "workflow_execution_logs_workspace_ended_at_id_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "date_trunc('milliseconds', \"ended_at\")", + "asc": true, + "isExpression": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_execution_logs_running_started_at_idx": { + "name": "workflow_execution_logs_running_started_at_idx", + "columns": [ + { + "expression": "started_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "status = 'running'", + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_execution_logs_running_deadline_idx": { + "name": "workflow_execution_logs_running_deadline_idx", + "columns": [ + { + "expression": "execution_deadline_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"workflow_execution_logs\".\"status\" = 'running' AND \"workflow_execution_logs\".\"execution_deadline_at\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_execution_logs_redacting_started_at_idx": { + "name": "workflow_execution_logs_redacting_started_at_idx", + "columns": [ + { + "expression": "started_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "status = 'redacting'", + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_execution_logs_redacting_deadline_idx": { + "name": "workflow_execution_logs_redacting_deadline_idx", + "columns": [ + { + "expression": "execution_deadline_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"workflow_execution_logs\".\"status\" = 'redacting' AND \"workflow_execution_logs\".\"execution_deadline_at\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_execution_logs_completed_ended_at_idx": { + "name": "workflow_execution_logs_completed_ended_at_idx", + "columns": [ + { + "expression": "ended_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "execution_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"workflow_execution_logs\".\"status\" = 'completed' AND \"workflow_execution_logs\".\"level\" = 'info' AND \"workflow_execution_logs\".\"ended_at\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workflow_execution_logs_workflow_id_workflow_id_fk": { + "name": "workflow_execution_logs_workflow_id_workflow_id_fk", + "tableFrom": "workflow_execution_logs", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + }, + "workflow_execution_logs_workspace_id_workspace_id_fk": { + "name": "workflow_execution_logs_workspace_id_workspace_id_fk", + "tableFrom": "workflow_execution_logs", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workflow_execution_logs_state_snapshot_id_workflow_execution_snapshots_id_fk": { + "name": "workflow_execution_logs_state_snapshot_id_workflow_execution_snapshots_id_fk", + "tableFrom": "workflow_execution_logs", + "tableTo": "workflow_execution_snapshots", + "columnsFrom": ["state_snapshot_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + }, + "workflow_execution_logs_deployment_version_id_workflow_deployment_version_id_fk": { + "name": "workflow_execution_logs_deployment_version_id_workflow_deployment_version_id_fk", + "tableFrom": "workflow_execution_logs", + "tableTo": "workflow_deployment_version", + "columnsFrom": ["deployment_version_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workflow_execution_snapshots": { + "name": "workflow_execution_snapshots", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "state_hash": { + "name": "state_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "state_data": { + "name": "state_data", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workflow_snapshots_workflow_id_idx": { + "name": "workflow_snapshots_workflow_id_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_snapshots_hash_idx": { + "name": "workflow_snapshots_hash_idx", + "columns": [ + { + "expression": "state_hash", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_snapshots_workflow_hash_idx": { + "name": "workflow_snapshots_workflow_hash_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "state_hash", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_snapshots_created_at_idx": { + "name": "workflow_snapshots_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workflow_execution_snapshots_workflow_id_workflow_id_fk": { + "name": "workflow_execution_snapshots_workflow_id_workflow_id_fk", + "tableFrom": "workflow_execution_snapshots", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workflow_mcp_server": { + "name": "workflow_mcp_server", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "is_public": { + "name": "is_public", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workflow_mcp_server_workspace_id_idx": { + "name": "workflow_mcp_server_workspace_id_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_mcp_server_created_by_idx": { + "name": "workflow_mcp_server_created_by_idx", + "columns": [ + { + "expression": "created_by", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_mcp_server_deleted_at_idx": { + "name": "workflow_mcp_server_deleted_at_idx", + "columns": [ + { + "expression": "deleted_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_mcp_server_workspace_deleted_partial_idx": { + "name": "workflow_mcp_server_workspace_deleted_partial_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "deleted_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"workflow_mcp_server\".\"deleted_at\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workflow_mcp_server_workspace_id_workspace_id_fk": { + "name": "workflow_mcp_server_workspace_id_workspace_id_fk", + "tableFrom": "workflow_mcp_server", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workflow_mcp_server_created_by_user_id_fk": { + "name": "workflow_mcp_server_created_by_user_id_fk", + "tableFrom": "workflow_mcp_server", + "tableTo": "user", + "columnsFrom": ["created_by"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workflow_mcp_tool": { + "name": "workflow_mcp_tool", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "server_id": { + "name": "server_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "tool_name": { + "name": "tool_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "tool_description": { + "name": "tool_description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "parameter_schema": { + "name": "parameter_schema", + "type": "json", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "parameter_description_overrides": { + "name": "parameter_description_overrides", + "type": "json", + "primaryKey": false, + "notNull": true, + "default": "'{}'::json" + }, + "archived_at": { + "name": "archived_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workflow_mcp_tool_server_id_idx": { + "name": "workflow_mcp_tool_server_id_idx", + "columns": [ + { + "expression": "server_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_mcp_tool_workflow_id_idx": { + "name": "workflow_mcp_tool_workflow_id_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_mcp_tool_server_workflow_unique": { + "name": "workflow_mcp_tool_server_workflow_unique", + "columns": [ + { + "expression": "server_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"workflow_mcp_tool\".\"archived_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_mcp_tool_archived_at_partial_idx": { + "name": "workflow_mcp_tool_archived_at_partial_idx", + "columns": [ + { + "expression": "archived_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"workflow_mcp_tool\".\"archived_at\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workflow_mcp_tool_server_id_workflow_mcp_server_id_fk": { + "name": "workflow_mcp_tool_server_id_workflow_mcp_server_id_fk", + "tableFrom": "workflow_mcp_tool", + "tableTo": "workflow_mcp_server", + "columnsFrom": ["server_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workflow_mcp_tool_workflow_id_workflow_id_fk": { + "name": "workflow_mcp_tool_workflow_id_workflow_id_fk", + "tableFrom": "workflow_mcp_tool", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workflow_schedule": { + "name": "workflow_schedule", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "deployment_version_id": { + "name": "deployment_version_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "deployment_operation_id": { + "name": "deployment_operation_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "block_id": { + "name": "block_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "cron_expression": { + "name": "cron_expression", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "next_run_at": { + "name": "next_run_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "last_ran_at": { + "name": "last_ran_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "last_queued_at": { + "name": "last_queued_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "trigger_type": { + "name": "trigger_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "timezone": { + "name": "timezone", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'UTC'" + }, + "failed_count": { + "name": "failed_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "infra_retry_count": { + "name": "infra_retry_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'active'" + }, + "last_failed_at": { + "name": "last_failed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "source_type": { + "name": "source_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'workflow'" + }, + "job_title": { + "name": "job_title", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "prompt": { + "name": "prompt", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "lifecycle": { + "name": "lifecycle", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'persistent'" + }, + "success_condition": { + "name": "success_condition", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "max_runs": { + "name": "max_runs", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "run_count": { + "name": "run_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "source_chat_id": { + "name": "source_chat_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "source_task_name": { + "name": "source_task_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "source_user_id": { + "name": "source_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "source_workspace_id": { + "name": "source_workspace_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "secret_scope": { + "name": "secret_scope", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'all'" + }, + "mounted_secrets": { + "name": "mounted_secrets", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "job_history": { + "name": "job_history", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "contexts": { + "name": "contexts", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "excluded_dates": { + "name": "excluded_dates", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "ends_at": { + "name": "ends_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "archived_at": { + "name": "archived_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workflow_schedule_workflow_block_deployment_unique": { + "name": "workflow_schedule_workflow_block_deployment_unique", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "block_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "deployment_version_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"workflow_schedule\".\"archived_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_schedule_workflow_deployment_idx": { + "name": "workflow_schedule_workflow_deployment_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "deployment_version_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_schedule_archived_at_partial_idx": { + "name": "workflow_schedule_archived_at_partial_idx", + "columns": [ + { + "expression": "archived_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"workflow_schedule\".\"archived_at\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_workflow_schedule_on_source_workspace_id_source_t_c07f3bba6": { + "name": "idx_workflow_schedule_on_source_workspace_id_source_t_c07f3bba6", + "columns": [ + { + "expression": "source_workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "source_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "archived_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_schedule_due_workflow_idx": { + "name": "workflow_schedule_due_workflow_idx", + "columns": [ + { + "expression": "next_run_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "last_queued_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "deployment_version_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"workflow_schedule\".\"archived_at\" IS NULL AND \"workflow_schedule\".\"status\" NOT IN ('disabled', 'completed') AND (\"workflow_schedule\".\"source_type\" = 'workflow' OR \"workflow_schedule\".\"source_type\" IS NULL)", + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_schedule_due_job_idx": { + "name": "workflow_schedule_due_job_idx", + "columns": [ + { + "expression": "next_run_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "last_queued_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"workflow_schedule\".\"archived_at\" IS NULL AND \"workflow_schedule\".\"status\" NOT IN ('disabled', 'completed') AND \"workflow_schedule\".\"source_type\" = 'job'", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workflow_schedule_workflow_id_workflow_id_fk": { + "name": "workflow_schedule_workflow_id_workflow_id_fk", + "tableFrom": "workflow_schedule", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workflow_schedule_deployment_version_id_workflow_deployment_version_id_fk": { + "name": "workflow_schedule_deployment_version_id_workflow_deployment_version_id_fk", + "tableFrom": "workflow_schedule", + "tableTo": "workflow_deployment_version", + "columnsFrom": ["deployment_version_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workflow_schedule_deployment_operation_id_workflow_deployment_operation_id_fk": { + "name": "workflow_schedule_deployment_operation_id_workflow_deployment_operation_id_fk", + "tableFrom": "workflow_schedule", + "tableTo": "workflow_deployment_operation", + "columnsFrom": ["deployment_operation_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + }, + "workflow_schedule_source_user_id_user_id_fk": { + "name": "workflow_schedule_source_user_id_user_id_fk", + "tableFrom": "workflow_schedule", + "tableTo": "user", + "columnsFrom": ["source_user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workflow_schedule_source_workspace_id_workspace_id_fk": { + "name": "workflow_schedule_source_workspace_id_workspace_id_fk", + "tableFrom": "workflow_schedule", + "tableTo": "workspace", + "columnsFrom": ["source_workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workflow_subflows": { + "name": "workflow_subflows", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "config": { + "name": "config", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workflow_subflows_workflow_id_idx": { + "name": "workflow_subflows_workflow_id_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_subflows_workflow_type_idx": { + "name": "workflow_subflows_workflow_type_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workflow_subflows_workflow_id_workflow_id_fk": { + "name": "workflow_subflows_workflow_id_workflow_id_fk", + "tableFrom": "workflow_subflows", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workflow_test": { + "name": "workflow_test", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "body_file_id": { + "name": "body_file_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "cases": { + "name": "cases", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'" + }, + "source_hash": { + "name": "source_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_by_user_id": { + "name": "created_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workflow_test_workspace_name_unique": { + "name": "workflow_test_workspace_name_unique", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"workflow_test\".\"deleted_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_test_body_file_unique": { + "name": "workflow_test_body_file_unique", + "columns": [ + { + "expression": "body_file_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workflow_test_workspace_id_workspace_id_fk": { + "name": "workflow_test_workspace_id_workspace_id_fk", + "tableFrom": "workflow_test", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workflow_test_body_file_id_workspace_files_id_fk": { + "name": "workflow_test_body_file_id_workspace_files_id_fk", + "tableFrom": "workflow_test", + "tableTo": "workspace_files", + "columnsFrom": ["body_file_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + }, + "workflow_test_created_by_user_id_user_id_fk": { + "name": "workflow_test_created_by_user_id_user_id_fk", + "tableFrom": "workflow_test", + "tableTo": "user", + "columnsFrom": ["created_by_user_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workflow_test_run": { + "name": "workflow_test_run", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "test_id": { + "name": "test_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "version": { + "name": "version", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'running'" + }, + "passed": { + "name": "passed", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "failed": { + "name": "failed", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "skipped": { + "name": "skipped", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "report": { + "name": "report", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "error": { + "name": "error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "source_hash": { + "name": "source_hash", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "ran_against": { + "name": "ran_against", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "progress": { + "name": "progress", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "triggered_by_actor": { + "name": "triggered_by_actor", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "triggered_by_user_id": { + "name": "triggered_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "started_at": { + "name": "started_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "workflow_test_run_test_version_started_idx": { + "name": "workflow_test_run_test_version_started_idx", + "columns": [ + { + "expression": "test_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "version", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "started_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workflow_test_run_test_id_workflow_test_id_fk": { + "name": "workflow_test_run_test_id_workflow_test_id_fk", + "tableFrom": "workflow_test_run", + "tableTo": "workflow_test", + "columnsFrom": ["test_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workflow_test_run_workspace_id_workspace_id_fk": { + "name": "workflow_test_run_workspace_id_workspace_id_fk", + "tableFrom": "workflow_test_run", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workflow_test_run_triggered_by_user_id_user_id_fk": { + "name": "workflow_test_run_triggered_by_user_id_user_id_fk", + "tableFrom": "workflow_test_run", + "tableTo": "user", + "columnsFrom": ["triggered_by_user_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "workflow_test_run_version_check": { + "name": "workflow_test_run_version_check", + "value": "\"workflow_test_run\".\"version\" IN ('draft', 'deployed')" + }, + "workflow_test_run_status_check": { + "name": "workflow_test_run_status_check", + "value": "\"workflow_test_run\".\"status\" IN ('running', 'passed', 'failed', 'error')" + }, + "workflow_test_run_completed_check": { + "name": "workflow_test_run_completed_check", + "value": "(\"workflow_test_run\".\"status\" = 'running') = (\"workflow_test_run\".\"completed_at\" IS NULL)" + } + }, + "isRLSEnabled": false + }, + "public.workspace": { + "name": "workspace", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "color": { + "name": "color", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'#33C482'" + }, + "logo_url": { + "name": "logo_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "owner_id": { + "name": "owner_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "project_id": { + "name": "project_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workspace_mode": { + "name": "workspace_mode", + "type": "workspace_mode", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'grandfathered_shared'" + }, + "billed_account_user_id": { + "name": "billed_account_user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "storage_used_bytes": { + "name": "storage_used_bytes", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "allow_personal_api_keys": { + "name": "allow_personal_api_keys", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "inbox_enabled": { + "name": "inbox_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "inbox_address": { + "name": "inbox_address", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "inbox_provider_id": { + "name": "inbox_provider_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "inbox_secret_scope": { + "name": "inbox_secret_scope", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'all'" + }, + "inbox_mounted_secrets": { + "name": "inbox_mounted_secrets", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "archived_at": { + "name": "archived_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "organization_assigned_at": { + "name": "organization_assigned_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "forked_from_workspace_id": { + "name": "forked_from_workspace_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "fork_sync_new_workflows_excluded": { + "name": "fork_sync_new_workflows_excluded", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "organization_scope_key": { + "name": "organization_scope_key", + "type": "text", + "primaryKey": false, + "notNull": false, + "generated": { + "as": "CASE WHEN organization_id IS NULL THEN 'personal' ELSE 'organization:' || organization_id END", + "type": "stored" + } + } + }, + "indexes": { + "workspace_owner_id_idx": { + "name": "workspace_owner_id_idx", + "columns": [ + { + "expression": "owner_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_organization_id_idx": { + "name": "workspace_organization_id_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_project_id_id_idx": { + "name": "workspace_project_id_id_idx", + "columns": [ + { + "expression": "project_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": true, + "method": "btree", + "with": {} + }, + "workspace_mode_idx": { + "name": "workspace_mode_idx", + "columns": [ + { + "expression": "workspace_mode", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_forked_from_workspace_id_idx": { + "name": "workspace_forked_from_workspace_id_idx", + "columns": [ + { + "expression": "forked_from_workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_inbox_provider_id_idx": { + "name": "workspace_inbox_provider_id_idx", + "columns": [ + { + "expression": "inbox_provider_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"workspace\".\"inbox_provider_id\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workspace_owner_id_user_id_fk": { + "name": "workspace_owner_id_user_id_fk", + "tableFrom": "workspace", + "tableTo": "user", + "columnsFrom": ["owner_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workspace_organization_id_organization_id_fk": { + "name": "workspace_organization_id_organization_id_fk", + "tableFrom": "workspace", + "tableTo": "organization", + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + }, + "workspace_project_id_project_id_fk": { + "name": "workspace_project_id_project_id_fk", + "tableFrom": "workspace", + "tableTo": "project", + "columnsFrom": ["project_id"], + "columnsTo": ["id"], + "onDelete": "restrict", + "onUpdate": "no action" + }, + "workspace_billed_account_user_id_user_id_fk": { + "name": "workspace_billed_account_user_id_user_id_fk", + "tableFrom": "workspace", + "tableTo": "user", + "columnsFrom": ["billed_account_user_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + }, + "workspace_forked_from_workspace_id_workspace_id_fk": { + "name": "workspace_forked_from_workspace_id_workspace_id_fk", + "tableFrom": "workspace", + "tableTo": "workspace", + "columnsFrom": ["forked_from_workspace_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + }, + "workspace_project_organization_fk": { + "name": "workspace_project_organization_fk", + "tableFrom": "workspace", + "tableTo": "project", + "columnsFrom": ["project_id", "organization_scope_key"], + "columnsTo": ["id", "organization_scope_key"], + "onDelete": "no action", + "onUpdate": "no action" + }, + "workspace_fork_project_fk": { + "name": "workspace_fork_project_fk", + "tableFrom": "workspace", + "tableTo": "workspace", + "columnsFrom": ["forked_from_workspace_id", "project_id"], + "columnsTo": ["id", "project_id"], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "workspace_id_project_unique": { + "name": "workspace_id_project_unique", + "nullsNotDistinct": false, + "columns": ["id", "project_id"] + } + }, + "policies": {}, + "checkConstraints": { + "workspace_storage_used_bytes_non_negative": { + "name": "workspace_storage_used_bytes_non_negative", + "value": "\"workspace\".\"storage_used_bytes\" >= 0" + } + }, + "isRLSEnabled": false + }, + "public.workspace_byok_keys": { + "name": "workspace_byok_keys", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider_id": { + "name": "provider_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "encrypted_api_key": { + "name": "encrypted_api_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workspace_byok_workspace_provider_idx": { + "name": "workspace_byok_workspace_provider_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "provider_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workspace_byok_keys_workspace_id_workspace_id_fk": { + "name": "workspace_byok_keys_workspace_id_workspace_id_fk", + "tableFrom": "workspace_byok_keys", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workspace_byok_keys_created_by_user_id_fk": { + "name": "workspace_byok_keys_created_by_user_id_fk", + "tableFrom": "workspace_byok_keys", + "tableTo": "user", + "columnsFrom": ["created_by"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workspace_environment": { + "name": "workspace_environment", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "variables": { + "name": "variables", + "type": "json", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workspace_environment_workspace_unique": { + "name": "workspace_environment_workspace_unique", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workspace_environment_workspace_id_workspace_id_fk": { + "name": "workspace_environment_workspace_id_workspace_id_fk", + "tableFrom": "workspace_environment", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workspace_file": { + "name": "workspace_file", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "size": { + "name": "size", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "uploaded_by": { + "name": "uploaded_by", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "uploaded_at": { + "name": "uploaded_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workspace_file_workspace_id_idx": { + "name": "workspace_file_workspace_id_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_file_deleted_at_idx": { + "name": "workspace_file_deleted_at_idx", + "columns": [ + { + "expression": "deleted_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_file_workspace_deleted_partial_idx": { + "name": "workspace_file_workspace_deleted_partial_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "deleted_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"workspace_file\".\"deleted_at\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workspace_file_workspace_id_workspace_id_fk": { + "name": "workspace_file_workspace_id_workspace_id_fk", + "tableFrom": "workspace_file", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workspace_file_uploaded_by_user_id_fk": { + "name": "workspace_file_uploaded_by_user_id_fk", + "tableFrom": "workspace_file", + "tableTo": "user", + "columnsFrom": ["uploaded_by"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "workspace_file_key_unique": { + "name": "workspace_file_key_unique", + "nullsNotDistinct": false, + "columns": ["key"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workspace_file_collab_state": { + "name": "workspace_file_collab_state", + "schema": "", + "columns": { + "file_id": { + "name": "file_id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "doc_state": { + "name": "doc_state", + "type": "bytea", + "primaryKey": false, + "notNull": true + }, + "source_hash": { + "name": "source_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "workspace_file_collab_state_file_id_workspace_files_id_fk": { + "name": "workspace_file_collab_state_file_id_workspace_files_id_fk", + "tableFrom": "workspace_file_collab_state", + "tableTo": "workspace_files", + "columnsFrom": ["file_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workspace_file_search_backfill": { + "name": "workspace_file_search_backfill", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "after_workspace_id": { + "name": "after_workspace_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "after_file_id": { + "name": "after_file_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workspace_file_search_build": { + "name": "workspace_file_search_build", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "file_id": { + "name": "file_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "source_content_updated_at": { + "name": "source_content_updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "workspace_file_search_build_file_idx": { + "name": "workspace_file_search_build_file_idx", + "columns": [ + { + "expression": "file_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_file_search_build_cleanup_idx": { + "name": "workspace_file_search_build_cleanup_idx", + "columns": [ + { + "expression": "expires_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"workspace_file_search_build\".\"expires_at\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workspace_file_search_chunk": { + "name": "workspace_file_search_chunk", + "schema": "", + "columns": { + "build_id": { + "name": "build_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "ordinal": { + "name": "ordinal", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "line_start": { + "name": "line_start", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "fragment": { + "name": "fragment", + "type": "boolean", + "primaryKey": false, + "notNull": true + }, + "overlap": { + "name": "overlap", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "content": { + "name": "content", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "workspace_file_search_chunk_line_idx": { + "name": "workspace_file_search_chunk_line_idx", + "columns": [ + { + "expression": "build_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "line_start", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "ordinal", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_file_search_chunk_content_idx": { + "name": "workspace_file_search_chunk_content_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last", + "opclass": "text_ops" + }, + { + "expression": "content", + "isExpression": false, + "asc": true, + "nulls": "last", + "opclass": "gin_trgm_ops" + } + ], + "isUnique": false, + "concurrently": true, + "method": "gin", + "with": { + "fastupdate": "off" + } + } + }, + "foreignKeys": { + "workspace_file_search_chunk_build_id_workspace_file_search_build_id_fk": { + "name": "workspace_file_search_chunk_build_id_workspace_file_search_build_id_fk", + "tableFrom": "workspace_file_search_chunk", + "tableTo": "workspace_file_search_build", + "columnsFrom": ["build_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "workspace_file_search_chunk_pk": { + "name": "workspace_file_search_chunk_pk", + "columns": ["build_id", "ordinal"] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "workspace_file_search_chunk_content_size": { + "name": "workspace_file_search_chunk_content_size", + "value": "octet_length(\"workspace_file_search_chunk\".\"content\") <= 8192" + }, + "workspace_file_search_chunk_position": { + "name": "workspace_file_search_chunk_position", + "value": "\"workspace_file_search_chunk\".\"ordinal\" >= 0 AND \"workspace_file_search_chunk\".\"line_start\" > 0 AND \"workspace_file_search_chunk\".\"overlap\" BETWEEN 0 AND 2" + } + }, + "isRLSEnabled": false + }, + "public.workspace_file_search_dispatch_queue": { + "name": "workspace_file_search_dispatch_queue", + "schema": "", + "columns": { + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "enqueued_at": { + "name": "enqueued_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "last_dispatched_at": { + "name": "last_dispatched_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workspace_file_search_dispatch_queue_schedule_idx": { + "name": "workspace_file_search_dispatch_queue_schedule_idx", + "columns": [ + { + "expression": "last_dispatched_at", + "isExpression": false, + "asc": true, + "nulls": "first" + }, + { + "expression": "enqueued_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workspace_file_search_queue_workspace_fk": { + "name": "workspace_file_search_queue_workspace_fk", + "tableFrom": "workspace_file_search_dispatch_queue", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workspace_file_search_index": { + "name": "workspace_file_search_index", + "schema": "", + "columns": { + "file_id": { + "name": "file_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "source_content_updated_at": { + "name": "source_content_updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "workspace_file_search_index_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "partial": { + "name": "partial", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "failure_reason": { + "name": "failure_reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "line_count": { + "name": "line_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "indexed_bytes": { + "name": "indexed_bytes", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "dispatched_at": { + "name": "dispatched_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workspace_file_search_index_workspace_status_idx": { + "name": "workspace_file_search_index_workspace_status_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "source_content_updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_file_search_index_pending_dispatch_idx": { + "name": "workspace_file_search_index_pending_dispatch_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "file_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "source_content_updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"workspace_file_search_index\".\"status\" = 'pending' AND \"workspace_file_search_index\".\"dispatched_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_file_search_index_active_dispatch_idx": { + "name": "workspace_file_search_index_active_dispatch_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "dispatched_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"workspace_file_search_index\".\"status\" = 'pending' AND \"workspace_file_search_index\".\"dispatched_at\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workspace_file_search_index_file_fk": { + "name": "workspace_file_search_index_file_fk", + "tableFrom": "workspace_file_search_index", + "tableTo": "workspace_files", + "columnsFrom": ["file_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workspace_file_search_index_workspace_fk": { + "name": "workspace_file_search_index_workspace_fk", + "tableFrom": "workspace_file_search_index", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "workspace_file_search_index_pk": { + "name": "workspace_file_search_index_pk", + "columns": ["file_id", "source_content_updated_at"] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workspace_file_search_revision": { + "name": "workspace_file_search_revision", + "schema": "", + "columns": { + "file_id": { + "name": "file_id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "source_content_updated_at": { + "name": "source_content_updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "workspace_file_search_index_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "build_id": { + "name": "build_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "failure_reason": { + "name": "failure_reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "line_count": { + "name": "line_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "indexed_bytes": { + "name": "indexed_bytes", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "chunk_count": { + "name": "chunk_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "dispatched_at": { + "name": "dispatched_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "handoff_expires_at": { + "name": "handoff_expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workspace_file_search_revision_workspace_status_idx": { + "name": "workspace_file_search_revision_workspace_status_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_file_search_revision_build_idx": { + "name": "workspace_file_search_revision_build_idx", + "columns": [ + { + "expression": "build_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_file_search_revision_pending_idx": { + "name": "workspace_file_search_revision_pending_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "file_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "source_content_updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"workspace_file_search_revision\".\"status\" = 'pending' AND \"workspace_file_search_revision\".\"dispatched_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_file_search_revision_active_idx": { + "name": "workspace_file_search_revision_active_idx", + "columns": [ + { + "expression": "dispatched_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"workspace_file_search_revision\".\"status\" = 'pending' AND \"workspace_file_search_revision\".\"dispatched_at\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workspace_file_search_revision_file_id_workspace_files_id_fk": { + "name": "workspace_file_search_revision_file_id_workspace_files_id_fk", + "tableFrom": "workspace_file_search_revision", + "tableTo": "workspace_files", + "columnsFrom": ["file_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workspace_file_search_revision_build_id_workspace_file_search_build_id_fk": { + "name": "workspace_file_search_revision_build_id_workspace_file_search_build_id_fk", + "tableFrom": "workspace_file_search_revision", + "tableTo": "workspace_file_search_build", + "columnsFrom": ["build_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workspace_file_search_segment": { + "name": "workspace_file_search_segment", + "schema": "", + "columns": { + "file_id": { + "name": "file_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "source_content_updated_at": { + "name": "source_content_updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "line_number": { + "name": "line_number", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "segment_number": { + "name": "segment_number", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "segment_start": { + "name": "segment_start", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "line_length": { + "name": "line_length", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "content": { + "name": "content", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "workspace_file_search_segment_workspace_revision_idx": { + "name": "workspace_file_search_segment_workspace_revision_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "file_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "source_content_updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_file_search_segment_workspace_content_trgm_idx": { + "name": "workspace_file_search_segment_workspace_content_trgm_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last", + "opclass": "text_ops" + }, + { + "expression": "content", + "isExpression": false, + "asc": true, + "nulls": "last", + "opclass": "gin_trgm_ops" + } + ], + "isUnique": false, + "concurrently": false, + "method": "gin", + "with": {} + } + }, + "foreignKeys": { + "workspace_file_search_segment_file_fk": { + "name": "workspace_file_search_segment_file_fk", + "tableFrom": "workspace_file_search_segment", + "tableTo": "workspace_files", + "columnsFrom": ["file_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workspace_file_search_segment_workspace_fk": { + "name": "workspace_file_search_segment_workspace_fk", + "tableFrom": "workspace_file_search_segment", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "workspace_file_search_segment_pk": { + "name": "workspace_file_search_segment_pk", + "columns": ["file_id", "source_content_updated_at", "line_number", "segment_number"] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workspace_file_secret_provenance": { + "name": "workspace_file_secret_provenance", + "schema": "", + "columns": { + "file_id": { + "name": "file_id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "content_updated_at": { + "name": "content_updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "entries": { + "name": "entries", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "workspace_file_secret_provenance_file_id_workspace_files_id_fk": { + "name": "workspace_file_secret_provenance_file_id_workspace_files_id_fk", + "tableFrom": "workspace_file_secret_provenance", + "tableTo": "workspace_files", + "columnsFrom": ["file_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "workspace_file_secret_provenance_status_check": { + "name": "workspace_file_secret_provenance_status_check", + "value": "\"workspace_file_secret_provenance\".\"status\" IN ('exact', 'unknown', 'unrecorded')" + } + }, + "isRLSEnabled": false + }, + "public.workspace_file_version": { + "name": "workspace_file_version", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "file_id": { + "name": "file_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "version": { + "name": "version", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "size_bytes": { + "name": "size_bytes", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "content_type": { + "name": "content_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "content_hash": { + "name": "content_hash", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "superseded_at": { + "name": "superseded_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "source": { + "name": "source", + "type": "workspace_file_version_source", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "author_user_ids": { + "name": "author_user_ids", + "type": "text[]", + "primaryKey": false, + "notNull": true, + "default": "'{}'::text[]" + }, + "restored_from_version": { + "name": "restored_from_version", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "secret_provenance_status": { + "name": "secret_provenance_status", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "secret_provenance_entries": { + "name": "secret_provenance_entries", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workspace_file_version_file_version_unique": { + "name": "workspace_file_version_file_version_unique", + "columns": [ + { + "expression": "file_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "version", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_file_version_key_unique": { + "name": "workspace_file_version_key_unique", + "columns": [ + { + "expression": "key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_file_version_workspace_id_idx": { + "name": "workspace_file_version_workspace_id_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_file_version_workspace_superseded_idx": { + "name": "workspace_file_version_workspace_superseded_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "superseded_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"workspace_file_version\".\"superseded_at\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workspace_file_version_file_id_workspace_files_id_fk": { + "name": "workspace_file_version_file_id_workspace_files_id_fk", + "tableFrom": "workspace_file_version", + "tableTo": "workspace_files", + "columnsFrom": ["file_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workspace_file_version_workspace_id_workspace_id_fk": { + "name": "workspace_file_version_workspace_id_workspace_id_fk", + "tableFrom": "workspace_file_version", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "workspace_file_version_provenance_status_check": { + "name": "workspace_file_version_provenance_status_check", + "value": "\"workspace_file_version\".\"secret_provenance_status\" IS NULL OR \"workspace_file_version\".\"secret_provenance_status\" IN ('exact', 'unknown', 'unrecorded')" + } + }, + "isRLSEnabled": false + }, + "public.workspace_files": { + "name": "workspace_files", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "folder_id": { + "name": "folder_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "context": { + "name": "context", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "chat_id": { + "name": "chat_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "message_id": { + "name": "message_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "original_name": { + "name": "original_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "display_name": { + "name": "display_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "content_type": { + "name": "content_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "size_bytes": { + "name": "size_bytes", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "width": { + "name": "width", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "height": { + "name": "height", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "uploaded_at": { + "name": "uploaded_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "content_updated_at": { + "name": "content_updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "date_trunc('milliseconds', now())" + }, + "secret_provenance_version": { + "name": "secret_provenance_version", + "type": "integer", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "workspace_files_key_active_unique": { + "name": "workspace_files_key_active_unique", + "columns": [ + { + "expression": "key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"workspace_files\".\"deleted_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_files_workspace_folder_name_active_unique": { + "name": "workspace_files_workspace_folder_name_active_unique", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "coalesce(\"folder_id\", '')", + "asc": true, + "isExpression": true, + "nulls": "last" + }, + { + "expression": "original_name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"workspace_files\".\"deleted_at\" IS NULL AND \"workspace_files\".\"context\" = 'workspace' AND \"workspace_files\".\"workspace_id\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_files_workspace_active_keyset_idx": { + "name": "workspace_files_workspace_active_keyset_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"workspace_files\".\"deleted_at\" IS NULL AND \"workspace_files\".\"context\" = 'workspace' AND \"workspace_files\".\"workspace_id\" IS NOT NULL", + "concurrently": true, + "method": "btree", + "with": {} + }, + "workspace_files_chat_display_name_unique": { + "name": "workspace_files_chat_display_name_unique", + "columns": [ + { + "expression": "chat_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "display_name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"workspace_files\".\"context\" = 'mothership' AND \"workspace_files\".\"chat_id\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_files_organization_id_idx": { + "name": "workspace_files_organization_id_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_files_key_idx": { + "name": "workspace_files_key_idx", + "columns": [ + { + "expression": "key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_files_user_id_idx": { + "name": "workspace_files_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_files_workspace_id_idx": { + "name": "workspace_files_workspace_id_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_files_folder_id_idx": { + "name": "workspace_files_folder_id_idx", + "columns": [ + { + "expression": "folder_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_files_context_idx": { + "name": "workspace_files_context_idx", + "columns": [ + { + "expression": "context", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_files_chat_id_idx": { + "name": "workspace_files_chat_id_idx", + "columns": [ + { + "expression": "chat_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_files_deleted_at_idx": { + "name": "workspace_files_deleted_at_idx", + "columns": [ + { + "expression": "deleted_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_files_workspace_deleted_partial_idx": { + "name": "workspace_files_workspace_deleted_partial_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "deleted_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"workspace_files\".\"deleted_at\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workspace_files_user_id_user_id_fk": { + "name": "workspace_files_user_id_user_id_fk", + "tableFrom": "workspace_files", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workspace_files_workspace_id_workspace_id_fk": { + "name": "workspace_files_workspace_id_workspace_id_fk", + "tableFrom": "workspace_files", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workspace_files_organization_id_organization_id_fk": { + "name": "workspace_files_organization_id_organization_id_fk", + "tableFrom": "workspace_files", + "tableTo": "organization", + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workspace_files_folder_id_folder_id_fk": { + "name": "workspace_files_folder_id_folder_id_fk", + "tableFrom": "workspace_files", + "tableTo": "folder", + "columnsFrom": ["folder_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + }, + "workspace_files_chat_id_copilot_chats_id_fk": { + "name": "workspace_files_chat_id_copilot_chats_id_fk", + "tableFrom": "workspace_files", + "tableTo": "copilot_chats", + "columnsFrom": ["chat_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "workspace_files_organization_binding_check": { + "name": "workspace_files_organization_binding_check", + "value": "\"workspace_files\".\"organization_id\" IS NULL OR (\"workspace_files\".\"workspace_id\" IS NULL AND \"workspace_files\".\"context\" = 'knowledge-base' AND \"workspace_files\".\"folder_id\" IS NULL AND \"workspace_files\".\"chat_id\" IS NULL)" + } + }, + "isRLSEnabled": false + }, + "public.workspace_fork_block_map": { + "name": "workspace_fork_block_map", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "child_workspace_id": { + "name": "child_workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "parent_workflow_id": { + "name": "parent_workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "parent_block_id": { + "name": "parent_block_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "child_workflow_id": { + "name": "child_workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "child_block_id": { + "name": "child_block_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workspace_fork_block_map_child_ws_parent_unique": { + "name": "workspace_fork_block_map_child_ws_parent_unique", + "columns": [ + { + "expression": "child_workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "parent_block_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_fork_block_map_child_ws_child_unique": { + "name": "workspace_fork_block_map_child_ws_child_unique", + "columns": [ + { + "expression": "child_workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "child_block_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_fork_block_map_child_ws_parent_wf_idx": { + "name": "workspace_fork_block_map_child_ws_parent_wf_idx", + "columns": [ + { + "expression": "child_workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "parent_workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_fork_block_map_child_ws_child_wf_idx": { + "name": "workspace_fork_block_map_child_ws_child_wf_idx", + "columns": [ + { + "expression": "child_workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "child_workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workspace_fork_block_map_child_workspace_id_workspace_id_fk": { + "name": "workspace_fork_block_map_child_workspace_id_workspace_id_fk", + "tableFrom": "workspace_fork_block_map", + "tableTo": "workspace", + "columnsFrom": ["child_workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workspace_fork_dependent_value": { + "name": "workspace_fork_dependent_value", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "child_workspace_id": { + "name": "child_workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "target_workflow_id": { + "name": "target_workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "target_block_id": { + "name": "target_block_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "sub_block_key": { + "name": "sub_block_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workspace_fork_dependent_value_child_ws_wf_idx": { + "name": "workspace_fork_dependent_value_child_ws_wf_idx", + "columns": [ + { + "expression": "child_workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "target_workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_fork_dependent_value_field_unique": { + "name": "workspace_fork_dependent_value_field_unique", + "columns": [ + { + "expression": "child_workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "target_workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "target_block_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "sub_block_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workspace_fork_dependent_value_child_workspace_id_workspace_id_fk": { + "name": "workspace_fork_dependent_value_child_workspace_id_workspace_id_fk", + "tableFrom": "workspace_fork_dependent_value", + "tableTo": "workspace", + "columnsFrom": ["child_workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workspace_fork_promote_run": { + "name": "workspace_fork_promote_run", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "child_workspace_id": { + "name": "child_workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "source_workspace_id": { + "name": "source_workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "target_workspace_id": { + "name": "target_workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "direction": { + "name": "direction", + "type": "workspace_fork_promote_direction", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "snapshot": { + "name": "snapshot", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workspace_fork_promote_run_child_ws_target_unique": { + "name": "workspace_fork_promote_run_child_ws_target_unique", + "columns": [ + { + "expression": "child_workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "target_workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_fork_promote_run_target_ws_idx": { + "name": "workspace_fork_promote_run_target_ws_idx", + "columns": [ + { + "expression": "target_workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workspace_fork_promote_run_child_workspace_id_workspace_id_fk": { + "name": "workspace_fork_promote_run_child_workspace_id_workspace_id_fk", + "tableFrom": "workspace_fork_promote_run", + "tableTo": "workspace", + "columnsFrom": ["child_workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workspace_fork_promote_run_created_by_user_id_fk": { + "name": "workspace_fork_promote_run_created_by_user_id_fk", + "tableFrom": "workspace_fork_promote_run", + "tableTo": "user", + "columnsFrom": ["created_by"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workspace_fork_resource_map": { + "name": "workspace_fork_resource_map", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "child_workspace_id": { + "name": "child_workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "resource_type": { + "name": "resource_type", + "type": "workspace_fork_resource_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "parent_resource_id": { + "name": "parent_resource_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "child_resource_id": { + "name": "child_resource_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workspace_fork_resource_map_child_ws_idx": { + "name": "workspace_fork_resource_map_child_ws_idx", + "columns": [ + { + "expression": "child_workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_fork_resource_map_child_ws_type_idx": { + "name": "workspace_fork_resource_map_child_ws_type_idx", + "columns": [ + { + "expression": "child_workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "resource_type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_fork_resource_map_child_type_parent_unique": { + "name": "workspace_fork_resource_map_child_type_parent_unique", + "columns": [ + { + "expression": "child_workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "resource_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "parent_resource_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workspace_fork_resource_map_child_workspace_id_workspace_id_fk": { + "name": "workspace_fork_resource_map_child_workspace_id_workspace_id_fk", + "tableFrom": "workspace_fork_resource_map", + "tableTo": "workspace", + "columnsFrom": ["child_workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workspace_fork_resource_map_created_by_user_id_fk": { + "name": "workspace_fork_resource_map_created_by_user_id_fk", + "tableFrom": "workspace_fork_resource_map", + "tableTo": "user", + "columnsFrom": ["created_by"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workspace_fork_workflow_sync": { + "name": "workspace_fork_workflow_sync", + "schema": "", + "columns": { + "deployment_operation_id": { + "name": "deployment_operation_id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "child_workspace_id": { + "name": "child_workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "source_workflow_id": { + "name": "source_workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "target_workflow_id": { + "name": "target_workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "source_deployment_version_id": { + "name": "source_deployment_version_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "sequence": { + "name": "sequence", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "identity": { + "type": "always", + "name": "workspace_fork_workflow_sync_sequence_seq", + "schema": "public", + "increment": "1", + "startWith": "1", + "minValue": "1", + "maxValue": "9223372036854775807", + "cache": "1", + "cycle": false + } + }, + "promote_run_id": { + "name": "promote_run_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "activated_at": { + "name": "activated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "rollback_operation_id": { + "name": "rollback_operation_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "rolled_back_at": { + "name": "rolled_back_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "workspace_fork_workflow_sync_baseline_idx": { + "name": "workspace_fork_workflow_sync_baseline_idx", + "columns": [ + { + "expression": "child_workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "source_workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "target_workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "sequence", + "isExpression": false, + "asc": false, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"workspace_fork_workflow_sync\".\"activated_at\" IS NOT NULL AND \"workspace_fork_workflow_sync\".\"rolled_back_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_fork_workflow_sync_run_idx": { + "name": "workspace_fork_workflow_sync_run_idx", + "columns": [ + { + "expression": "promote_run_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "target_workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_fork_workflow_sync_rollback_idx": { + "name": "workspace_fork_workflow_sync_rollback_idx", + "columns": [ + { + "expression": "rollback_operation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"workspace_fork_workflow_sync\".\"rollback_operation_id\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_fork_workflow_sync_source_idx": { + "name": "workspace_fork_workflow_sync_source_idx", + "columns": [ + { + "expression": "source_workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_fork_workflow_sync_target_idx": { + "name": "workspace_fork_workflow_sync_target_idx", + "columns": [ + { + "expression": "target_workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_fork_workflow_sync_child_workspace_idx": { + "name": "workspace_fork_workflow_sync_child_workspace_idx", + "columns": [ + { + "expression": "child_workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workspace_fork_workflow_sync_child_workspace_id_workspace_id_fk": { + "name": "workspace_fork_workflow_sync_child_workspace_id_workspace_id_fk", + "tableFrom": "workspace_fork_workflow_sync", + "tableTo": "workspace", + "columnsFrom": ["child_workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workspace_fork_workflow_sync_source_workflow_id_workflow_id_fk": { + "name": "workspace_fork_workflow_sync_source_workflow_id_workflow_id_fk", + "tableFrom": "workspace_fork_workflow_sync", + "tableTo": "workflow", + "columnsFrom": ["source_workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workspace_fork_workflow_sync_target_workflow_id_workflow_id_fk": { + "name": "workspace_fork_workflow_sync_target_workflow_id_workflow_id_fk", + "tableFrom": "workspace_fork_workflow_sync", + "tableTo": "workflow", + "columnsFrom": ["target_workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workspace_operation_receipt": { + "name": "workspace_operation_receipt", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "request_id": { + "name": "request_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "request_hash": { + "name": "request_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "kind": { + "name": "kind", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "report": { + "name": "report", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workspace_operation_receipt_request_unique": { + "name": "workspace_operation_receipt_request_unique", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "request_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_operation_receipt_workspace_created_idx": { + "name": "workspace_operation_receipt_workspace_created_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workspace_operation_receipt_workspace_id_workspace_id_fk": { + "name": "workspace_operation_receipt_workspace_id_workspace_id_fk", + "tableFrom": "workspace_operation_receipt", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workspace_sandbox": { + "name": "workspace_sandbox", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "language": { + "name": "language", + "type": "sandbox_language", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "dependencies": { + "name": "dependencies", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "cli_tools": { + "name": "cli_tools", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "system_packages": { + "name": "system_packages", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "spec_hash": { + "name": "spec_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workspace_sandbox_workspace_name_unique": { + "name": "workspace_sandbox_workspace_name_unique", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_sandbox_workspace_idx": { + "name": "workspace_sandbox_workspace_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_sandbox_spec_hash_idx": { + "name": "workspace_sandbox_spec_hash_idx", + "columns": [ + { + "expression": "spec_hash", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workspace_sandbox_workspace_id_workspace_id_fk": { + "name": "workspace_sandbox_workspace_id_workspace_id_fk", + "tableFrom": "workspace_sandbox", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workspace_sandbox_created_by_user_id_fk": { + "name": "workspace_sandbox_created_by_user_id_fk", + "tableFrom": "workspace_sandbox", + "tableTo": "user", + "columnsFrom": ["created_by"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workspace_visit": { + "name": "workspace_visit", + "schema": "", + "columns": { + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "visited_at": { + "name": "visited_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workspace_visit_workspace_idx": { + "name": "workspace_visit_workspace_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workspace_visit_user_id_user_id_fk": { + "name": "workspace_visit_user_id_user_id_fk", + "tableFrom": "workspace_visit", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workspace_visit_workspace_id_workspace_id_fk": { + "name": "workspace_visit_workspace_id_workspace_id_fk", + "tableFrom": "workspace_visit", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "workspace_visit_user_id_workspace_id_pk": { + "name": "workspace_visit_user_id_workspace_id_pk", + "columns": ["user_id", "workspace_id"] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + } + }, + "enums": { + "public.academy_cert_status": { + "name": "academy_cert_status", + "schema": "public", + "values": ["active", "revoked", "expired"] + }, + "public.background_work_kind": { + "name": "background_work_kind", + "schema": "public", + "values": ["deployment_side_effects", "fork_content_copy", "fork_sync", "fork_rollback"] + }, + "public.background_work_status_value": { + "name": "background_work_status_value", + "schema": "public", + "values": ["pending", "processing", "completed", "completed_with_warnings", "failed"] + }, + "public.billing_blocked_reason": { + "name": "billing_blocked_reason", + "schema": "public", + "values": ["payment_failed", "dispute"] + }, + "public.billing_entity_type": { + "name": "billing_entity_type", + "schema": "public", + "values": ["user", "organization"] + }, + "public.chat_type": { + "name": "chat_type", + "schema": "public", + "values": ["mothership", "copilot"] + }, + "public.copilot_async_tool_status": { + "name": "copilot_async_tool_status", + "schema": "public", + "values": ["pending", "running", "completed", "failed", "cancelled", "delivered"] + }, + "public.copilot_run_status": { + "name": "copilot_run_status", + "schema": "public", + "values": ["active", "paused_waiting_for_tool", "resuming", "complete", "error", "cancelled"] + }, + "public.copilot_tool_permission_decision": { + "name": "copilot_tool_permission_decision", + "schema": "public", + "values": ["allow", "allow_chat", "always_allow", "skip"] + }, + "public.credential_group_enrollment_status": { + "name": "credential_group_enrollment_status", + "schema": "public", + "values": ["invited", "delivery_failed", "in_progress", "completed", "revoked"] + }, + "public.credential_group_status": { + "name": "credential_group_status", + "schema": "public", + "values": ["active", "disabled"] + }, + "public.credential_member_role": { + "name": "credential_member_role", + "schema": "public", + "values": ["admin", "member"] + }, + "public.credential_member_status": { + "name": "credential_member_status", + "schema": "public", + "values": ["active", "pending", "revoked"] + }, + "public.credential_type": { + "name": "credential_type", + "schema": "public", + "values": [ + "oauth", + "managed_oauth", + "managed_mcp", + "env_workspace", + "env_personal", + "service_account", + "personal_token" + ] + }, + "public.data_drain_cadence": { + "name": "data_drain_cadence", + "schema": "public", + "values": ["hourly", "daily"] + }, + "public.data_drain_destination": { + "name": "data_drain_destination", + "schema": "public", + "values": ["s3", "gcs", "azure_blob", "datadog", "bigquery", "snowflake", "webhook"] + }, + "public.data_drain_run_status": { + "name": "data_drain_run_status", + "schema": "public", + "values": ["running", "success", "failed"] + }, + "public.data_drain_run_trigger": { + "name": "data_drain_run_trigger", + "schema": "public", + "values": ["cron", "manual"] + }, + "public.data_drain_source": { + "name": "data_drain_source", + "schema": "public", + "values": ["workflow_logs", "job_logs", "audit_logs", "copilot_chats", "copilot_runs"] + }, + "public.execution_large_value_reference_source": { + "name": "execution_large_value_reference_source", + "schema": "public", + "values": ["execution_log", "paused_snapshot"] + }, + "public.folder_resource_type": { + "name": "folder_resource_type", + "schema": "public", + "values": ["workflow", "file", "knowledge_base", "table"] + }, + "public.invitation_kind": { + "name": "invitation_kind", + "schema": "public", + "values": ["organization", "workspace"] + }, + "public.invitation_membership_intent": { + "name": "invitation_membership_intent", + "schema": "public", + "values": ["internal", "external"] + }, + "public.invitation_status": { + "name": "invitation_status", + "schema": "public", + "values": ["pending", "accepted", "rejected", "cancelled", "expired"] + }, + "public.managed_oauth_credential_status": { + "name": "managed_oauth_credential_status", + "schema": "public", + "values": ["active", "needs_reauth", "revoked"] + }, + "public.permission_type": { + "name": "permission_type", + "schema": "public", + "values": ["admin", "write", "read"] + }, + "public.sandbox_image_status": { + "name": "sandbox_image_status", + "schema": "public", + "values": ["pending", "building", "ready", "failed"] + }, + "public.sandbox_language": { + "name": "sandbox_language", + "schema": "public", + "values": ["javascript", "python"] + }, + "public.secret_usage_scope": { + "name": "secret_usage_scope", + "schema": "public", + "values": ["workspace", "personal"] + }, + "public.secret_usage_source": { + "name": "secret_usage_source", + "schema": "public", + "values": ["workflow", "copilot", "mcp"] + }, + "public.upload_session_method": { + "name": "upload_session_method", + "schema": "public", + "values": ["put", "multipart"] + }, + "public.upload_session_provider": { + "name": "upload_session_provider", + "schema": "public", + "values": ["local", "s3", "blob", "gcs"] + }, + "public.upload_session_purpose": { + "name": "upload_session_purpose", + "schema": "public", + "values": [ + "workspace_file", + "table_import", + "knowledge_document", + "profile_picture", + "workspace_logo", + "organization_logo", + "mothership_attachment", + "execution_attachment" + ] + }, + "public.upload_session_status": { + "name": "upload_session_status", + "schema": "public", + "values": [ + "uploading", + "completing", + "finalizing", + "completed", + "aborting", + "aborted", + "failed", + "expired" + ] + }, + "public.usage_log_category": { + "name": "usage_log_category", + "schema": "public", + "values": ["model", "fixed", "tool", "model_unbilled"] + }, + "public.usage_log_source": { + "name": "usage_log_source", + "schema": "public", + "values": [ + "workflow", + "wand", + "copilot", + "workspace-chat", + "mcp_copilot", + "mothership_block", + "knowledge-base", + "voice-input", + "enrichment", + "voice-output", + "api-tool", + "workflow-test" + ] + }, + "public.workspace_file_search_index_status": { + "name": "workspace_file_search_index_status", + "schema": "public", + "values": ["pending", "ready", "skipped", "failed"] + }, + "public.workspace_file_version_source": { + "name": "workspace_file_version_source", + "schema": "public", + "values": ["upload", "user", "api", "copilot", "workflow", "collab", "revert", "unknown"] + }, + "public.workspace_fork_promote_direction": { + "name": "workspace_fork_promote_direction", + "schema": "public", + "values": ["push", "pull"] + }, + "public.workspace_fork_resource_type": { + "name": "workspace_fork_resource_type", + "schema": "public", + "values": [ + "workflow", + "oauth_credential", + "service_account_credential", + "env_var", + "table", + "knowledge_base", + "knowledge_document", + "file", + "file_folder", + "mcp_server", + "workflow_mcp_server", + "custom_block", + "custom_tool", + "skill", + "sandbox" + ] + }, + "public.workspace_mode": { + "name": "workspace_mode", + "schema": "public", + "values": ["personal", "organization", "grandfathered_shared"] + } + }, + "schemas": {}, + "sequences": { + "public.copilot_async_tool_calls_persist_seq": { + "name": "copilot_async_tool_calls_persist_seq", + "schema": "public", + "increment": "1", + "startWith": "1", + "minValue": "1", + "maxValue": "9223372036854775807", + "cache": "1", + "cycle": false + } + }, + "roles": {}, + "policies": {}, + "views": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} diff --git a/packages/db/migrations/meta/_journal.json b/packages/db/migrations/meta/_journal.json index 1ddfb5b1568..4a717f8cb2c 100644 --- a/packages/db/migrations/meta/_journal.json +++ b/packages/db/migrations/meta/_journal.json @@ -2843,6 +2843,13 @@ "when": 1791594144265, "tag": "0406_workspace_project_column", "breakpoints": true + }, + { + "idx": 407, + "version": "7", + "when": 1791594144266, + "tag": "0407_project_membership_enforcement", + "breakpoints": true } ] } diff --git a/packages/db/schema.ts b/packages/db/schema.ts index 1c36a1581d1..9cbd5c30150 100644 --- a/packages/db/schema.ts +++ b/packages/db/schema.ts @@ -2027,8 +2027,13 @@ export const workspace = pgTable( organizationId: text('organization_id').references(() => organization.id, { onDelete: 'set null', }), - /** Legacy memberships remain nullable until the registered Project backfill completes. */ - projectId: text('project_id').references(() => project.id, { onDelete: 'restrict' }), + /** Encodes nullable organization identity for the composite membership foreign key. */ + organizationScopeKey: text('organization_scope_key').generatedAlwaysAs( + sql`CASE WHEN organization_id IS NULL THEN 'personal' ELSE 'organization:' || organization_id END` + ), + projectId: text('project_id') + .notNull() + .references((): AnyPgColumn => project.id, { onDelete: 'restrict' }), workspaceMode: workspaceModeEnum('workspace_mode').notNull().default('grandfathered_shared'), billedAccountUserId: text('billed_account_user_id') .notNull() @@ -2068,6 +2073,19 @@ export const workspace = pgTable( updatedAt: timestamp('updated_at').notNull().defaultNow(), }, (table) => ({ + idProjectUnique: unique('workspace_id_project_unique').on(table.id, table.projectId), + /** Maintenance installs these two composite checks as DEFERRABLE INITIALLY DEFERRED. */ + projectOrganizationFk: foreignKey({ + name: 'workspace_project_organization_fk', + columns: [table.projectId, table.organizationScopeKey], + foreignColumns: [project.id, project.organizationScopeKey], + }), + /** The existing parent FK clears only the parent pointer on deletion. */ + forkProjectFk: foreignKey({ + name: 'workspace_fork_project_fk', + columns: [table.forkedFromWorkspaceId, table.projectId], + foreignColumns: [table.id, table.projectId], + }), ownerIdIdx: index('workspace_owner_id_idx').on(table.ownerId), organizationIdIdx: index('workspace_organization_id_idx').on(table.organizationId), projectIdIdx: index('workspace_project_id_id_idx').on(table.projectId, table.id).concurrently(), @@ -2100,6 +2118,10 @@ export const project = pgTable( organizationId: text('organization_id').references(() => organization.id, { onDelete: 'restrict', }), + /** Encodes nullable organization identity for the composite membership foreign key. */ + organizationScopeKey: text('organization_scope_key').generatedAlwaysAs( + sql`CASE WHEN organization_id IS NULL THEN 'personal' ELSE 'organization:' || organization_id END` + ), /** Lifecycle owner for personal and organization Projects; never an implicit access grant. */ ownerId: text('owner_id') .notNull() @@ -2109,6 +2131,10 @@ export const project = pgTable( updatedAt: timestamp('updated_at').notNull().defaultNow(), }, (table) => ({ + organizationScopeUnique: unique('project_id_organization_scope_unique').on( + table.id, + table.organizationScopeKey + ), nameLength: check( 'project_name_length', sql`char_length(btrim(${table.name})) BETWEEN 1 AND 100` @@ -2122,46 +2148,6 @@ export const project = pgTable( }) ) -/** contract-pending(after #8830 is fully deployed): #8590 drops this with the connector; readers recognize the completed schema. */ -export const projectMembershipRollout = pgTable( - 'project_membership_rollout', - { - id: text('id').primaryKey(), - phase: text('phase', { enum: ['connector', 'column'] }) - .notNull() - .default('connector'), - }, - (table) => ({ - singleton: check('project_membership_rollout_singleton', sql`${table.id} = 'membership'`), - phase: check( - 'project_membership_rollout_phase', - sql`${table.phase} IN ('connector', 'column')` - ), - }) -) - -// contract-pending(after project writers are fully deployed and backfill validates): enforce exactly-one membership and active Project environment minimums at commit. -export const projectWorkspace = pgTable( - 'project_workspace', - { - // contract-pending(after #8830 is fully deployed and pre-8830 servers/workers drain): retire this table in #8590 — the durable authority marker has switched to column and compatibility readers tolerate its absence. - /** @deprecated Use the phase-aware membership helpers; retained for pre-8830 binaries during rollout. */ - projectId: text('project_id') - .notNull() - .references(() => project.id, { onDelete: 'restrict' }), - /** @deprecated Use workspace.id. */ - workspaceId: text('workspace_id') - .notNull() - .references(() => workspace.id, { onDelete: 'cascade' }), - /** @deprecated Membership is stored on workspace. */ - createdAt: timestamp('created_at').notNull().defaultNow(), - }, - (table) => ({ - pk: primaryKey({ columns: [table.projectId, table.workspaceId] }), - workspaceUnique: uniqueIndex('project_workspace_workspace_id_unique').on(table.workspaceId), - }) -) - export const workspaceForkResourceTypeEnum = pgEnum('workspace_fork_resource_type', [ 'workflow', 'oauth_credential', diff --git a/packages/db/script-migrations/0013_backfill_legacy_knowledge_base_workspaces.integration.ts b/packages/db/script-migrations/0013_backfill_legacy_knowledge_base_workspaces.integration.ts index c4b00e14e28..2f8039dc120 100644 --- a/packages/db/script-migrations/0013_backfill_legacy_knowledge_base_workspaces.integration.ts +++ b/packages/db/script-migrations/0013_backfill_legacy_knowledge_base_workspaces.integration.ts @@ -46,6 +46,7 @@ describe('legacy KB workspace backfill in PostgreSQL', () => { connection: { search_path: schemaName }, onnotice: () => undefined, }) + await sql`ALTER TABLE workspace ALTER COLUMN project_id DROP NOT NULL` /** Reproduce the pre-0014 schema so this historical backfill can seed unscoped rows. */ await sql`ALTER TABLE knowledge_base DROP CONSTRAINT kb_owner_check` await sql`ALTER TABLE knowledge_base ADD CONSTRAINT kb_owner_check diff --git a/packages/db/script-migrations/0014_require_knowledge_base_owner.integration.ts b/packages/db/script-migrations/0014_require_knowledge_base_owner.integration.ts index a119631bd9f..c07b13658e7 100644 --- a/packages/db/script-migrations/0014_require_knowledge_base_owner.integration.ts +++ b/packages/db/script-migrations/0014_require_knowledge_base_owner.integration.ts @@ -41,6 +41,7 @@ describe('required KB ownership migration in PostgreSQL', () => { connection: { search_path: schema }, onnotice: () => undefined, }) + await sql`ALTER TABLE workspace ALTER COLUMN project_id DROP NOT NULL` migrationSql = await readFile( new URL('../migrations/0331_knowledge_base_required_owner.sql', import.meta.url), 'utf8' diff --git a/packages/db/script-migrations/0031_project_membership.ts b/packages/db/script-migrations/0031_project_membership.ts new file mode 100644 index 00000000000..c9b587cd5eb --- /dev/null +++ b/packages/db/script-migrations/0031_project_membership.ts @@ -0,0 +1,180 @@ +import { + assertProjectBackfillDatabase, + assignProjectBackfillBatch, + discoverProjectBackfill, + ProjectBackfillBusy, + ProjectBackfillConflict, + projectBackfillDatabaseId, + readProjectGroupingReviews, + verifyProjectBackfill, +} from '@sim/db/maintenance/project-backfill' +import { enforceProjectMembership } from '@sim/db/maintenance/project-enforcement' +import { countPendingProjectArchiveRepairs } from '@sim/db/maintenance/project-repairs' +import { switchProjectMembershipAuthority } from '@sim/db/maintenance/project-rollout' +import type { ScriptMigration } from '@sim/db/script-migrations/types' +import { createLogger } from '@sim/logger' +import { describeError, getTransientDatabaseFailure } from '@sim/utils/errors' +import { sleep } from '@sim/utils/helpers' +import { backoffWithJitter } from '@sim/utils/retry' + +const logger = createLogger('ProjectMembershipMigration') +function retryable(error: unknown): boolean { + const failure = getTransientDatabaseFailure(error) + return failure === 'capacity' || failure === 'conflict' +} +const MAX_DURATION_MS = 20 * 60_000 + +/** Commits bounded assignments before validation and contraction; retry discovers only remaining work. */ +export const projectMembershipMigration: ScriptMigration = { + name: '0031_project_membership', + async up(sql) { + if (sql.options.max !== 1) + throw new Error('Project migration requires the runner single-session connection') + await sql.unsafe("SET statement_timeout = '60s'") + await sql.unsafe("SET lock_timeout = '250ms'") + const [lock] = await sql`SELECT pg_backend_pid() AS pid, + pg_try_advisory_lock(hashtextextended('sim:project-backfill-operator', 0)) AS acquired` + if (!lock.acquired) + throw new Error('Project preparation is still running; retry after it finishes') + async function assertSession() { + const [state] = await sql`SELECT pg_backend_pid() AS pid, EXISTS ( + SELECT 1 FROM pg_locks WHERE pid = pg_backend_pid() AND locktype = 'advisory' AND granted + AND classid = ((hashtextextended('sim:project-backfill-operator',0) >> 32) & 4294967295)::oid + AND objid = (hashtextextended('sim:project-backfill-operator',0) & 4294967295)::oid + AND objsubid = 1) AS held` + if (state.pid !== lock.pid || !state.held) + throw new Error('Project migration session was lost; rerun to resume') + } + try { + await assertProjectBackfillDatabase(sql, true) + await switchProjectMembershipAuthority(sql) + await assertSession() + if (await countPendingProjectArchiveRepairs(sql)) { + throw new Error( + 'Project archive cleanup is incomplete; resume repair with the original manifest before retrying' + ) + } + const reviewPath = process.env.PROJECT_BACKFILL_REVIEW_PATH + const reviewUrl = process.env.MIGRATION_DATABASE_URL || process.env.DATABASE_URL + if (reviewPath && !reviewUrl) + throw new Error('Reviewed Project migration requires its database URL') + const databaseId = reviewUrl ? projectBackfillDatabaseId(reviewUrl) : 'migration-runner' + const reviews = reviewPath ? await readProjectGroupingReviews(reviewPath, databaseId) : [] + const manifest = await discoverProjectBackfill(sql, databaseId, reviews) + if (manifest.conflicts.length || manifest.repairs.length) { + logger.error('Project preparation requires operator remediation', { + conflicts: manifest.conflicts.slice(0, 50), + conflictCount: manifest.conflicts.length, + archiveRepairs: manifest.repairs.length, + }) + throw new ProjectBackfillConflict( + 'Resolve Project conflicts and archive/provider repairs with backfill-projects.ts plan/repair before retrying' + ) + } + const pending = manifest.families.map((family) => ({ family, attempts: 0 })) + const failed: string[] = [] + const started = performance.now() + let batchSize = 50 + let assigned = 0 + let batches = 0 + for (let position = 0; position < pending.length; ) { + if (performance.now() - started > MAX_DURATION_MS || batches >= 10_000) { + throw new Error( + 'Project backfill scheduling budget exhausted; rerun to resume committed progress' + ) + } + await assertSession() + const items = [pending[position++]] + if (items[0].family.members.length === 1 && items[0].attempts === 0) { + while ( + items.length < batchSize && + position < pending.length && + pending[position].family.members.length === 1 && + pending[position].attempts === 0 + ) { + items.push(pending[position++]) + } + } + const batchStarted = performance.now() + batches++ + try { + const result = await assignProjectBackfillBatch( + sql, + items.map((item) => item.family) + ) + assigned += result.assigned + const elapsedMs = performance.now() - batchStarted + if (elapsedMs > 250) batchSize = Math.max(1, Math.floor(batchSize / 2)) + logger.info('Project migration batch committed', { + ...result, + assigned, + batches, + elapsedMs, + batchSize, + }) + } catch (error) { + if ( + !(error instanceof ProjectBackfillBusy) && + !(error instanceof ProjectBackfillConflict) && + !retryable(error) + ) + throw error + await assertSession() + for (const item of items) { + if (!(error instanceof ProjectBackfillConflict) && ++item.attempts < 3) + pending.push(item) + else failed.push(item.family.rootId) + } + logger.warn('Project migration batch deferred', { + roots: items.map((item) => item.family.rootId), + error: describeError(error), + }) + await sleep(backoffWithJitter(items[0].attempts, null, { baseMs: 250, maxMs: 1000 })) + } + await sleep(100) + } + if (failed.length) { + logger.error('Project families still need retry or reconciliation', { + count: failed.length, + roots: failed.slice(0, 50), + }) + throw new ProjectBackfillConflict( + 'Project backfill incomplete; resolve contention or changed families and rerun' + ) + } + await assertSession() + const remaining = await discoverProjectBackfill(sql, databaseId, reviews) + if (remaining.conflicts.length || remaining.repairs.length || remaining.families.length) + throw new ProjectBackfillConflict( + 'Project evidence changed during preparation; rediscover and rerun' + ) + const counts = await verifyProjectBackfill(sql) + logger.info('Project migration verification', counts) + if (Object.values(counts).some((count) => count !== 0)) { + throw new ProjectBackfillConflict( + 'Project validation failed; reconcile the reported invariants before enforcement' + ) + } + for (let attempt = 0; ; attempt++) { + await assertSession() + try { + await enforceProjectMembership(sql) + break + } catch (error) { + if (attempt >= 2 || !retryable(error)) throw error + logger.warn('Project enforcement deferred by contention', { + error: describeError(error), + attempt, + }) + await sleep(backoffWithJitter(attempt, null, { baseMs: 250, maxMs: 1000 })) + } + } + await assertSession() + logger.info('Project backfill and enforcement complete', { assigned, batches }) + } finally { + await sql`SELECT pg_advisory_unlock(hashtextextended('sim:project-backfill-operator', 0))` + await sql.unsafe('SET statement_timeout = 0') + await sql.unsafe('SET lock_timeout = 0') + } + }, +} diff --git a/packages/db/script-migrations/index.ts b/packages/db/script-migrations/index.ts index e0a313b5327..ae0762265bc 100644 --- a/packages/db/script-migrations/index.ts +++ b/packages/db/script-migrations/index.ts @@ -9,6 +9,7 @@ import { projectionAclSkipUnfilledMigration } from '@sim/db/script-migrations/00 import { knowledgeProjectionAsyncMigration } from '@sim/db/script-migrations/0024_knowledge_projection_async' import { userTableSchemaForWriteMigration } from '@sim/db/script-migrations/0026_user_table_schema_for_write' import { retireKeywordWritersMigration } from '@sim/db/script-migrations/0030_retire_keyword_writers' +import { projectMembershipMigration } from '@sim/db/script-migrations/0031_project_membership' import type { Sql } from 'postgres' import { backfillTableOrderKeys } from './0001_backfill_table_order_keys' import { backfillPausedBillingAttribution } from './0002_backfill_paused_billing_attribution' @@ -55,6 +56,7 @@ export const scriptMigrations: readonly ScriptMigration[] = [ /** 0026 installs the schema guard every table row write takes before it validates. */ userTableSchemaForWriteMigration, retireKeywordWritersMigration, + projectMembershipMigration, /** * Search data retirement is an operator-run maintenance command, not a deploy step: * run `packages/db/scripts/retire-indexed-search.ts --help` for usage. diff --git a/packages/db/scripts/project-authority.integration.ts b/packages/db/scripts/project-authority.integration.ts new file mode 100644 index 00000000000..433516066c8 --- /dev/null +++ b/packages/db/scripts/project-authority.integration.ts @@ -0,0 +1,166 @@ +import { execFile } from 'node:child_process' +import { readFile } from 'node:fs/promises' +import { promisify } from 'node:util' +import { readProjectMembershipPhase } from '@sim/db/maintenance/project-rollout' +import { runScriptMigrations, scriptMigrations } from '@sim/db/script-migrations' +import { readTestDatabaseUrl } from '@sim/db/testing/test-infrastructure' +import { generateId } from '@sim/utils/id' +import postgres, { type Sql } from 'postgres' +import { describe, expect, it } from 'vitest' + +async function database(run: (sql: Sql, runner: Sql, url: string) => Promise) { + const admin = postgres(readTestDatabaseUrl(), { max: 1 }) + const name = `project_authority_test_${generateId().replaceAll('-', '')}` + const url = new URL(readTestDatabaseUrl()) + url.pathname = `/${name}` + await admin`CREATE DATABASE ${admin(name)}` + const sql = postgres(url.toString(), { max: 1, onnotice: () => {} }) + const runner = postgres(url.toString(), { max: 1, max_lifetime: null, onnotice: () => {} }) + try { + await sql.unsafe(` + CREATE TABLE "user" (id text PRIMARY KEY); + CREATE TABLE organization (id text PRIMARY KEY); + CREATE TABLE workspace (id text PRIMARY KEY, name text NOT NULL, owner_id text NOT NULL, + organization_id text, archived_at timestamp, forked_from_workspace_id text REFERENCES workspace(id)); + CREATE INDEX workspace_parent_idx ON workspace(forked_from_workspace_id); + CREATE TABLE workflow (id text PRIMARY KEY, workspace_id text REFERENCES workspace(id), archived_at timestamp); + INSERT INTO "user" VALUES ('owner'); + `) + await sql.unsafe( + await readFile(new URL('../migrations/0394_project_foundation.sql', import.meta.url), 'utf8') + ) + const expansion = await readFile( + new URL('../migrations/0406_workspace_project_column.sql', import.meta.url), + 'utf8' + ) + for (const statement of expansion.split('--> statement-breakpoint')) await sql.unsafe(statement) + await sql`INSERT INTO project (id,name,owner_id) VALUES ('retained','Retained','owner')` + await sql`INSERT INTO workspace (id,name,owner_id) VALUES ('env','Environment','owner')` + await sql`INSERT INTO project_workspace (project_id,workspace_id) VALUES ('retained','env')` + await run(sql, runner, url.toString()) + } finally { + await Promise.all([sql.end({ timeout: 2 }), runner.end({ timeout: 2 })]) + await admin`DROP DATABASE ${admin(name)} WITH (FORCE)` + await admin.end() + } +} + +function migrate(sql: Sql) { + return runScriptMigrations( + sql, + scriptMigrations.filter((entry) => entry.name === '0031_project_membership') + ) +} + +async function phase(sql: Sql) { + return [{ phase: await readProjectMembershipPhase(sql) }] +} + +describe('Project membership authority cutover', () => { + it('leaves connector authority intact while a legacy transaction is active and switches after it commits', async () => { + await database(async (sql, runner) => { + await sql.begin(async (tx) => { + await tx`UPDATE workspace SET name = 'Legacy write' WHERE id = 'env'` + await expect(migrate(runner)).rejects.toMatchObject({ code: '55P03' }) + expect(await phase(tx)).toEqual([{ phase: 'connector' }]) + expect(await tx`SELECT project_id FROM workspace`).toEqual([{ project_id: null }]) + }) + await migrate(runner) + expect(await phase(sql)).toEqual([{ phase: 'column' }]) + expect(await sql`SELECT to_regclass('public.project_membership_rollout') AS marker`).toEqual([ + { marker: null }, + ]) + expect(await sql`SELECT name,project_id FROM workspace`).toEqual([ + { name: 'Legacy write', project_id: 'retained' }, + ]) + expect(await sql`SELECT to_regclass('project_workspace') AS connector`).toEqual([ + { connector: null }, + ]) + }) + }) + + it('rejects acknowledged CLI writes before the authority switch', async () => { + await database(async (sql, _runner, url) => { + await expect( + promisify(execFile)( + 'bun', + [ + '--no-env-file', + 'scripts/backfill-projects.ts', + 'apply', + '--manifest', + '/nonexistent-project-plan.json', + '--report', + '/nonexistent-project-report.json', + '--ack-release-drained', + ], + { + cwd: new URL('../../../apps/sim/', import.meta.url), + env: { ...process.env, MIGRATION_DATABASE_URL: url }, + } + ) + ).rejects.toMatchObject({ code: 1, stderr: expect.stringContaining('authority must switch') }) + expect(await phase(sql)).toEqual([{ phase: 'connector' }]) + expect(await sql`SELECT project_id FROM workspace`).toEqual([{ project_id: null }]) + }) + }) + + it('keeps read-only connector transactions ahead of cutover', async () => { + await database(async (sql, runner) => { + await sql.begin('isolation level repeatable read read only', async (tx) => { + await tx`LOCK TABLE workspace IN ACCESS SHARE MODE` + expect(await phase(tx)).toEqual([{ phase: 'connector' }]) + await expect(migrate(runner)).rejects.toMatchObject({ code: '55P03' }) + expect(await phase(tx)).toEqual([{ phase: 'connector' }]) + }) + await migrate(runner) + expect(await phase(sql)).toEqual([{ phase: 'column' }]) + }) + }) + + it('commits the switch before a discovery failure and resumes with intervening column writes', async () => { + await database(async (sql, runner) => { + await sql`INSERT INTO project (id,name,owner_id) VALUES ('empty','Needs remediation','owner')` + await expect(migrate(runner)).rejects.toThrow('conflicts') + expect(await phase(sql)).toEqual([{ phase: 'column' }]) + expect( + await sql`SELECT name FROM script_migrations WHERE name = '0031_project_membership'` + ).toHaveLength(0) + await sql`INSERT INTO workspace (id,name,owner_id,project_id) VALUES ('new','Column writer','owner','empty')` + await migrate(runner) + await migrate(runner) + expect(await phase(sql)).toEqual([{ phase: 'column' }]) + expect(await sql`SELECT id,project_id FROM workspace ORDER BY id`).toEqual([ + { id: 'env', project_id: 'retained' }, + { id: 'new', project_id: 'empty' }, + ]) + expect( + await sql`SELECT name FROM script_migrations WHERE name = '0031_project_membership'` + ).toHaveLength(1) + }) + }) + + it.each(['missing-row', 'missing-table', 'invalid-phase', 'populated-column'] as const)( + 'fails closed for %s without copying membership or retiring the connector', + async (state) => { + await database(async (sql, runner) => { + if (state === 'missing-row') await sql`DELETE FROM project_membership_rollout` + if (state === 'missing-table') await sql`DROP TABLE project_membership_rollout` + if (state === 'invalid-phase') { + await sql`ALTER TABLE project_membership_rollout DROP CONSTRAINT project_membership_rollout_phase` + await sql`UPDATE project_membership_rollout SET phase = 'invalid'` + } + if (state === 'populated-column') await sql`UPDATE workspace SET project_id = 'retained'` + const before = await sql`SELECT id,project_id FROM workspace` + await expect(migrate(runner)).rejects.toThrow(/authority|rollout|populated/i) + expect(await sql`SELECT id,project_id FROM workspace`).toEqual(before) + expect(await sql`SELECT project_id,workspace_id FROM project_workspace`).toEqual([ + { project_id: 'retained', workspace_id: 'env' }, + ]) + expect( + await sql`SELECT name FROM script_migrations WHERE name = '0031_project_membership'` + ).toHaveLength(0) + }) + } + ) +}) diff --git a/packages/db/scripts/project-contract-required.ts b/packages/db/scripts/project-contract-required.ts new file mode 100644 index 00000000000..34f883680fe --- /dev/null +++ b/packages/db/scripts/project-contract-required.ts @@ -0,0 +1,73 @@ +import { readProjectMembershipPhase } from '@sim/db/maintenance/project-rollout' +import journal from '@sim/db/migrations/meta/_journal.json' +import { projectMembershipMigration } from '@sim/db/script-migrations/0031_project_membership' +import { createLogger } from '@sim/logger' +import { getErrorMessage } from '@sim/utils/errors' +import postgres from 'postgres' + +const logger = createLogger('ProjectContractPreflight') +const expansion = journal.entries.find((entry) => entry.tag === '0406_workspace_project_column') +const url = process.env.MIGRATION_DATABASE_URL || process.env.DATABASE_URL +if (!expansion || !url) + throw new Error( + 'Project column contract preflight requires migration metadata and a database URL' + ) +const sql = postgres(url, { + max: 1, + connect_timeout: 10, + connection: { statement_timeout: 10_000 }, +}) +try { + const [tables] = await sql<{ workspace: string | null; journal: string | null }[]>` + SELECT to_regclass('public.workspace')::text AS workspace, + to_regclass('drizzle.__drizzle_migrations')::text AS journal + ` + if (!tables) throw new Error('Could not inspect migration prerequisites') + let required = Boolean(tables.workspace) + if (required) { + if (!tables.journal) { + throw new Error('Deploy the workspace Project column expansion before enforcing membership') + } + const [applied] = await sql<{ expanded: boolean; columnExists: boolean }[]>` + SELECT EXISTS (SELECT 1 FROM drizzle.__drizzle_migrations WHERE created_at = ${expansion.when}) AS expanded, + EXISTS ( + SELECT 1 FROM information_schema.columns + WHERE table_schema = 'public' AND table_name = 'workspace' AND column_name = 'project_id' + ) AS "columnExists" + ` + if (!applied?.expanded || !applied.columnExists) { + throw new Error('Deploy the workspace Project column expansion before enforcing membership') + } + const phase = await readProjectMembershipPhase(sql) + if (phase === 'connector') { + const [state] = await sql`SELECT + to_regclass('public.project_workspace') IS NOT NULL AS connector, + EXISTS (SELECT 1 FROM public.workspace WHERE project_id IS NOT NULL) AS populated` + if (!state?.connector || state.populated) + throw new Error('Connector authority has inconsistent membership; reconcile before rollout') + } + const [scripts] = + await sql`SELECT to_regclass('public.script_migrations') IS NOT NULL AS present` + if (scripts.present) { + const [completed] = await sql`SELECT EXISTS ( + SELECT 1 FROM script_migrations WHERE name = ${projectMembershipMigration.name}) AS applied` + required = !completed.applied + } + if (!required) { + const [state] = await sql`SELECT + to_regclass('public.project_workspace') IS NULL AS retired, + EXISTS (SELECT 1 FROM pg_attribute WHERE attrelid = 'workspace'::regclass + AND attname = 'project_id' AND attnotnull AND NOT attisdropped) AS enforced` + if (phase !== 'column' || !state.retired || !state.enforced) + throw new Error('Project migration receipt disagrees with the physical schema') + } + } + process.stdout.write(`required=${required}\n`) +} catch (error) { + logger.error('Cannot establish Project column contract prerequisites; refusing migration', { + reason: getErrorMessage(error), + }) + process.exitCode = 1 +} finally { + await sql.end({ timeout: 5 }) +} diff --git a/packages/db/scripts/project-contract.integration.ts b/packages/db/scripts/project-contract.integration.ts new file mode 100644 index 00000000000..060b99d9eb8 --- /dev/null +++ b/packages/db/scripts/project-contract.integration.ts @@ -0,0 +1,1790 @@ +import { execFile } from 'node:child_process' +import { once } from 'node:events' +import { mkdir, mkdtemp, readdir, readFile, rm, writeFile } from 'node:fs/promises' +import { tmpdir } from 'node:os' +import { join } from 'node:path' +import { promisify } from 'node:util' +import { + assignProjectBackfillBatch, + discoverProjectBackfill, + type ProjectGroupingReview, + projectBackfillDatabaseId, + verifyProjectBackfill, +} from '@sim/db/maintenance/project-backfill' +import { enforceProjectMembership } from '@sim/db/maintenance/project-enforcement' +import journal from '@sim/db/migrations/meta/_journal.json' +import { runScriptMigrations, scriptMigrations } from '@sim/db/script-migrations' +import { readTestDatabaseUrl } from '@sim/db/testing/test-infrastructure' +import { createDeferred } from '@sim/testing/helpers/deferred' +import { getPostgresErrorCode } from '@sim/utils/errors' +import { sleep } from '@sim/utils/helpers' +import { generateId } from '@sim/utils/id' +import { drizzle } from 'drizzle-orm/postgres-js' +import { migrate } from 'drizzle-orm/postgres-js/migrator' +import postgres, { type Sql } from 'postgres' +import { describe, expect, it, vi } from 'vitest' + +const migration = await readFile( + new URL('../maintenance/project-membership.sql', import.meta.url), + 'utf8' +) + +const expansion = await readFile( + new URL('../migrations/0406_workspace_project_column.sql', import.meta.url), + 'utf8' +) + +async function database(run: (sql: Sql, url: string) => Promise) { + const admin = postgres(readTestDatabaseUrl(), { max: 1 }) + const name = `project_contract_test_${generateId().replaceAll('-', '')}` + const url = new URL(readTestDatabaseUrl()) + url.pathname = `/${name}` + await admin.unsafe(`CREATE DATABASE "${name}"`) + const sql = postgres(url.toString(), { max: 4, onnotice: () => undefined }) + try { + await sql.unsafe(` + CREATE TABLE "user" (id text PRIMARY KEY); + CREATE TABLE organization (id text PRIMARY KEY); + CREATE TABLE workspace (id text PRIMARY KEY, name text NOT NULL, owner_id text NOT NULL, + organization_id text, archived_at timestamp, forked_from_workspace_id text REFERENCES workspace(id) ON DELETE SET NULL); + CREATE INDEX workspace_parent_idx ON workspace(forked_from_workspace_id); + CREATE TABLE workflow (id text PRIMARY KEY, workspace_id text REFERENCES workspace(id) ON DELETE CASCADE, archived_at timestamp); + INSERT INTO "user" VALUES ('owner'); INSERT INTO organization VALUES ('org'); + `) + await sql.unsafe( + await readFile(new URL('../migrations/0394_project_foundation.sql', import.meta.url), 'utf8') + ) + await applyMigration(sql, expansion) + /** These reconciliation fixtures model the already committed column-authority phase. */ + await sql`UPDATE project_membership_rollout SET phase = 'column' WHERE id = 'membership'` + await run(sql, url.toString()) + } finally { + await sql.end({ timeout: 2 }) + await admin.unsafe(`DROP DATABASE "${name}" WITH (FORCE)`) + await admin.end() + } +} + +async function applyMigration(sql: Sql, source: string) { + const reserved = await sql.reserve() + try { + for (const statement of source.split('--> statement-breakpoint')) { + await reserved.unsafe(statement) + } + } catch (error) { + if (!['25P03', '25P04', 'CONNECTION_CLOSED'].includes(getPostgresErrorCode(error) ?? '')) { + await reserved.unsafe('ROLLBACK') + } + throw error + } finally { + await reserved + .unsafe("SELECT pg_advisory_unlock(hashtextextended('sim:project-backfill-operator',0))") + .catch(() => undefined) + reserved.release() + } +} + +async function prepare(sql: Sql) { + const manifest = await discoverProjectBackfill(sql, 'fixture') + for (const family of manifest.families) await assignProjectBackfillBatch(sql, [family]) + if (manifest.conflicts.length) + throw Object.assign(new Error(manifest.conflicts[0].reason), { + code: manifest.conflicts[0].reason.includes('oversized') ? '54000' : '55000', + }) +} + +async function prepareAndEnforce(sql: Sql) { + await prepare(sql) + await applyMigration(sql, migration) +} + +async function runRegisteredProjectMigration(url: string, reviews?: ProjectGroupingReview[]) { + const previousPath = process.env.PROJECT_BACKFILL_REVIEW_PATH + const previousUrl = process.env.MIGRATION_DATABASE_URL + const directory = reviews ? await mkdtemp(join(tmpdir(), 'project-review-')) : null + if (directory) { + process.env.PROJECT_BACKFILL_REVIEW_PATH = join(directory, 'manifest.json') + process.env.MIGRATION_DATABASE_URL = url + await writeFile( + process.env.PROJECT_BACKFILL_REVIEW_PATH, + JSON.stringify({ version: 1, databaseId: projectBackfillDatabaseId(url), groupings: reviews }) + ) + } + const runner = postgres(url, { max: 1, onnotice: () => undefined, max_lifetime: null }) + try { + await runScriptMigrations( + runner, + scriptMigrations.filter((item) => item.name === '0031_project_membership') + ) + } finally { + await runner.end({ timeout: 2 }) + vi.stubEnv('PROJECT_BACKFILL_REVIEW_PATH', previousPath) + vi.stubEnv('MIGRATION_DATABASE_URL', previousUrl) + if (directory) await rm(directory, { recursive: true, force: true }) + } +} + +async function seed(sql: Sql) { + await sql`INSERT INTO workspace (id, name, owner_id) VALUES ('root', 'Production', 'owner')` + await sql`INSERT INTO workspace (id, name, owner_id, forked_from_workspace_id) VALUES ('child', 'Staging', 'owner', 'root')` + await sql`INSERT INTO workflow VALUES ('flow', 'root', NULL)` + await prepareAndEnforce(sql) +} + +const constraintFailure = (error: unknown) => getPostgresErrorCode(error) === '23503' + +describe('Project expand/backfill/contract against PostgreSQL', () => { + it('repairs fully assigned stale Project archive and scope without restoring environments or workflows', async () => { + await database(async (sql, url) => { + await sql`INSERT INTO project (id,name,owner_id,archived_at) VALUES + ('stale','Keep identity','owner','2026-01-01'), ('archived','Keep archive','owner',NULL)` + await sql`INSERT INTO workspace (id,name,owner_id,organization_id,project_id,archived_at) VALUES + ('root','Root','owner','org','stale','2026-01-01'), + ('old','Old','owner',NULL,'archived','2026-01-02')` + await sql`INSERT INTO workspace (id,name,owner_id,organization_id,project_id,forked_from_workspace_id) + VALUES ('child','Child','owner','org','stale','root')` + await sql`INSERT INTO workflow VALUES ('old-flow','root','2026-01-01'), ('live-flow','child',NULL)` + const before = await sql`SELECT id,archived_at FROM workspace ORDER BY id` + const workflows = await sql`SELECT * FROM workflow ORDER BY id` + await runRegisteredProjectMigration(url) + expect( + await sql`SELECT id,name,organization_id,archived_at::text FROM project ORDER BY id` + ).toEqual([ + { + id: 'archived', + name: 'Keep archive', + organization_id: null, + archived_at: '2026-01-02 00:00:00', + }, + { id: 'stale', name: 'Keep identity', organization_id: 'org', archived_at: null }, + ]) + expect(await sql`SELECT id,archived_at FROM workspace ORDER BY id`).toEqual(before) + expect(await sql`SELECT * FROM workflow ORDER BY id`).toEqual(workflows) + expect(Object.values(await verifyProjectBackfill(sql)).every((count) => count === 0)).toBe( + true + ) + }) + }) + + it('reports fully assigned mixed-scope and personal-owner conflicts without changing ownership', async () => { + await database(async (sql) => { + await sql`INSERT INTO "user" VALUES ('other')` + await sql`INSERT INTO project (id,name,owner_id) VALUES ('shared','Shared','owner')` + await sql`INSERT INTO workspace (id,name,owner_id,project_id) VALUES ('root','Root','owner','shared')` + await sql`INSERT INTO workspace (id,name,owner_id,project_id,forked_from_workspace_id) + VALUES ('child','Child','other','shared','root')` + expect((await discoverProjectBackfill(sql, 'fixture')).conflicts).toEqual([ + { id: 'root', reason: expect.stringContaining('owners') }, + ]) + await sql`UPDATE workspace SET organization_id = 'org' WHERE id = 'child'` + expect((await discoverProjectBackfill(sql, 'fixture')).conflicts).toEqual([ + { id: 'root', reason: expect.stringContaining('organizations') }, + ]) + expect(await sql`SELECT owner_id FROM project`).toEqual([{ owner_id: 'owner' }]) + }) + }) + + it('reports unproven multi-root grouping and accepts an exact reviewed retention decision', async () => { + await database(async (sql, url) => { + await sql`INSERT INTO project (id,name,owner_id) VALUES ('shared','Keep context','owner')` + await sql`INSERT INTO workspace (id,name,owner_id,project_id) VALUES + ('root','Root','owner','shared'), ('detached','Detached','owner','shared')` + expect((await discoverProjectBackfill(sql, 'fixture')).conflicts).toContainEqual({ + id: 'shared', + reason: expect.stringContaining('grouping'), + }) + await expect(runRegisteredProjectMigration(url)).rejects.toThrow('conflicts') + expect(await sql`SELECT id,project_id FROM workspace ORDER BY id`).toEqual([ + { id: 'detached', project_id: 'shared' }, + { id: 'root', project_id: 'shared' }, + ]) + const reviews = (await discoverProjectBackfill(sql, 'fixture')).groupings.map((group) => ({ + ...group, + decision: 'retain' as const, + })) + await runRegisteredProjectMigration(url, reviews) + expect(await sql`SELECT id,name FROM project`).toEqual([ + { id: 'shared', name: 'Keep context' }, + ]) + }) + }) + + it.each(['root', 'unassigned-child'])( + 'rediscoveries Project-wide evidence after a %s changes during repair planning', + async (changed) => { + await database(async (sql) => { + await sql`INSERT INTO project (id,name,owner_id,archived_at) VALUES ('shared','Shared','owner','2026-01-01')` + await sql`INSERT INTO workspace (id,name,owner_id,project_id) VALUES ('a','A','owner','shared'),('b','B','owner','shared')` + await sql`INSERT INTO project_workspace (project_id,workspace_id) VALUES ('shared','a'),('shared','b')` + await sql`INSERT INTO workspace (id,name,owner_id,forked_from_workspace_id) VALUES ('c','Pending child','owner','b')` + const manifest = await discoverProjectBackfill(sql, 'fixture') + await sql`UPDATE workspace SET archived_at = '2026-01-02' WHERE id = ${changed === 'root' ? 'b' : 'c'}` + await expect(assignProjectBackfillBatch(sql, [manifest.families[0]])).rejects.toThrow( + 'rediscover' + ) + expect(await sql`SELECT archived_at IS NOT NULL AS archived FROM project`).toEqual([ + { archived: true }, + ]) + await prepareAndEnforce(sql) + expect(await sql`SELECT archived_at FROM project`).toEqual([{ archived_at: null }]) + }) + } + ) + + it.each([false, true])( + 'replays a reviewed partial family after commit while preserving legitimate shared roots (shared=%s)', + async (shared) => { + await database(async (sql) => { + await sql`INSERT INTO project(id,name,owner_id) VALUES ('existing','Keep context','owner')` + await sql`INSERT INTO workspace(id,name,owner_id) VALUES ('root','Root','owner')` + await sql`INSERT INTO workspace(id,name,owner_id,forked_from_workspace_id) VALUES ('child','Child','owner','root')` + await sql`INSERT INTO project_workspace(project_id,workspace_id) VALUES ('existing','root')` + if (shared) { + await sql`INSERT INTO workspace(id,name,owner_id,project_id) VALUES ('other-root','Other','owner','existing')` + await sql`INSERT INTO project_workspace(project_id,workspace_id) VALUES ('existing','other-root')` + await sql`INSERT INTO workspace(id,name,owner_id,forked_from_workspace_id) VALUES ('other-child','Other child','owner','other-root')` + } + const manifest = await discoverProjectBackfill(sql, 'fixture') + const family = manifest.families.find((item) => item.rootId === 'root') + if (!family) throw new Error('Missing partial family') + expect(await assignProjectBackfillBatch(sql, [family])).toMatchObject({ + assigned: 2, + projectsCreated: 0, + }) + expect(await assignProjectBackfillBatch(sql, [family])).toMatchObject({ + assigned: 0, + alreadyAssigned: 2, + projectsCreated: 0, + }) + if (shared) { + const other = manifest.families.find((item) => item.rootId === 'other-root') + if (!other) throw new Error('Missing other partial family') + expect(await assignProjectBackfillBatch(sql, [other])).toMatchObject({ + assigned: 1, + projectsCreated: 0, + }) + expect(await assignProjectBackfillBatch(sql, [other])).toMatchObject({ + assigned: 0, + alreadyAssigned: 2, + projectsCreated: 0, + }) + } + expect(await sql`SELECT id,name FROM project`).toEqual([ + { id: 'existing', name: 'Keep context' }, + ]) + expect( + await sql`SELECT id FROM workspace WHERE project_id IS DISTINCT FROM 'existing'` + ).toHaveLength(0) + }) + } + ) + + it('runs registered assignment before enforcement for forks, singletons and archived families', async () => { + await database(async (sql, url) => { + await sql`INSERT INTO workspace (id,name,owner_id,archived_at) VALUES + ('root','Root','owner',NULL), ('single','Single','owner',NULL), + ('archived','Archived','owner','2026-01-01')` + await sql`INSERT INTO workspace (id,name,owner_id,forked_from_workspace_id) + VALUES ('child','Child','owner','root')` + await runRegisteredProjectMigration(url) + expect(await sql`SELECT id FROM workspace WHERE project_id IS NULL`).toHaveLength(0) + expect(await sql`SELECT id FROM project`).toHaveLength(3) + expect( + await sql`SELECT DISTINCT project_id FROM workspace WHERE id IN ('root','child')` + ).toHaveLength(1) + expect(await sql`SELECT id FROM project WHERE archived_at IS NOT NULL`).toHaveLength(1) + expect(await sql`SELECT to_regclass('project_workspace')::text AS connector`).toEqual([ + { connector: null }, + ]) + expect(await sql`SELECT to_regclass('public.project_membership_rollout') AS marker`).toEqual([ + { marker: null }, + ]) + const assignments = await sql`SELECT id,project_id FROM workspace ORDER BY id` + await runRegisteredProjectMigration(url) + expect(await sql`SELECT id,project_id FROM workspace ORDER BY id`).toEqual(assignments) + await expect( + sql`INSERT INTO workspace (id,name,owner_id) VALUES ('bad','Bad','owner')` + ).rejects.toSatisfy((error: unknown) => getPostgresErrorCode(error) === '23502') + }) + }) + + it('preserves legacy Project identity through a mixed fork family and a detached column assignment', async () => { + await database(async (sql, url) => { + await sql`INSERT INTO project (id,name,owner_id) VALUES + ('original','Keep original context','owner'), ('detached','Keep detached context','owner')` + await sql`INSERT INTO workspace (id,name,owner_id,project_id,forked_from_workspace_id) VALUES + ('root','Root','owner',NULL,NULL), ('child','Child','owner','original','root'), + ('unassigned','Unassigned','owner',NULL,'child'), + ('detached','Detached','owner','detached',NULL)` + await sql`INSERT INTO project_workspace (project_id,workspace_id) VALUES + ('original','root'), ('original','detached')` + await sql`INSERT INTO workflow VALUES ('flow','root',NULL)` + await runRegisteredProjectMigration(url) + expect(await sql`SELECT id,name FROM project ORDER BY id`).toEqual([ + { id: 'detached', name: 'Keep detached context' }, + { id: 'original', name: 'Keep original context' }, + ]) + expect(await sql`SELECT id,project_id FROM workspace ORDER BY id`).toEqual([ + { id: 'child', project_id: 'original' }, + { id: 'detached', project_id: 'detached' }, + { id: 'root', project_id: 'original' }, + { id: 'unassigned', project_id: 'original' }, + ]) + expect(await sql`SELECT id,workspace_id FROM workflow`).toEqual([ + { id: 'flow', workspace_id: 'root' }, + ]) + expect(await sql`SELECT to_regclass('project_workspace')::text AS connector`).toEqual([ + { connector: null }, + ]) + expect(await sql`SELECT name FROM script_migrations`).toEqual([ + { name: '0031_project_membership' }, + ]) + }) + }) + + it('copies exact legacy memberships across independent roots without changing Project lifecycle or the connector', async () => { + await database(async (sql) => { + await sql`INSERT INTO project (id,name,owner_id) VALUES ('existing','Existing','owner')` + await sql`INSERT INTO workspace (id,name,owner_id,archived_at) VALUES + ('a-archived','Archived','owner','2026-01-01'), ('b-active','Active','owner',NULL)` + await sql`INSERT INTO project_workspace (project_id,workspace_id) VALUES + ('existing','a-archived'), ('existing','b-active')` + const before = + await sql`SELECT project_id,workspace_id FROM project_workspace ORDER BY workspace_id` + const manifest = await discoverProjectBackfill(sql, 'fixture') + for (const family of manifest.families) { + expect(await assignProjectBackfillBatch(sql, [family])).toMatchObject({ + assigned: 1, + projectsCreated: 0, + }) + } + expect(await sql`SELECT id,project_id FROM workspace ORDER BY id`).toEqual([ + { id: 'a-archived', project_id: 'existing' }, + { id: 'b-active', project_id: 'existing' }, + ]) + expect(await sql`SELECT id,archived_at FROM project`).toEqual([ + { id: 'existing', archived_at: null }, + ]) + expect( + await sql`SELECT project_id,workspace_id FROM project_workspace ORDER BY workspace_id` + ).toEqual(before) + await prepareAndEnforce(sql) + }) + }) + + it('rejects changed legacy assignment evidence before writing and accepts a fresh review', async () => { + await database(async (sql) => { + await sql`INSERT INTO project (id,name,owner_id) VALUES ('first','First','owner'),('second','Second','owner')` + await sql`INSERT INTO workspace (id,name,owner_id) VALUES ('root','Root','owner')` + await sql`INSERT INTO project_workspace (project_id,workspace_id) VALUES ('first','root')` + const manifest = await discoverProjectBackfill(sql, 'fixture') + await sql`UPDATE project_workspace SET project_id = 'second' WHERE workspace_id = 'root'` + await expect(assignProjectBackfillBatch(sql, manifest.families)).rejects.toThrow('changed') + expect(await sql`SELECT project_id FROM workspace`).toEqual([{ project_id: null }]) + expect(await sql`SELECT id FROM project ORDER BY id`).toEqual([ + { id: 'first' }, + { id: 'second' }, + ]) + const reviewed = await discoverProjectBackfill(sql, 'fixture') + expect(await assignProjectBackfillBatch(sql, reviewed.families)).toMatchObject({ + assigned: 1, + projectsCreated: 0, + }) + expect(await sql`SELECT project_id FROM workspace`).toEqual([{ project_id: 'second' }]) + }) + }) + + it('releases a batch promptly when its legacy assignment row is being changed', async () => { + await database(async (sql) => { + await sql`INSERT INTO project (id,name,owner_id) VALUES ('first','First','owner'),('second','Second','owner')` + await sql`INSERT INTO workspace (id,name,owner_id) VALUES ('root','Root','owner')` + await sql`INSERT INTO project_workspace (project_id,workspace_id) VALUES ('first','root')` + const manifest = await discoverProjectBackfill(sql, 'fixture') + await sql.begin(async (tx) => { + await tx`UPDATE project_workspace SET project_id = 'second' WHERE workspace_id = 'root'` + await expect(assignProjectBackfillBatch(sql, manifest.families)).rejects.toSatisfy( + (error: unknown) => getPostgresErrorCode(error) === '55P03' + ) + await tx`SET LOCAL lock_timeout = '100ms'` + await tx`UPDATE workspace SET name = 'Still editable' WHERE id = 'root'` + expect(await tx`SELECT project_id FROM workspace`).toEqual([{ project_id: null }]) + }) + await expect(assignProjectBackfillBatch(sql, manifest.families)).rejects.toThrow('changed') + const reviewed = await discoverProjectBackfill(sql, 'fixture') + expect(await assignProjectBackfillBatch(sql, reviewed.families)).toMatchObject({ + assigned: 1, + projectsCreated: 0, + }) + expect(await sql`SELECT project_id FROM workspace`).toEqual([{ project_id: 'second' }]) + }) + }) + + it('defers a legacy Project mutation without blocking unrelated assignment', async () => { + await database(async (sql) => { + await sql`INSERT INTO project (id,name,owner_id) VALUES ('legacy','Legacy','owner')` + await sql`INSERT INTO workspace (id,name,owner_id) VALUES ('a-legacy','Legacy','owner'),('b-free','Free','owner')` + await sql`INSERT INTO project_workspace (project_id,workspace_id) VALUES ('legacy','a-legacy')` + const manifest = await discoverProjectBackfill(sql, 'fixture') + await sql.begin(async (tx) => { + await tx`SELECT pg_advisory_xact_lock(hashtextextended('project:legacy',0))` + await expect(assignProjectBackfillBatch(sql, [manifest.families[0]])).rejects.toThrow( + 'busy' + ) + expect(await assignProjectBackfillBatch(sql, [manifest.families[1]])).toMatchObject({ + assigned: 1, + }) + expect(await sql`SELECT project_id FROM workspace WHERE id = 'a-legacy'`).toEqual([ + { project_id: null }, + ]) + }) + expect(await assignProjectBackfillBatch(sql, [manifest.families[0]])).toMatchObject({ + assigned: 1, + projectsCreated: 0, + }) + }) + }) + + it('retries the registered backfill after a failed batch without repeating committed assignments', async () => { + await database(async (sql, url) => { + await sql`INSERT INTO workspace (id,name,owner_id) SELECT lpad(n::text,3,'0'), 'Env ' || n,'owner' FROM generate_series(1,101) n` + await sql.unsafe(`CREATE FUNCTION reject_fixture() RETURNS trigger LANGUAGE plpgsql AS $$ BEGIN + IF NEW.name = 'Env 75 - Project' THEN RAISE EXCEPTION 'fixture failure'; END IF; RETURN NEW; END $$; + CREATE TRIGGER reject_fixture BEFORE INSERT ON project FOR EACH ROW EXECUTE FUNCTION reject_fixture();`) + await expect(runRegisteredProjectMigration(url)).rejects.toThrow('fixture failure') + const assignments = + await sql`SELECT id,project_id FROM workspace WHERE project_id IS NOT NULL ORDER BY id` + expect(assignments).toHaveLength(50) + expect(await sql`SELECT name FROM script_migrations`).toHaveLength(0) + expect(await sql`SELECT to_regclass('project_workspace')::text AS connector`).toEqual([ + { connector: 'project_workspace' }, + ]) + await sql`DROP TRIGGER reject_fixture ON project` + await runRegisteredProjectMigration(url) + expect( + await sql`SELECT id,project_id FROM workspace WHERE id = ANY(${assignments.map((row) => row.id)}) ORDER BY id` + ).toEqual(assignments) + expect(await sql`SELECT id FROM project`).toHaveLength(101) + expect(await sql`SELECT name FROM script_migrations`).toEqual([ + { name: '0031_project_membership' }, + ]) + }) + }) + + it('commits unrelated assignments while a family stays busy and resumes before enforcement', async () => { + await database(async (sql, url) => { + await sql`INSERT INTO workspace (id,name,owner_id) VALUES ('a-busy','Busy','owner'),('b-free','Free','owner')` + await sql.begin(async (tx) => { + await tx`SELECT pg_advisory_xact_lock_shared(hashtextextended('project-backfill:a-busy',0))` + await expect(runRegisteredProjectMigration(url)).rejects.toThrow('backfill incomplete') + expect(await sql`SELECT id FROM workspace WHERE project_id IS NOT NULL`).toEqual([ + { id: 'b-free' }, + ]) + expect(await sql`SELECT name FROM script_migrations`).toHaveLength(0) + expect(await sql`SELECT to_regclass('project_workspace')::text AS connector`).toEqual([ + { connector: 'project_workspace' }, + ]) + }) + await runRegisteredProjectMigration(url) + expect(await sql`SELECT id FROM project`).toHaveLength(2) + expect(await sql`SELECT id FROM workspace WHERE project_id IS NULL`).toHaveLength(0) + }) + }) + + it.each(['archive', 'ownership'] as const)( + 'stops the registered migration for unresolved %s before installing enforcement', + async (scenario) => { + await database(async (sql, url) => { + await sql`INSERT INTO workspace (id,name,owner_id) VALUES ('root','Root','owner')` + if (scenario === 'archive') { + await sql`UPDATE workspace SET archived_at = now()` + await sql`INSERT INTO workflow VALUES ('flow','root',NULL)` + } else { + await sql`INSERT INTO "user" VALUES ('other')` + await sql`INSERT INTO workspace (id,name,owner_id,forked_from_workspace_id) VALUES ('child','Child','other','root')` + } + await expect(runRegisteredProjectMigration(url)).rejects.toThrow( + 'Resolve Project conflicts' + ) + expect(await sql`SELECT id FROM project`).toHaveLength(0) + expect(await sql`SELECT name FROM script_migrations`).toHaveLength(0) + expect(await sql`SELECT to_regclass('project_workspace')::text AS connector`).toEqual([ + { connector: 'project_workspace' }, + ]) + if (scenario === 'archive') await sql`UPDATE workflow SET archived_at = now()` + else await sql`UPDATE workspace SET owner_id = 'owner' WHERE id = 'child'` + await runRegisteredProjectMigration(url) + expect(await sql`SELECT id FROM project`).toHaveLength(1) + }) + } + ) + + it('enforces public membership across a shadow search path and restores the caller path', async () => { + await database(async (sql, url) => { + await sql`INSERT INTO project (id,name,owner_id) VALUES ('retained','Retained','owner')` + await sql`CREATE SCHEMA shadow` + await sql`CREATE TABLE shadow.project (LIKE public.project INCLUDING ALL)` + await sql`CREATE TABLE shadow.workspace (LIKE public.workspace INCLUDING ALL)` + await sql`CREATE TABLE shadow.workflow (LIKE public.workflow INCLUDING ALL)` + await sql`CREATE TABLE shadow.project_workspace (project_id text, workspace_id text)` + await sql`INSERT INTO shadow.workspace (id,name,owner_id) VALUES ('shadow','Untouched','owner')` + const scoped = postgres(url, { + max: 1, + connection: { search_path: 'shadow,public' }, + onnotice: () => {}, + }) + try { + await expect(enforceProjectMembership(scoped)).rejects.toThrow('nonempty Projects') + expect(await scoped`SHOW search_path`).toEqual([{ search_path: 'shadow,public' }]) + await sql`INSERT INTO workspace (id,name,owner_id,project_id) VALUES ('env','Environment','owner','retained')` + await enforceProjectMembership(scoped) + expect(await scoped`SHOW search_path`).toEqual([{ search_path: 'shadow,public' }]) + expect(await sql`SELECT to_regclass('public.project_workspace') AS connector`).toEqual([ + { connector: null }, + ]) + expect( + await sql`SELECT attnotnull FROM pg_attribute WHERE attrelid = 'public.workspace'::regclass AND attname = 'project_id'` + ).toEqual([{ attnotnull: true }]) + expect( + await sql`SELECT count(*)::int AS count FROM pg_trigger WHERE tgrelid IN ('public.workspace'::regclass,'public.project'::regclass) AND NOT tgisinternal` + ).toEqual([{ count: 0 }]) + expect(await scoped`SELECT id,project_id FROM workspace`).toEqual([ + { id: 'shadow', project_id: null }, + ]) + expect( + await sql`SELECT to_regclass('shadow.project_workspace') IS NOT NULL AS retained` + ).toEqual([{ retained: true }]) + expect( + await sql`SELECT attnotnull FROM pg_attribute WHERE attrelid = 'shadow.workspace'::regclass AND attname = 'project_id'` + ).toEqual([{ attnotnull: false }]) + expect( + await sql`SELECT count(*)::int AS count FROM pg_trigger WHERE tgrelid IN ('shadow.workspace'::regclass,'shadow.project'::regclass) AND NOT tgisinternal` + ).toEqual([{ count: 0 }]) + await scoped`UPDATE public.workspace SET name = 'Updated' WHERE id = 'env'` + await expect( + scoped`UPDATE public.workspace SET organization_id = 'org' WHERE id = 'env'` + ).rejects.toMatchObject({ code: '23503' }) + expect(await sql`SELECT name,archived_at FROM public.workspace`).toEqual([ + { name: 'Updated', archived_at: null }, + ]) + } finally { + await scoped.end({ timeout: 2 }) + } + }) + }) + + it('requires completed assignments before final enforcement and leaves missing assignments untouched', async () => { + await database(async (sql) => { + await sql`INSERT INTO workspace (id,name,owner_id) VALUES ('legacy','Legacy','owner')` + await expect(applyMigration(sql, migration)).rejects.toThrow('preparation is incomplete') + expect(await sql`SELECT id FROM project`).toHaveLength(0) + expect(await sql`SELECT id FROM workspace WHERE project_id IS NULL`).toHaveLength(1) + await prepareAndEnforce(sql) + expect(Object.values(await verifyProjectBackfill(sql)).every((count) => count === 0)).toBe( + true + ) + }) + }) + + it('rolls back an entire singleton batch on failure without losing earlier commits', async () => { + await database(async (sql) => { + await sql`INSERT INTO workspace (id,name,owner_id) SELECT lpad(n::text,3,'0'), 'Env ' || n,'owner' FROM generate_series(1,101) n` + const manifest = await discoverProjectBackfill(sql, 'fixture') + expect(await assignProjectBackfillBatch(sql, manifest.families.slice(0, 50))).toMatchObject({ + assigned: 50, + projectsCreated: 50, + }) + await sql.unsafe(`CREATE FUNCTION reject_fixture() RETURNS trigger LANGUAGE plpgsql AS $$ BEGIN + IF NEW.name = 'Env 75 - Project' THEN RAISE EXCEPTION 'fixture failure'; END IF; RETURN NEW; END $$; + CREATE TRIGGER reject_fixture BEFORE INSERT ON project FOR EACH ROW EXECUTE FUNCTION reject_fixture();`) + await expect( + assignProjectBackfillBatch(sql, manifest.families.slice(50, 100)) + ).rejects.toThrow('fixture failure') + expect(await sql`SELECT id FROM project`).toHaveLength(50) + expect(await sql`SELECT id FROM workspace WHERE project_id IS NULL`).toHaveLength(51) + await sql`DROP TRIGGER reject_fixture ON project` + expect(await assignProjectBackfillBatch(sql, manifest.families.slice(50, 100))).toMatchObject( + { assigned: 50 } + ) + expect(await assignProjectBackfillBatch(sql, manifest.families.slice(0, 50))).toMatchObject({ + assigned: 0, + alreadyAssigned: 50, + projectsCreated: 0, + }) + expect(await sql`SELECT id FROM project`).toHaveLength(100) + }) + }) + + it('rejects a stale owner or archive plan without writing and reports archive repairs during discovery', async () => { + await database(async (sql) => { + await sql`INSERT INTO workspace (id,name,owner_id) VALUES ('root','Root','owner')` + const manifest = await discoverProjectBackfill(sql, 'fixture') + await sql`INSERT INTO "user" VALUES ('next-owner')` + await sql`UPDATE workspace SET owner_id = 'next-owner'` + await expect(assignProjectBackfillBatch(sql, manifest.families)).rejects.toThrow('changed') + expect(await sql`SELECT id FROM project`).toHaveLength(0) + await sql`UPDATE workspace SET owner_id = 'owner', archived_at = now()` + await sql`INSERT INTO workflow VALUES ('flow','root',NULL)` + await expect(assignProjectBackfillBatch(sql, manifest.families)).rejects.toThrow('changed') + const next = await discoverProjectBackfill(sql, 'fixture') + expect(next.repairs).toMatchObject([{ workspaceId: 'root', workflowIds: ['flow'] }]) + await expect(assignProjectBackfillBatch(sql, next.families)).rejects.toThrow('Archive repair') + expect(await sql`SELECT id FROM project`).toHaveLength(0) + }) + }) + + it('keeps operator discovery, assignment and verification on public under a shadow URL search path', async () => { + await database(async (sql, url) => { + const directory = await mkdtemp(join(tmpdir(), 'project-operator-shadow-')) + const manifest = join(directory, 'manifest.json') + const report = join(directory, 'report.json') + const scopedUrl = new URL(url) + scopedUrl.searchParams.set('search_path', 'shadow,public') + const run = (command: string) => + promisify(execFile)( + 'bun', + [ + '--no-env-file', + 'scripts/backfill-projects.ts', + command, + '--manifest', + manifest, + ...(command === 'plan' + ? [] + : ['--report', report, '--ack-release-drained', '--pause-ms', '1']), + ], + { + cwd: new URL('../../../apps/sim/', import.meta.url), + env: { ...process.env, MIGRATION_DATABASE_URL: scopedUrl.toString() }, + timeout: 30000, + } + ) + try { + await sql`INSERT INTO workspace (id,name,owner_id) VALUES ('public-env','Public','owner')` + await sql`CREATE SCHEMA shadow` + for (const name of [ + 'workspace', + 'project', + 'workflow', + 'user', + 'organization', + 'project_workspace', + ]) { + await sql`CREATE TABLE ${sql(`shadow.${name}`)} (LIKE ${sql(`public.${name}`)} INCLUDING ALL)` + } + await sql`INSERT INTO shadow.workspace (id,name,owner_id) VALUES ('shadow-env','Untouched','owner')` + await run('plan') + const plan = JSON.parse(await readFile(manifest, 'utf8')) + expect(plan.families.map((family: { rootId: string }) => family.rootId)).toEqual([ + 'public-env', + ]) + await run('apply') + expect(await sql`SELECT id,project_id FROM public.workspace`).toEqual([ + { id: 'public-env', project_id: expect.any(String) }, + ]) + expect(await sql`SELECT count(*)::int AS count FROM public.project`).toEqual([{ count: 1 }]) + await run('verify') + expect(await sql`SELECT id,project_id FROM shadow.workspace`).toEqual([ + { id: 'shadow-env', project_id: null }, + ]) + expect(await sql`SELECT count(*)::int AS count FROM shadow.project`).toEqual([{ count: 0 }]) + } finally { + await rm(directory, { recursive: true, force: true }) + } + }) + }) + + it('runs the real operator CLI read-only, resumes bounded batches and survives a lost checkpoint', async () => { + await database(async (sql, url) => { + const directory = await mkdtemp(join(tmpdir(), 'project-operator-test-')) + const manifestPath = join(directory, 'manifest.json') + const reportPath = join(directory, 'report.json') + const run = (command: string, extra: string[] = []) => + promisify(execFile)( + 'bun', + [ + '--no-env-file', + 'scripts/backfill-projects.ts', + command, + '--manifest', + manifestPath, + ...(command === 'plan' ? [] : ['--report', reportPath]), + ...extra, + ], + { + cwd: new URL('../../../apps/sim/', import.meta.url), + env: { ...process.env, MIGRATION_DATABASE_URL: url, DATABASE_URL: url }, + timeout: 30000, + } + ) + try { + await sql`INSERT INTO workspace (id,name,owner_id) SELECT lpad(n::text,3,'0'),'Env ' || n,'owner' FROM generate_series(1,101) n` + await run('plan') + const reviewed = await readFile(manifestPath, 'utf8') + await expect(run('plan')).rejects.toMatchObject({ code: 1 }) + expect(await readFile(manifestPath, 'utf8')).toBe(reviewed) + expect(await sql`SELECT id FROM project`).toHaveLength(0) + await expect(run('apply')).rejects.toMatchObject({ code: 1 }) + await expect( + run('apply', ['--ack-release-drained', '--max-batches', '1', '--pause-ms', '1']) + ).rejects.toMatchObject({ code: 2 }) + const report = JSON.parse(await readFile(reportPath, 'utf8')) + expect(report).toMatchObject({ + nextIndex: 50, + assigned: 50, + projectsCreated: 50, + status: 'paused', + }) + const mismatched = JSON.stringify({ ...report, codeHash: 'another-checkout' }) + await writeFile(reportPath, mismatched) + await expect(run('apply', ['--ack-release-drained'])).rejects.toMatchObject({ + code: 1, + stderr: expect.stringContaining('Report belongs to different backfill code'), + }) + expect(await readFile(reportPath, 'utf8')).toBe(mismatched) + expect(await sql`SELECT id FROM project`).toHaveLength(50) + expect(await sql`SELECT id FROM workspace WHERE project_id IS NOT NULL`).toHaveLength(50) + await writeFile(reportPath, JSON.stringify(report)) + for (const [status, exitCode] of [ + ['running', 2], + ['paused', 2], + ['incomplete', 2], + ['failed', 1], + ['complete', 0], + ] as const) { + const checkpoint = JSON.stringify({ ...report, status }) + await writeFile(reportPath, checkpoint) + const statusRun = run('status') + if (exitCode === 0) await statusRun + else await expect(statusRun).rejects.toMatchObject({ code: exitCode }) + expect(await readFile(reportPath, 'utf8')).toBe(checkpoint) + } + const first = + await sql`SELECT id,project_id FROM workspace WHERE project_id IS NOT NULL ORDER BY id` + await rm(reportPath) + const child = execFile( + 'bun', + [ + '--no-env-file', + 'scripts/backfill-projects.ts', + 'apply', + '--manifest', + manifestPath, + '--report', + reportPath, + '--ack-release-drained', + '--pause-ms', + '1000', + ], + { + cwd: new URL('../../../apps/sim/', import.meta.url), + env: { ...process.env, MIGRATION_DATABASE_URL: url, DATABASE_URL: url }, + timeout: 15000, + } + ) + const exited = once(child, 'close') + try { + let checkpointed = false + for (let attempt = 0; attempt < 200; attempt++) { + const text = await readFile(reportPath, 'utf8').catch(() => '{}') + if (JSON.parse(text).nextIndex === 50) { + checkpointed = true + break + } + await sleep(25) + } + expect(checkpointed).toBe(true) + await expect(run('apply', ['--ack-release-drained'])).rejects.toMatchObject({ code: 1 }) + child.kill('SIGTERM') + expect((await exited)[0]).toBe(2) + expect(JSON.parse(await readFile(reportPath, 'utf8'))).toMatchObject({ + status: 'paused', + nextIndex: 50, + assigned: 0, + alreadyAssigned: 50, + }) + expect(await sql`SELECT id FROM project`).toHaveLength(50) + } finally { + if (child.exitCode === null) child.kill('SIGKILL') + await exited + } + await rm(reportPath) + await run('apply', ['--ack-release-drained', '--max-batches', '4', '--pause-ms', '1']) + expect( + await sql`SELECT id,project_id FROM workspace WHERE id = ANY(${first.map((row) => row.id)}) ORDER BY id` + ).toEqual(first) + expect(await sql`SELECT id FROM project`).toHaveLength(101) + await run('verify') + for (const artifacts of [[], ['--manifest', manifestPath], ['--report', reportPath]]) { + await expect( + promisify(execFile)( + 'bun', + ['--no-env-file', 'scripts/backfill-projects.ts', 'verify', ...artifacts], + { + cwd: new URL('../../../apps/sim/', import.meta.url), + env: { ...process.env, MIGRATION_DATABASE_URL: url, DATABASE_URL: url }, + timeout: 30000, + } + ) + ).rejects.toMatchObject({ code: 1 }) + } + const manifest = JSON.parse(await readFile(manifestPath, 'utf8')) + manifest.databaseId = projectBackfillDatabaseId('postgres://localhost/wrong_test') + await writeFile(manifestPath, JSON.stringify(manifest)) + await expect(run('apply', ['--ack-release-drained'])).rejects.toMatchObject({ code: 1 }) + await applyMigration(sql, migration) + } finally { + await rm(directory, { recursive: true, force: true }) + } + }) + }, 60000) + + it('refuses a conflicted manifest even after its database invariants have been repaired', async () => { + await database(async (sql, url) => { + const directory = await mkdtemp(join(tmpdir(), 'project-conflict-verify-test-')) + const run = (command: string, plan: string) => + promisify(execFile)( + 'bun', + [ + '--no-env-file', + 'scripts/backfill-projects.ts', + command, + '--manifest', + join(directory, `${plan}-manifest.json`), + '--report', + join(directory, `${plan}-report.json`), + '--ack-release-drained', + ], + { + cwd: new URL('../../../apps/sim/', import.meta.url), + env: { ...process.env, MIGRATION_DATABASE_URL: url }, + timeout: 15000, + } + ) + try { + await sql`INSERT INTO "user" VALUES ('other-owner')` + await sql`INSERT INTO workspace (id,name,owner_id) VALUES ('root','Root','owner')` + await sql`INSERT INTO workspace (id,name,owner_id,forked_from_workspace_id) + VALUES ('child','Child','other-owner','root')` + await expect(run('plan', 'conflicted')).rejects.toMatchObject({ code: 2 }) + await expect(run('apply', 'conflicted')).rejects.toMatchObject({ code: 2 }) + await sql`UPDATE workspace SET owner_id = 'owner' WHERE id = 'child'` + await run('plan', 'repaired') + await run('apply', 'repaired') + expect(Object.values(await verifyProjectBackfill(sql)).every((count) => count === 0)).toBe( + true + ) + await run('verify', 'repaired') + await expect(run('verify', 'conflicted')).rejects.toMatchObject({ code: 2 }) + } finally { + await rm(directory, { recursive: true, force: true }) + } + }) + }, 30000) + + it.each([ + ['plan', 'SIGINT'], + ['verify', 'SIGTERM'], + ] as const)( + 'coordinates a stalled %s scan with enforcement and terminates on %s', + async (command, signal) => { + await database(async (sql, url) => { + const directory = await mkdtemp(join(tmpdir(), 'project-read-cancel-test-')) + const artifacts = [ + '--manifest', + join(directory, 'manifest.json'), + '--report', + join(directory, 'report.json'), + ] + const options = { + cwd: new URL('../../../apps/sim/', import.meta.url), + env: { ...process.env, MIGRATION_DATABASE_URL: url }, + timeout: 15000, + } + await sql`INSERT INTO workspace (id,name,owner_id) VALUES ('root','Root','owner')` + if (command === 'verify') { + for (const step of ['plan', 'apply']) + await promisify(execFile)( + 'bun', + [ + '--no-env-file', + 'scripts/backfill-projects.ts', + step, + ...artifacts, + '--ack-release-drained', + ], + options + ) + } + const files = await readdir(directory) + const contents = await Promise.all( + files.map((file) => readFile(join(directory, file), 'utf8')) + ) + await sql.unsafe(` + ALTER TABLE workspace RENAME TO workspace_fixture_source; + CREATE FUNCTION pause_fixture_scan() RETURNS boolean LANGUAGE plpgsql AS $$ + BEGIN PERFORM pg_sleep(5); RETURN true; END $$; + CREATE VIEW workspace AS SELECT * FROM workspace_fixture_source WHERE pause_fixture_scan(); + `) + const child = execFile( + 'bun', + ['--no-env-file', 'scripts/backfill-projects.ts', command, ...artifacts], + options + ) + const exited = once(child, 'close') + try { + await expect + .poll( + async () => { + const [state] = await sql`SELECT count(*)::int AS waiting FROM pg_stat_activity + WHERE datname = current_database() AND application_name = 'sim-project-backfill' + AND wait_event = 'PgSleep'` + return state.waiting + }, + { timeout: 5000, interval: 20 } + ) + .toBe(1) + const competing = await sql.reserve() + try { + const [attempt] = + await competing`SELECT pg_try_advisory_lock(hashtextextended('sim:project-backfill-operator',0)) AS acquired` + try { + expect(attempt.acquired).toBe(false) + } finally { + if (attempt.acquired) + await competing`SELECT pg_advisory_unlock(hashtextextended('sim:project-backfill-operator',0))` + } + } finally { + competing.release() + } + child.kill(signal) + expect((await exited)[1]).toBe(signal) + expect(await readdir(directory)).toEqual(files) + expect( + await Promise.all(files.map((file) => readFile(join(directory, file), 'utf8'))) + ).toEqual(contents) + } finally { + if (child.exitCode === null && child.signalCode === null) child.kill('SIGKILL') + await exited + await rm(directory, { recursive: true, force: true }) + } + }) + }, + 20000 + ) + + it('refuses a generated plan exceeding the artifact size limit before publishing it', async () => { + await database(async (sql, url) => { + const directory = await mkdtemp(join(tmpdir(), 'project-plan-size-test-')) + try { + await sql`INSERT INTO "user" VALUES (repeat('o',1024))` + await sql`INSERT INTO workspace (id,name,owner_id) + SELECT rpad('w-' || lpad(n::text,5,'0'),1024,'x'),'Large identifier fixture',repeat('o',1024) + FROM generate_series(1,45000) n` + await expect( + promisify(execFile)( + 'bun', + [ + '--no-env-file', + 'scripts/backfill-projects.ts', + 'plan', + '--manifest', + join(directory, 'manifest.json'), + ], + { + cwd: new URL('../../../apps/sim/', import.meta.url), + env: { ...process.env, MIGRATION_DATABASE_URL: url }, + timeout: 60000, + } + ) + ).rejects.toMatchObject({ code: 1 }) + expect(await readdir(directory)).toEqual([]) + expect(await sql`SELECT id FROM project`).toHaveLength(0) + } finally { + await rm(directory, { recursive: true, force: true }) + } + }) + }, 90000) + + it('gives workflow creation share locks precedence and bounds a stalled batch without retaining locks', async () => { + await database(async (sql) => { + await sql`INSERT INTO workspace (id,name,owner_id) VALUES ('root','Root','owner')` + const manifest = await discoverProjectBackfill(sql, 'fixture') + const held = createDeferred() + const release = createDeferred() + const writer = sql.begin(async (tx) => { + await tx`SELECT id FROM workspace WHERE id = 'root' FOR SHARE` + held.resolve() + await release.promise + await tx`INSERT INTO workflow VALUES ('flow','root',NULL)` + }) + await held.promise + try { + await expect(assignProjectBackfillBatch(sql, manifest.families)).rejects.toSatisfy( + (error: unknown) => getPostgresErrorCode(error) === '55P03' + ) + expect(await sql`SELECT id FROM project`).toHaveLength(0) + } finally { + release.resolve() + await writer + } + await sql.unsafe(`CREATE FUNCTION delay_fixture() RETURNS trigger LANGUAGE plpgsql AS $$ BEGIN PERFORM pg_sleep(8); RETURN NEW; END $$; + CREATE TRIGGER delay_fixture BEFORE INSERT ON project FOR EACH ROW EXECUTE FUNCTION delay_fixture();`) + await expect(assignProjectBackfillBatch(sql, manifest.families)).rejects.toSatisfy( + (error: unknown) => getPostgresErrorCode(error) === '57014' + ) + expect(await sql`SELECT id FROM project`).toHaveLength(0) + await sql.begin(async (tx) => { + await tx`SET LOCAL lock_timeout = '100ms'` + await tx`SELECT pg_advisory_xact_lock_shared(hashtextextended('project-backfill:root',0))` + await tx`UPDATE workspace SET name = 'Recovered' WHERE id = 'root'` + }) + await sql`DROP TRIGGER delay_fixture ON project` + await assignProjectBackfillBatch(sql, manifest.families) + expect(await sql`SELECT name FROM project`).toEqual([{ name: 'Recovered - Project' }]) + }) + }, 10000) + + it('excludes SQL contraction and database-backed commands while preserving offline status', async () => { + await database(async (sql, url) => { + const directory = await mkdtemp(join(tmpdir(), 'project-command-lock-test-')) + const manifest = join(directory, 'manifest.json') + const report = join(directory, 'report.json') + const concurrentPlan = join(directory, 'concurrent-plan.json') + const run = (command: string, plan = manifest) => + promisify(execFile)( + 'bun', + [ + '--no-env-file', + 'scripts/backfill-projects.ts', + command, + '--manifest', + plan, + '--report', + report, + '--ack-release-drained', + ], + { + cwd: new URL('../../../apps/sim/', import.meta.url), + env: { ...process.env, MIGRATION_DATABASE_URL: url }, + timeout: 15000, + } + ) + try { + await run('plan') + await run('apply') + const connection = await sql.reserve() + try { + await connection`SELECT pg_advisory_lock(hashtextextended('sim:project-backfill-operator',0))` + await expect(applyMigration(sql, migration)).rejects.toThrow( + 'preparation is still running' + ) + const attempts = await Promise.allSettled([run('plan', concurrentPlan), run('verify')]) + expect(attempts).toMatchObject([ + { status: 'rejected', reason: { code: 1 } }, + { status: 'rejected', reason: { code: 1 } }, + ]) + await run('status') + expect(await readdir(directory)).not.toContain('concurrent-plan.json') + expect(await sql`SELECT to_regclass('project_workspace')::text AS name`).toEqual([ + { name: 'project_workspace' }, + ]) + } finally { + await connection`SELECT pg_advisory_unlock(hashtextextended('sim:project-backfill-operator',0))` + connection.release() + } + await run('plan', concurrentPlan) + await run('verify') + await applyMigration(sql, migration) + } finally { + await rm(directory, { recursive: true, force: true }) + } + }) + }) + + it('leaves lifecycle enforcement to the application without touching Project rows', async () => { + await database(async (sql) => { + await seed(sql) + const before = await sql`SELECT id, xmin::text, updated_at FROM project` + await sql`UPDATE workspace SET archived_at = now()` + expect(await sql`SELECT id, xmin::text, updated_at FROM project`).toEqual(before) + await sql`UPDATE project SET archived_at = now()` + await sql`UPDATE workspace SET archived_at = NULL` + await sql`INSERT INTO project (id,name,owner_id) VALUES ('empty','Empty','owner')` + await sql`DELETE FROM workspace` + expect(await sql`SELECT id FROM project`).toHaveLength(2) + }) + }) + + it('revalidates structural mutations after constraints switch to immediate mode', async () => { + await database(async (sql) => { + await seed(sql) + await expect( + sql.begin(async (tx) => { + await tx`SET CONSTRAINTS ALL IMMEDIATE` + await tx`UPDATE workspace SET organization_id = 'org' WHERE id = 'child'` + }) + ).rejects.toSatisfy(constraintFailure) + }) + }) + + it('requires the exact expansion journal and physical column before rollout evidence can pass', async () => { + await database(async (sql, url) => { + const run = () => + promisify(execFile)('bun', ['--no-env-file', 'scripts/project-contract-required.ts'], { + cwd: new URL('..', import.meta.url), + env: { ...process.env, DATABASE_URL: url, MIGRATION_DATABASE_URL: url }, + }) + await expect(run()).rejects.toMatchObject({ code: 1 }) + await sql`CREATE SCHEMA drizzle` + await sql`CREATE TABLE drizzle.__drizzle_migrations (created_at bigint)` + const expanded = journal.entries.find((item) => item.tag === '0406_workspace_project_column') + const entry = journal.entries.find( + (item) => item.tag === '0407_project_membership_enforcement' + ) + if (!expanded || !entry) throw new Error('Missing Project migration metadata') + await sql`INSERT INTO drizzle.__drizzle_migrations VALUES (${expanded.when - 1})` + await expect(run()).rejects.toMatchObject({ code: 1 }) + await sql`INSERT INTO drizzle.__drizzle_migrations VALUES (${expanded.when})` + await sql`INSERT INTO workspace (id,name,owner_id) VALUES ('legacy','Legacy','owner')` + expect((await run()).stdout.trim()).toBe('required=true') + await sql`DELETE FROM project_membership_rollout` + await expect(run()).rejects.toMatchObject({ code: 1 }) + await sql`INSERT INTO project_membership_rollout (id, phase) VALUES ('membership', 'connector')` + expect((await run()).stdout.trim()).toBe('required=true') + await sql`INSERT INTO project (id,name,owner_id) VALUES ('gate','Gate','owner')` + await sql`UPDATE workspace SET project_id = 'gate' WHERE id = 'legacy'` + await expect(run()).rejects.toMatchObject({ code: 1 }) + await sql`UPDATE workspace SET project_id = NULL WHERE id = 'legacy'` + await sql`DELETE FROM project WHERE id = 'gate'` + await sql`ALTER TABLE workspace DROP COLUMN project_id CASCADE` + await expect(run()).rejects.toMatchObject({ code: 1 }) + await applyMigration(sql, expansion) + expect((await run()).stdout.trim()).toBe('required=true') + await sql`INSERT INTO drizzle.__drizzle_migrations VALUES (${entry.when})` + expect((await run()).stdout.trim()).toBe('required=true') + await runRegisteredProjectMigration(url) + expect((await run()).stdout.trim()).toBe('required=false') + await sql`DELETE FROM drizzle.__drizzle_migrations WHERE created_at = ${expanded.when}` + await expect(run()).rejects.toMatchObject({ code: 1 }) + await sql`DROP TABLE workspace CASCADE` + await sql`DROP SCHEMA drizzle CASCADE` + expect((await run()).stdout.trim()).toBe('required=false') + }) + }) + + it('replays committed contraction when recording its script receipt fails', async () => { + const directory = await mkdtemp(join(tmpdir(), 'project-contract-runner-')) + try { + await mkdir(join(directory, 'meta')) + await writeFile(join(directory, '0406_workspace_project_column.sql'), expansion) + await writeFile( + join(directory, '0407_project_membership_enforcement.sql'), + await readFile( + new URL('../migrations/0407_project_membership_enforcement.sql', import.meta.url), + 'utf8' + ) + ) + await writeFile( + join(directory, 'meta/_journal.json'), + JSON.stringify({ + ...journal, + entries: journal.entries.filter( + (entry) => + entry.tag === '0406_workspace_project_column' || + entry.tag === '0407_project_membership_enforcement' + ), + }) + ) + await database(async (sql, url) => { + const runner = postgres(url, { max: 1, onnotice: () => undefined, max_lifetime: null }) + const run = async () => { + await migrate(drizzle(runner), { migrationsFolder: directory }) + await runScriptMigrations( + runner, + scriptMigrations.filter((item) => item.name === '0031_project_membership') + ) + } + try { + await sql`INSERT INTO project (id,name,owner_id) VALUES ('empty','Empty','owner')` + await expect(run()).rejects.toThrow('conflicts') + expect(await sql`SELECT * FROM drizzle.__drizzle_migrations`).toHaveLength(2) + expect(await sql`SELECT * FROM script_migrations`).toHaveLength(0) + await sql`DELETE FROM project WHERE id = 'empty'` + await sql`INSERT INTO workspace (id,name,owner_id) VALUES ('legacy','Legacy','owner')` + await sql.unsafe(`CREATE FUNCTION reject_receipt() RETURNS trigger LANGUAGE plpgsql AS $$ BEGIN + RAISE EXCEPTION 'receipt failure'; END $$; + CREATE TRIGGER reject_receipt BEFORE INSERT ON script_migrations FOR EACH ROW EXECUTE FUNCTION reject_receipt();`) + await expect(run()).rejects.toThrow('receipt failure') + expect(await sql`SELECT to_regclass('project_workspace')::text AS connector`).toEqual([ + { connector: null }, + ]) + expect( + await sql`SELECT to_regclass('public.project_membership_rollout') AS marker` + ).toEqual([{ marker: null }]) + expect(await sql`SELECT * FROM script_migrations`).toHaveLength(0) + await expect( + sql`INSERT INTO workspace (id,name,owner_id) VALUES ('bad','Bad','owner')` + ).rejects.toSatisfy((error: unknown) => getPostgresErrorCode(error) === '23502') + await expect( + sql`INSERT INTO workspace (id,project_id,name,owner_id) VALUES ('missing','missing','Missing','owner')` + ).rejects.toSatisfy((error: unknown) => getPostgresErrorCode(error) === '23503') + const assignments = await sql`SELECT id,project_id FROM workspace ORDER BY id` + await sql`DROP TRIGGER reject_receipt ON script_migrations` + await run() + await run() + expect(await sql`SELECT * FROM drizzle.__drizzle_migrations`).toHaveLength(2) + expect(await sql`SELECT name FROM script_migrations`).toEqual([ + { name: '0031_project_membership' }, + ]) + expect(await sql`SELECT id,project_id FROM workspace ORDER BY id`).toEqual(assignments) + await sql`DELETE FROM workspace` + } finally { + await runner.end() + } + }) + } finally { + await rm(directory, { recursive: true, force: true }) + } + }) + + it.each(['workspace', 'project', 'project_workspace'] as const)( + 'fails contract DDL promptly on busy %s and releases partially acquired locks', + async (table) => { + await database(async (sql) => { + const held = createDeferred() + const release = createDeferred() + const reader = sql.begin(async (tx) => { + await tx`SELECT * FROM ${tx(table)}` + held.resolve() + await release.promise + }) + await held.promise + try { + const started = performance.now() + await expect(prepareAndEnforce(sql)).rejects.toSatisfy( + (error: unknown) => getPostgresErrorCode(error) === '55P03' + ) + expect(performance.now() - started).toBeLessThan(2000) + await sql.begin(async (tx) => { + await tx`SET LOCAL statement_timeout = '500ms'` + await tx`INSERT INTO project (id, name, owner_id) VALUES ('live-project', 'Live project', 'owner')` + await tx`INSERT INTO workspace (id, project_id, name, owner_id) VALUES ('live', 'live-project', 'Live', 'owner')` + await tx`SELECT * FROM project` + }) + expect(await sql`SELECT project_id FROM workspace WHERE id = 'live'`).toEqual([ + { project_id: 'live-project' }, + ]) + expect( + await sql`SELECT project_id FROM project_workspace WHERE workspace_id = 'live'` + ).toHaveLength(0) + } finally { + release.resolve() + await reader + } + await prepareAndEnforce(sql) + expect( + await sql`SELECT id FROM workspace WHERE id = 'live' AND project_id = 'live-project'` + ).toHaveLength(1) + }) + } + ) + + it('releases installation table locks when the migration client stalls between statements', async () => { + await database(async (sql, url) => { + const result = await promisify(execFile)( + 'bun', + [ + '--no-env-file', + '-e', + ` + import { readFile } from 'node:fs/promises'; + import { sleep } from '@sim/utils/helpers'; + import postgres from 'postgres'; + const sql = postgres(process.env.TEST_DATABASE_URL, { max: 1, onnotice: () => undefined }); + const observer = postgres(process.env.TEST_DATABASE_URL, { max: 1 }); + const [{ pid }] = await sql.unsafe('SELECT pg_backend_pid() AS pid'); + const locks = async () => (await observer.unsafe( + "SELECT count(*)::int AS count FROM pg_locks WHERE pid = $1 AND mode = 'AccessExclusiveLock' AND granted", + [pid] + ))[0].count; + const migration = await readFile('maintenance/project-membership.sql', 'utf8'); + for (const statement of migration.split('--> statement-breakpoint')) { + await sql.unsafe(statement); + if (statement.includes('LOCK TABLE workspace,')) { + const before = await locks(); + await sleep(6500); + const result = JSON.stringify({ before, after: await locks() }); + await new Promise((resolve, reject) => process.stdout.write(result, (error) => error ? reject(error) : resolve())); + process.exit(0); + } + } + process.exit(1); + `, + ], + { + cwd: new URL('..', import.meta.url), + env: { ...process.env, TEST_DATABASE_URL: url }, + timeout: 12000, + } + ) + expect(JSON.parse(result.stdout)).toEqual({ before: 2, after: 0 }) + await sql.begin(async (tx) => { + await tx`SET LOCAL statement_timeout = '500ms'` + await tx`INSERT INTO project (id, name, owner_id) VALUES ('live-project', 'Live project', 'owner')` + await tx`INSERT INTO workspace (id, project_id, name, owner_id) VALUES ('live', 'live-project', 'Live', 'owner')` + await tx`SELECT * FROM project` + }) + await prepareAndEnforce(sql) + expect( + await sql`SELECT id FROM workspace WHERE id = 'live' AND project_id = 'live-project'` + ).toHaveLength(1) + }) + }, 15000) + + it('backfills legacy environments before installing mandatory membership', async () => { + await database(async (sql) => { + await sql`INSERT INTO workspace (id, name, owner_id) VALUES ('legacy', 'Legacy', 'owner')` + await prepareAndEnforce(sql) + expect( + await sql`SELECT 1 FROM workspace WHERE id = 'legacy' AND project_id IS NOT NULL` + ).toHaveLength(1) + await expect( + sql`INSERT INTO workspace (id, name, owner_id) VALUES ('unassigned', 'Invalid', 'owner')` + ).rejects.toSatisfy((error: unknown) => getPostgresErrorCode(error) === '23502') + expect(await sql`SELECT 1 FROM workspace WHERE id = 'unassigned'`).toHaveLength(0) + }) + }) + + it('rebuilds an interrupted unique index before attaching native constraints', async () => { + await database(async (sql) => { + await sql`INSERT INTO workspace (id,name,owner_id) VALUES ('root','Root','owner')` + await prepare(sql) + const prefix = migration.slice(0, migration.indexOf('CREATE UNIQUE INDEX CONCURRENTLY')) + await applyMigration(sql, prefix) + const held = createDeferred() + const release = createDeferred() + const reader = sql.begin(async (tx) => { + await tx`SELECT id FROM project` + held.resolve() + await release.promise + }) + await held.promise + try { + await expect( + applyMigration( + sql, + `SET statement_timeout = '100ms'; + --> statement-breakpoint + CREATE UNIQUE INDEX CONCURRENTLY project_id_organization_scope_unique + ON project(id,organization_scope_key);` + ) + ).rejects.toSatisfy((error: unknown) => getPostgresErrorCode(error) === '57014') + } finally { + release.resolve() + await reader + } + expect( + await sql`SELECT indisvalid FROM pg_index WHERE indexrelid = 'project_id_organization_scope_unique'::regclass` + ).toEqual([{ indisvalid: false }]) + await applyMigration(sql, migration) + await applyMigration(sql, migration) + expect( + await sql`SELECT convalidated FROM pg_constraint WHERE conname = 'workspace_project_organization_fk'` + ).toEqual([{ convalidated: true }]) + }) + }) + + it('supports a fresh database, repeated installation, and atomic first-environment creation', async () => { + await database(async (sql) => { + await prepareAndEnforce(sql) + await prepareAndEnforce(sql) + await sql.begin(async (tx) => { + await tx`INSERT INTO project (id, name, owner_id) VALUES ('new', 'New', 'owner')` + await tx`INSERT INTO workspace (id, project_id, name, owner_id) VALUES ('first', 'new', 'First', 'owner')` + await tx`INSERT INTO workflow VALUES ('flow', 'first', NULL)` + }) + await sql`INSERT INTO project (id, name, owner_id) VALUES ('empty', 'Empty', 'owner')` + expect(await sql`SELECT 1 FROM project`).toHaveLength(2) + }) + }) + + it('rejects invalid scope/fork changes and commits an atomic subtree disconnect', async () => { + await database(async (sql) => { + await seed(sql) + await prepareAndEnforce(sql) + await expect( + sql`UPDATE workspace SET organization_id = 'org' WHERE id = 'child'` + ).rejects.toSatisfy(constraintFailure) + await expect( + sql`UPDATE workspace SET project_id = NULL WHERE id = 'child'` + ).rejects.toSatisfy((error: unknown) => getPostgresErrorCode(error) === '23502') + await expect( + sql.begin(async (tx) => { + await tx`INSERT INTO project (id, name, owner_id) VALUES ('detached', 'Detached', 'owner')` + await tx`UPDATE workspace SET project_id = 'detached' WHERE id = 'child'` + }) + ).rejects.toSatisfy(constraintFailure) + await sql.begin(async (tx) => { + await tx`INSERT INTO project (id, name, owner_id) VALUES ('detached', 'Detached', 'owner')` + await tx`UPDATE workspace SET project_id = 'detached' WHERE id = 'child'` + await tx`UPDATE workspace SET forked_from_workspace_id = NULL WHERE id = 'child'` + }) + expect(await sql`SELECT DISTINCT project_id FROM workspace`).toHaveLength(2) + await expect(sql`DELETE FROM project WHERE id = 'detached'`).rejects.toSatisfy( + (error: unknown) => getPostgresErrorCode(error) === '23503' + ) + }) + }) + + it.each([ + [null, 'org'], + ['org', null], + ['org', 'other'], + ])('rejects organization mismatch %s / %s on both sides', async (projectOrg, workspaceOrg) => { + await database(async (sql) => { + await seed(sql) + await sql`INSERT INTO organization VALUES ('other')` + await sql.begin(async (tx) => { + await tx`UPDATE project SET organization_id = ${projectOrg}` + await tx`UPDATE workspace SET organization_id = ${projectOrg}` + }) + await expect( + sql`UPDATE workspace SET organization_id = ${workspaceOrg} WHERE id = 'child'` + ).rejects.toSatisfy(constraintFailure) + await expect(sql`UPDATE project SET organization_id = ${workspaceOrg}`).rejects.toSatisfy( + constraintFailure + ) + await sql.begin(async (tx) => { + await tx`UPDATE workspace SET organization_id = ${workspaceOrg}` + await tx`UPDATE project SET organization_id = ${workspaceOrg}` + }) + expect( + await sql`SELECT 1 FROM workspace w JOIN project p ON p.id = w.project_id + WHERE w.organization_id IS DISTINCT FROM p.organization_id` + ).toHaveLength(0) + }) + }) + + it('keeps generated scope keys distinct from organization identifiers and rejects overrides', async () => { + await database(async (sql) => { + await seed(sql) + await sql`INSERT INTO organization VALUES ('personal')` + await expect( + sql`UPDATE workspace SET organization_id = 'personal' WHERE id = 'child'` + ).rejects.toSatisfy(constraintFailure) + await expect( + sql`UPDATE workspace SET organization_scope_key = 'organization:org' WHERE id = 'child'` + ).rejects.toSatisfy((error: unknown) => getPostgresErrorCode(error) === '428C9') + }) + }) + + it('checks parent moves, allows atomic subtree moves and preserves membership on parent deletion', async () => { + await database(async (sql) => { + await seed(sql) + const [source] = await sql`SELECT project_id FROM workspace WHERE id = 'root'` + await sql`INSERT INTO project (id,name,owner_id) VALUES ('target','Target','owner')` + await sql`INSERT INTO workspace (id,project_id,name,owner_id,forked_from_workspace_id) + VALUES ('grandchild',${source.project_id},'Grandchild','owner','child')` + const before = await sql`SELECT id,xmin::text,updated_at FROM project ORDER BY id` + await expect( + sql`UPDATE workspace SET project_id = 'target' WHERE id = 'root'` + ).rejects.toSatisfy(constraintFailure) + await sql.begin(async (tx) => { + await tx`UPDATE workspace SET project_id = 'target' WHERE id = 'child'` + await tx`UPDATE workspace SET project_id = 'target' WHERE id = 'grandchild'` + await tx`UPDATE workspace SET forked_from_workspace_id = NULL WHERE id = 'child'` + }) + await sql`DELETE FROM workspace WHERE id = 'child'` + expect( + await sql`SELECT project_id,forked_from_workspace_id FROM workspace WHERE id = 'grandchild'` + ).toEqual([{ project_id: 'target', forked_from_workspace_id: null }]) + expect(await sql`SELECT id,xmin::text,updated_at FROM project ORDER BY id`).toEqual(before) + expect( + await sql`SELECT tgname FROM pg_trigger WHERE tgrelid IN ('workspace'::regclass,'project'::regclass) + AND NOT tgisinternal` + ).toHaveLength(0) + }) + }) + + it.each(['read committed', 'repeatable read'] as const)( + 'rejects a stale fork insertion after its parent moves under %s without touching Projects', + async (isolation) => { + await database(async (sql) => { + await seed(sql) + await sql`DELETE FROM workspace WHERE id = 'child'` + await sql`INSERT INTO project (id,name,owner_id) VALUES ('target','Target','owner')` + const before = await sql`SELECT id,xmin::text FROM project ORDER BY id` + await expect( + sql.begin(`isolation level ${isolation}`, async (tx) => { + const [parent] = await tx`SELECT project_id FROM workspace WHERE id = 'root'` + await sql`UPDATE workspace SET project_id = 'target' WHERE id = 'root'` + await tx`INSERT INTO workspace (id,project_id,name,owner_id,forked_from_workspace_id) + VALUES ('stale-child',${parent.project_id},'Stale','owner','root')` + }) + ).rejects.toSatisfy((error: unknown) => + ['23503', '40001'].includes(getPostgresErrorCode(error) ?? '') + ) + expect(await sql`SELECT id FROM workspace WHERE id = 'stale-child'`).toHaveLength(0) + expect(await sql`SELECT id,xmin::text FROM project ORDER BY id`).toEqual(before) + }) + } + ) + + it.each(['read committed', 'repeatable read'] as const)( + 'rejects stale organization assignment under %s', + async (isolation) => { + await database(async (sql) => { + await seed(sql) + await expect( + sql.begin(`isolation level ${isolation}`, async (tx) => { + const [parent] = await tx`SELECT id,organization_id FROM project` + await sql.begin(async (move) => { + await move`UPDATE project SET organization_id = 'org'` + await move`UPDATE workspace SET organization_id = 'org'` + }) + await tx`INSERT INTO workspace (id,project_id,name,owner_id,organization_id) + VALUES ('stale',${parent.id},'Stale','owner',${parent.organization_id})` + }) + ).rejects.toSatisfy((error: unknown) => + ['23503', '40001'].includes(getPostgresErrorCode(error) ?? '') + ) + expect(await sql`SELECT id FROM workspace WHERE id = 'stale'`).toHaveLength(0) + }) + } + ) + + it('allows concurrent workflow lifecycle writes without rewriting or locking their Project', async () => { + await database(async (sql) => { + await seed(sql) + const before = await sql`SELECT id, xmin::text FROM project` + const held = createDeferred() + const release = createDeferred() + const writer = sql.begin(async (tx) => { + await tx`INSERT INTO workflow VALUES ('first', 'root', NULL)` + held.resolve() + await release.promise + }) + await held.promise + try { + await sql.begin(async (tx) => { + await tx`SET LOCAL statement_timeout = '500ms'` + await tx`INSERT INTO workflow VALUES ('same-workspace', 'root', NULL), ('other-workspace', 'child', NULL)` + await tx`UPDATE workflow SET archived_at = now() WHERE id = 'other-workspace'` + await tx`UPDATE workflow SET workspace_id = 'child' WHERE id = 'same-workspace'` + await tx`DELETE FROM workflow WHERE id = 'other-workspace'` + }) + } finally { + release.resolve() + await writer + } + expect(await sql`SELECT id, xmin::text FROM project`).toEqual(before) + expect( + await sql`SELECT id FROM workflow WHERE id = 'same-workspace' AND workspace_id = 'child'` + ).toHaveLength(1) + }) + }) + + it('refuses enforcement when an archived Project retains an active workflow', async () => { + await database(async (sql) => { + await sql`INSERT INTO workspace (id, name, owner_id) VALUES ('root', 'Root', 'owner')` + await sql`INSERT INTO project (id, name, owner_id, archived_at) VALUES ('existing', 'Existing', 'owner', now())` + await sql`UPDATE workspace SET project_id = 'existing', archived_at = now()` + await sql`INSERT INTO workflow VALUES ('flow', 'root', NULL)` + await expect(applyMigration(sql, migration)).rejects.toSatisfy( + (error: unknown) => getPostgresErrorCode(error) === '55000' + ) + expect(await sql`SELECT to_regclass('project_workspace')::text AS name`).toEqual([ + { name: 'project_workspace' }, + ]) + await sql`UPDATE workflow SET archived_at = now()` + await applyMigration(sql, migration) + }) + }) + it('groups fork families, preserves partial assignments, and retains archived and detached Projects on replay', async () => { + await database(async (sql) => { + await sql`INSERT INTO "user" VALUES ('other-owner')` + await sql`INSERT INTO workspace (id, name, owner_id, organization_id, forked_from_workspace_id, archived_at) VALUES + ('a-root', 'Production', 'owner', 'org', NULL, NULL), + ('b-child', 'Staging', 'other-owner', 'org', 'a-root', NULL), + ('c-grandchild', 'Dev', 'owner', 'org', 'b-child', NULL), + ('d-detached', 'Detached', 'owner', NULL, NULL, NULL), + ('e-child', 'Child', 'owner', NULL, 'd-detached', NULL), + ('f-archived', 'Archive', 'owner', NULL, NULL, '2025-01-01'), + ('g-archived', 'Archive child', 'owner', NULL, 'f-archived', '2025-02-01')` + await sql`INSERT INTO project (id, name, owner_id, organization_id) VALUES ('existing', 'Keep name', 'owner', 'org')` + await sql`INSERT INTO project_workspace (project_id, workspace_id) VALUES ('existing', 'a-root')` + await prepareAndEnforce(sql) + expect(await sql`SELECT id FROM workspace WHERE project_id = 'existing' ORDER BY id`).toEqual( + [{ id: 'a-root' }, { id: 'b-child' }, { id: 'c-grandchild' }] + ) + const projects = + await sql`SELECT id, name, owner_id, archived_at::text FROM project ORDER BY name` + expect( + projects.map(({ name, owner_id, archived_at }) => ({ name, owner_id, archived_at })) + ).toEqual([ + { name: 'Archive - Project', owner_id: 'owner', archived_at: '2025-02-01 00:00:00' }, + { name: 'Detached - Project', owner_id: 'owner', archived_at: null }, + { name: 'Keep name', owner_id: 'owner', archived_at: null }, + ]) + await prepareAndEnforce(sql) + expect( + await sql`SELECT id, name, owner_id, archived_at::text FROM project ORDER BY name` + ).toEqual(projects) + }) + }) + + it('keeps committed families on conflict and resumes without duplicating Projects after remediation', async () => { + await database(async (sql) => { + await sql`INSERT INTO workspace (id, name, owner_id, organization_id, forked_from_workspace_id) VALUES + ('a-good', 'Good', 'owner', NULL, NULL), + ('b-root', 'Root', 'owner', 'org', NULL), + ('b-child', 'Child', 'owner', NULL, 'b-root')` + await expect(prepareAndEnforce(sql)).rejects.toSatisfy( + (error: unknown) => getPostgresErrorCode(error) === '55000' + ) + const committed = + await sql`SELECT project_id FROM workspace WHERE id = 'a-good' AND project_id IS NOT NULL` + expect(committed).toHaveLength(1) + expect( + await sql`SELECT 1 FROM workspace WHERE id LIKE 'b-%' AND project_id IS NOT NULL` + ).toHaveLength(0) + expect( + await sql`SELECT 1 FROM pg_constraint WHERE conname = 'workspace_fork_project_fk'` + ).toHaveLength(0) + await sql`UPDATE workspace SET organization_id = 'org' WHERE id = 'b-child'` + await prepareAndEnforce(sql) + expect( + await sql`SELECT project_id FROM workspace WHERE id = 'a-good' AND project_id IS NOT NULL` + ).toEqual(committed) + expect(await sql`SELECT 1 FROM project`).toHaveLength(2) + expect(await sql`SELECT 1 FROM workspace WHERE project_id IS NOT NULL`).toHaveLength(3) + }) + }) + + it('defers a locked family, commits unrelated work and detects a new fork before replay', async () => { + await database(async (sql) => { + await sql`INSERT INTO workspace (id,name,owner_id) VALUES ('a-free','Free','owner'),('b-busy','Busy','owner')` + const manifest = await discoverProjectBackfill(sql, 'fixture') + const held = createDeferred() + const release = createDeferred() + const writer = sql.begin(async (tx) => { + await tx`SELECT pg_advisory_xact_lock_shared(hashtextextended('project-backfill:b-busy',0))` + held.resolve() + await release.promise + await tx`INSERT INTO workspace (id,name,owner_id,forked_from_workspace_id) VALUES ('child','Child','owner','b-busy')` + }) + await held.promise + try { + await expect(assignProjectBackfillBatch(sql, [manifest.families[1]])).rejects.toThrow( + 'busy' + ) + expect(await assignProjectBackfillBatch(sql, [manifest.families[0]])).toMatchObject({ + assigned: 1, + }) + await sql.begin(async (tx) => { + await tx`SET LOCAL lock_timeout = '100ms'` + await tx`SELECT pg_advisory_xact_lock_shared(hashtextextended('project-backfill:a-free',0))` + await tx`UPDATE workspace SET name = 'Live edit' WHERE id = 'a-free'` + }) + } finally { + release.resolve() + await writer + } + await expect(assignProjectBackfillBatch(sql, [manifest.families[1]])).rejects.toThrow( + 'gained descendants' + ) + expect(await sql`SELECT id FROM workspace WHERE project_id IS NULL`).toHaveLength(2) + await prepareAndEnforce(sql) + expect( + await sql`SELECT DISTINCT project_id FROM workspace WHERE id IN ('b-busy','child')` + ).toHaveLength(1) + }) + }) + + it.each(['cycle', 'assigned-cycle', 'split', 'oversized'] as const)( + 'refuses %s legacy data without silently changing existing assignments', + async (scenario) => { + await database(async (sql) => { + await sql`INSERT INTO workspace (id, name, owner_id, forked_from_workspace_id) VALUES ('root', 'Root', 'owner', NULL), ('child', 'Child', 'owner', 'root')` + if (scenario === 'cycle' || scenario === 'assigned-cycle') { + if (scenario === 'assigned-cycle') { + await sql`INSERT INTO project (id, name, owner_id) VALUES ('existing', 'Existing', 'owner')` + await sql`INSERT INTO project_workspace (project_id, workspace_id) VALUES ('existing', 'root'), ('existing', 'child')` + } + await sql`UPDATE workspace SET forked_from_workspace_id = 'child' WHERE id = 'root'` + } else if (scenario === 'oversized') { + await sql`INSERT INTO workspace (id, name, owner_id, forked_from_workspace_id) SELECT 'extra-' || n, 'Extra', 'owner', 'root' FROM generate_series(1, 1000) n` + } else { + await sql`INSERT INTO project (id, name, owner_id) VALUES ('existing', 'Existing', 'owner')` + await sql`INSERT INTO project_workspace (project_id, workspace_id) VALUES ('existing', 'root')` + if (scenario === 'split') { + await sql`INSERT INTO project (id, name, owner_id) VALUES ('second', 'Second', 'owner')` + await sql`INSERT INTO project_workspace (project_id, workspace_id) VALUES ('second', 'child')` + } + } + const before = await sql`SELECT w.id, coalesce(w.project_id, pw.project_id) AS project_id + FROM workspace w LEFT JOIN project_workspace pw ON pw.workspace_id = w.id ORDER BY w.id` + if (scenario === 'split') { + expect(await discoverProjectBackfill(sql, 'fixture')).toMatchObject({ + conflicts: [{ id: 'root', reason: 'Fork family spans Projects' }], + }) + } + await expect(prepareAndEnforce(sql)).rejects.toSatisfy( + (error: unknown) => + getPostgresErrorCode(error) === (scenario === 'oversized' ? '54000' : '55000') + ) + expect( + await sql`SELECT w.id, coalesce(w.project_id, pw.project_id) AS project_id + FROM workspace w LEFT JOIN project_workspace pw ON pw.workspace_id = w.id ORDER BY w.id` + ).toEqual(before) + }) + } + ) + + it('backfills a representative multi-family dataset through independently committed batches', async () => { + await database(async (sql) => { + await sql`INSERT INTO workspace (id, name, owner_id) SELECT 'root-' || n, 'Environment ' || n, 'owner' FROM generate_series(1, 100) n` + await sql`INSERT INTO workspace (id, name, owner_id, forked_from_workspace_id) + SELECT 'child-' || r || '-' || n, 'Child', 'owner', 'root-' || r FROM generate_series(1, 100) r CROSS JOIN generate_series(1, 20) n` + await prepareAndEnforce(sql) + expect((await sql`SELECT count(*)::int AS count FROM project`)[0].count).toBe(100) + expect( + (await sql`SELECT count(*)::int AS count FROM workspace WHERE project_id IS NOT NULL`)[0] + .count + ).toBe(2100) + expect( + await sql`SELECT project_id FROM workspace GROUP BY project_id HAVING count(*) <> 21` + ).toHaveLength(0) + }) + }) + it.each(['separate-roots', 'large-family'] as const)( + 'preserves a complete existing Project with %s without putting it through legacy assignment', + async (shape) => { + await database(async (sql) => { + await sql`INSERT INTO project (id, name, owner_id) VALUES ('existing', 'Keep this Project', 'owner')` + await sql`INSERT INTO workspace (id, name, owner_id) VALUES ('root', 'Root', 'owner')` + const count = shape === 'large-family' ? 1001 : 150 + await sql`INSERT INTO workspace (id, name, owner_id, forked_from_workspace_id) + SELECT 'other-' || n, 'Other', 'owner', ${shape === 'large-family' ? 'root' : null} FROM generate_series(1, ${count}) n` + await sql`UPDATE workspace SET project_id = 'existing'` + await sql`INSERT INTO project_workspace (project_id, workspace_id) SELECT 'existing', id FROM workspace` + await prepareAndEnforce(sql) + expect(await sql`SELECT id, name FROM project`).toEqual([ + { id: 'existing', name: 'Keep this Project' }, + ]) + expect( + (await sql`SELECT count(*)::int AS count FROM workspace WHERE project_id IS NOT NULL`)[0] + .count + ).toBe(count + 1) + expect( + (await sql`SELECT count(*)::int AS count FROM workspace WHERE project_id = 'existing'`)[0] + .count + ).toBe(count + 1) + await prepareAndEnforce(sql) + expect(await sql`SELECT id, name FROM project`).toEqual([ + { id: 'existing', name: 'Keep this Project' }, + ]) + }) + } + ) +}) diff --git a/packages/db/scripts/push.integration.ts b/packages/db/scripts/push.integration.ts index f0d51756eda..cd67bfcb906 100644 --- a/packages/db/scripts/push.integration.ts +++ b/packages/db/scripts/push.integration.ts @@ -3,6 +3,12 @@ import { mkdtemp, readFile, rm, symlink, writeFile } from 'node:fs/promises' import { tmpdir } from 'node:os' import { join } from 'node:path' import { fileURLToPath } from 'node:url' +import drizzleConfig from '@sim/db/drizzle.config' +import { + completeProjectArchiveRepair, + countPendingProjectArchiveRepairs, + ensureProjectArchiveRepairJournal, +} from '@sim/db/maintenance/project-repairs' import { prepareForcedPush } from '@sim/db/scripts/prepare-push' import { readTestDatabaseUrl } from '@sim/db/testing/test-infrastructure' import { generateId } from '@sim/utils/id' @@ -33,7 +39,7 @@ describe('patched Drizzle push against PostgreSQL', () => { dialect: 'postgresql', schema: ${JSON.stringify(join(directory, 'schema.ts'))}, schemaFilter: ['public', 'old_scope', 'new_scope'], - tablesFilter: ['!script_migrations'], + tablesFilter: ${JSON.stringify(drizzleConfig.tablesFilter)}, dbCredentials: { url: process.env.DATABASE_URL }, }` ) @@ -58,7 +64,7 @@ describe('patched Drizzle push against PostgreSQL', () => { async function schema(source: string) { await writeFile( join(directory, 'schema.ts'), - `import { pgTable, pgSchema, pgEnum, text, integer, bigint, boolean, check, index } from ${JSON.stringify(import.meta.resolve('drizzle-orm/pg-core'))} + `import { pgTable, pgSchema, pgEnum, text, integer, bigint, boolean, check, timestamp, unique, foreignKey } from ${JSON.stringify(import.meta.resolve('drizzle-orm/pg-core'))} import { sql } from ${JSON.stringify(import.meta.resolve('drizzle-orm'))} ${source}` ) @@ -157,6 +163,25 @@ export const knowledgeBases = pgTable('knowledge_base', { ]) }, 30_000) + it('keeps the provider-cleanup gate visible across session search paths', async () => { + const scoped = postgres(fixtureUrl, { + max: 1, + connection: { search_path: 'old_scope,public' }, + onnotice: () => {}, + }) + try { + await sql`CREATE SCHEMA old_scope` + await ensureProjectArchiveRepairJournal(scoped) + const repair = { workspaceId: 'archived', archivedAt: '2026-01-01', workflowIds: ['flow'] } + await scoped`INSERT INTO project_backfill_archive_repairs (workspace_id,repair) VALUES ('archived',${JSON.stringify(repair)}::text::jsonb)` + expect(await countPendingProjectArchiveRepairs(sql)).toBe(1) + await completeProjectArchiveRepair(sql, repair) + expect(await countPendingProjectArchiveRepairs(scoped)).toBe(0) + } finally { + await scoped.end() + } + }) + it.each([false, true])( 'uses the direct migration connection for Project reconciliation (application URL present=%s)', async (applicationUrlPresent) => { @@ -185,126 +210,102 @@ export const knowledgeBases = pgTable('knowledge_base', { 30_000 ) - it('preserves legacy assignments without copying or synchronizing through schema push and replay', async () => { - await sql`CREATE TABLE project (id text PRIMARY KEY)` - await sql`CREATE TABLE workspace (id text PRIMARY KEY, forked_from_workspace_id text)` - await sql`CREATE TABLE project_workspace (project_id text NOT NULL, workspace_id text NOT NULL CONSTRAINT project_workspace_workspace_id_unique UNIQUE)` - await sql`INSERT INTO project VALUES ('family'), ('singleton')` - await sql`INSERT INTO workspace VALUES ('root', NULL), ('fork', 'root'), ('standalone', NULL)` - await sql`INSERT INTO project_workspace VALUES ('family', 'root'), ('family', 'fork'), ('singleton', 'standalone')` - await schema(`export const projects = pgTable('project', { id: text('id').primaryKey() }) + it.each([false, true])( + 'refuses Project contract through push and preserves legacy assignments (copied=%s)', + async (copied) => { + await sql`CREATE TABLE project (id text PRIMARY KEY)` + await sql`CREATE TABLE workspace (id text PRIMARY KEY)` + await sql`CREATE TABLE project_workspace (project_id text NOT NULL, workspace_id text NOT NULL UNIQUE)` + await sql`INSERT INTO project VALUES ('retained')` + await sql`INSERT INTO workspace VALUES ('environment')` + await sql`INSERT INTO project_workspace VALUES ('retained', 'environment')` + if (copied) { + await sql`ALTER TABLE workspace ADD COLUMN project_id text` + await sql`UPDATE workspace SET project_id = 'retained'` + } + await schema(`export const projects = pgTable('project', { id: text('id').primaryKey() }) +export const workspaces = pgTable('workspace', { id: text('id').primaryKey(), projectId: text('project_id').notNull() })`) + const result = runPush(['--force']) + expect(result.error).toBeUndefined() + expect(result.status, result.stdout + result.stderr).toBe(1) + expect(await sql`SELECT * FROM project_workspace`).toEqual([ + { project_id: 'retained', workspace_id: 'environment' }, + ]) + expect(await sql`SELECT id FROM workspace`).toEqual([{ id: 'environment' }]) + expect( + await sql`SELECT attnotnull FROM pg_attribute WHERE attrelid = 'workspace'::regclass AND attname = 'project_id' AND NOT attisdropped` + ).toEqual(copied ? [{ attnotnull: false }] : []) + }, + 30_000 + ) + + it('installs native Project constraints through the fresh-push reconciler and preserves it on replay', async () => { + function reconcileProjects() { + const result = spawnSync( + 'bun', + [ + '--no-env-file', + fileURLToPath(new URL('./reconcile-project-membership.ts', import.meta.url)), + ], + { + env: { ...process.env, DATABASE_URL: fixtureUrl, MIGRATION_DATABASE_URL: fixtureUrl }, + encoding: 'utf8', + timeout: 15000, + } + ) + expect(result.error).toBeUndefined() + expect(result.status, result.stdout + result.stderr).toBe(0) + } + await schema(`export const projects = pgTable('project', { + id: text('id').primaryKey(), name: text('name').notNull(), ownerId: text('owner_id').notNull(), + organizationScopeKey: text('organization_scope_key').generatedAlwaysAs(sql\`CASE WHEN organization_id IS NULL THEN 'personal' ELSE 'organization:' || organization_id END\`), + organizationId: text('organization_id'), archivedAt: timestamp('archived_at'), updatedAt: timestamp('updated_at').notNull().defaultNow(), +}, (t) => [unique('project_id_organization_scope_unique').on(t.id, t.organizationScopeKey)]) export const workspaces = pgTable('workspace', { - id: text('id').primaryKey(), forkedFromWorkspaceId: text('forked_from_workspace_id'), projectId: text('project_id'), -}, (table) => [index('workspace_project_id_id_idx').on(table.projectId, table.id).concurrently()]) -export const rollout = pgTable('project_membership_rollout', { - id: text('id').primaryKey(), phase: text('phase').notNull().default('connector'), -}, (table) => [check('project_membership_rollout_singleton', sql\`\${table.id} = 'membership'\`), check('project_membership_rollout_phase', sql\`\${table.phase} IN ('connector', 'column')\`)]) -export const memberships = pgTable('project_workspace', { - projectId: text('project_id').notNull(), workspaceId: text('workspace_id').notNull().unique(), + id: text('id').primaryKey(), name: text('name').notNull(), ownerId: text('owner_id').notNull(), + projectId: text('project_id').notNull().references(() => projects.id, { onDelete: 'restrict' }), + organizationScopeKey: text('organization_scope_key').generatedAlwaysAs(sql\`CASE WHEN organization_id IS NULL THEN 'personal' ELSE 'organization:' || organization_id END\`), + organizationId: text('organization_id'), archivedAt: timestamp('archived_at'), forkedFromWorkspaceId: text('forked_from_workspace_id').references(() => workspaces.id, { onDelete: 'set null' }), +}, (t) => [ + unique('workspace_id_project_unique').on(t.id, t.projectId), + foreignKey({name: 'workspace_project_organization_fk', columns: [t.projectId,t.organizationScopeKey], foreignColumns: [projects.id,projects.organizationScopeKey]}), + foreignKey({name: 'workspace_fork_project_fk', columns: [t.forkedFromWorkspaceId,t.projectId], foreignColumns: [t.id,t.projectId]}), +]) +export const workflows = pgTable('workflow', { + id: text('id').primaryKey(), workspaceId: text('workspace_id'), archivedAt: timestamp('archived_at'), })`) - const first = runPush(['--force']) + const first = push() expect(first.error, first.stderr).toBeUndefined() - // Other reconcilers require their own tables; their failure must not undo Project preparation. - expect( - await sql`SELECT id, project_id FROM workspace ORDER BY id`, - first.stdout + first.stderr - ).toEqual([ - { id: 'fork', project_id: null }, - { id: 'root', project_id: null }, - { id: 'standalone', project_id: null }, + expect(first.status, first.stdout + first.stderr).toBe(0) + reconcileProjects() + expect(await sql`SELECT to_regclass('public.project_membership_rollout') AS marker`).toEqual([ + { marker: null }, ]) - expect(await sql`SELECT phase FROM project_membership_rollout`).toEqual([ - { phase: 'connector' }, - ]) - await sql`UPDATE project_membership_rollout SET phase = 'column' WHERE id = 'membership'` - await sql`UPDATE project_workspace SET project_id = 'singleton' WHERE workspace_id = 'fork'` - await sql`UPDATE workspace SET project_id = 'family' WHERE id = 'standalone'` - const indexBeforeReplay = await sql`SELECT indexrelid, indisvalid FROM pg_index - WHERE indexrelid = 'workspace_project_id_id_idx'::regclass` - expect(indexBeforeReplay).toEqual([{ indexrelid: expect.any(Number), indisvalid: true }]) - /** Replay must restore real DDL, not pass from unchanged state after an early child failure. */ - await sql`ALTER TABLE workspace DROP CONSTRAINT workspace_project_id_project_id_fk` - const repeated = runPush(['--force']) + await sql`INSERT INTO project (id,name,owner_id) VALUES ('empty','Empty','owner')` + await sql`DELETE FROM project WHERE id = 'empty'` + await sql.begin(async (tx) => { + await tx`INSERT INTO project (id,name,owner_id) VALUES ('family','Family','owner')` + await tx`INSERT INTO workspace (id,name,owner_id,project_id,forked_from_workspace_id) VALUES ('root','Root','owner','family',NULL), ('fork','Fork','owner','family','root')` + }) + const repeated = push() expect(repeated.error, repeated.stderr).toBeUndefined() - /** This partial fixture reaches Project reconciliation, then lacks the next reconciler's tables. */ - expect(repeated.status, repeated.stdout + repeated.stderr).toBe(1) - expect( - await sql`SELECT confdeltype, convalidated FROM pg_constraint - WHERE conrelid = 'workspace'::regclass AND conname = 'workspace_project_id_project_id_fk'` - ).toEqual([{ confdeltype: 'r', convalidated: false }]) - expect(await sql`SELECT phase FROM project_membership_rollout`).toEqual([{ phase: 'column' }]) - expect( - await sql`SELECT indexrelid, indisvalid FROM pg_index - WHERE indexrelid = 'workspace_project_id_id_idx'::regclass` - ).toEqual(indexBeforeReplay) - expect( - await sql`SELECT w.id, w.project_id, pw.project_id AS legacy FROM workspace w - JOIN project_workspace pw ON pw.workspace_id = w.id ORDER BY w.id` - ).toEqual([ - { id: 'fork', project_id: null, legacy: 'singleton' }, - { id: 'root', project_id: null, legacy: 'family' }, - { id: 'standalone', project_id: 'family', legacy: 'singleton' }, + expect(repeated.status, repeated.stdout + repeated.stderr).toBe(0) + reconcileProjects() + expect(await sql`SELECT to_regclass('public.project_membership_rollout') AS marker`).toEqual([ + { marker: null }, ]) - }, 60_000) - - it('repairs an interrupted Project index build during schema-push reconciliation', async () => { - await sql`CREATE TABLE project (id text PRIMARY KEY)` - await sql`CREATE TABLE workspace (id text PRIMARY KEY, project_id text)` - await sql`CREATE TABLE project_workspace (project_id text NOT NULL, workspace_id text NOT NULL UNIQUE)` - await sql`INSERT INTO project VALUES ('family')` - await sql`CREATE TABLE project_membership_rollout (id text PRIMARY KEY, phase text NOT NULL)` - await sql`INSERT INTO project_membership_rollout VALUES ('membership', 'column')` - await sql`INSERT INTO workspace VALUES ('root', 'family'), ('fork', 'family')` - await expect( - sql`CREATE UNIQUE INDEX CONCURRENTLY workspace_project_id_id_idx ON workspace(project_id)` - ).rejects.toMatchObject({ code: '23505' }) - expect( - await sql`SELECT indisvalid FROM pg_index WHERE indexrelid = 'workspace_project_id_id_idx'::regclass` - ).toEqual([{ indisvalid: false }]) - const result = spawnSync( - 'bun', - [ - '--no-env-file', - fileURLToPath(new URL('./reconcile-project-membership.ts', import.meta.url)), - ], - { - env: { ...process.env, DATABASE_URL: fixtureUrl, MIGRATION_DATABASE_URL: fixtureUrl }, - encoding: 'utf8', - timeout: 15_000, - } - ) - expect(result.error).toBeUndefined() - expect(result.status, result.stdout + result.stderr).toBe(0) - expect( - await sql`SELECT indisvalid, indisunique FROM pg_index - WHERE indexrelid = 'workspace_project_id_id_idx'::regclass` - ).toEqual([{ indisvalid: true, indisunique: false }]) - expect(await sql`SELECT id, project_id FROM workspace ORDER BY id`).toEqual([ + expect(await sql`SELECT id,project_id FROM workspace ORDER BY id`).toEqual([ { id: 'fork', project_id: 'family' }, { id: 'root', project_id: 'family' }, ]) - }) - - it('refuses a schema downgrade before recreating the retired Project connector', async () => { - await sql`CREATE TABLE project (id text PRIMARY KEY)` - await sql`CREATE TABLE workspace (id text PRIMARY KEY, project_id text NOT NULL)` - await sql`INSERT INTO project VALUES ('retained')` - await sql`INSERT INTO workspace VALUES ('environment', 'retained')` - await schema(`export const projects = pgTable('project', { id: text('id').primaryKey() }) -export const workspaces = pgTable('workspace', { id: text('id').primaryKey(), projectId: text('project_id') }) -export const memberships = pgTable('project_workspace', { - projectId: text('project_id').notNull(), workspaceId: text('workspace_id').notNull().unique(), -})`) - const result = runPush(['--force']) - expect(result.status, result.stdout + result.stderr).toBe(1) + await expect( + sql`UPDATE workspace SET organization_id = 'other' WHERE id = 'fork'` + ).rejects.toMatchObject({ code: '23503' }) + await sql`DELETE FROM workspace` expect(await sql`SELECT to_regclass('public.project_workspace') AS legacy`).toEqual([ { legacy: null }, ]) - expect( - await sql`SELECT attnotnull FROM pg_attribute WHERE attrelid = 'workspace'::regclass AND attname = 'project_id'` - ).toEqual([{ attnotnull: true }]) - expect(await sql`SELECT project_id FROM workspace`).toEqual([{ project_id: 'retained' }]) - }, 30_000) + }, 60_000) it('retires the legacy size bridge without losing bigint or unbackfilled values', async () => { await sql`CREATE TABLE workspace_files (id text PRIMARY KEY, size integer NOT NULL, size_bytes bigint)` @@ -385,12 +386,15 @@ export const memberships = pgTable('project_workspace', { expect(repeated.stdout).toContain('No changes detected') }, 60_000) - it('creates independent tables and enums while preserving the excluded script ledger', async () => { + it('creates independent tables and enums while preserving runner journals', async () => { await sql`CREATE TYPE old_status AS ENUM ('active')` await sql`CREATE TABLE old_records (id text PRIMARY KEY, status old_status)` await sql`INSERT INTO old_records VALUES ('old-row', 'active')` await sql`CREATE TABLE script_migrations (name text PRIMARY KEY)` await sql`INSERT INTO script_migrations VALUES ('completed-fixture-migration')` + await ensureProjectArchiveRepairJournal(sql) + await sql`INSERT INTO project_backfill_archive_repairs (workspace_id, repair) + VALUES ('pending-workspace', '{"workspaceId":"pending-workspace"}'::jsonb)` await schema(`export const status = pgEnum('new_status', ['active']) export const records = pgTable('new_records', { id: text('id').primaryKey(), status: status('status') })`) const result = push() @@ -403,6 +407,13 @@ export const records = pgTable('new_records', { id: text('id').primaryKey(), sta expect(await sql`SELECT * FROM script_migrations`).toEqual([ { name: 'completed-fixture-migration' }, ]) + expect(await sql`SELECT * FROM project_backfill_archive_repairs`).toEqual([ + { + workspace_id: 'pending-workspace', + repair: { workspaceId: 'pending-workspace' }, + completed_at: null, + }, + ]) }, 30_000) it('creates a new schema instead of moving a removed schema', async () => { diff --git a/packages/db/scripts/reconcile-project-membership.ts b/packages/db/scripts/reconcile-project-membership.ts index 68534eeced3..38f212df5d8 100644 --- a/packages/db/scripts/reconcile-project-membership.ts +++ b/packages/db/scripts/reconcile-project-membership.ts @@ -1,4 +1,4 @@ -import { readFile } from 'node:fs/promises' +import { enforceProjectMembership } from '@sim/db/maintenance/project-enforcement' import { createLogger } from '@sim/logger' import { getErrorMessage, getPostgresErrorCode } from '@sim/utils/errors' import postgres from 'postgres' @@ -17,34 +17,15 @@ try { EXISTS (SELECT 1 FROM pg_attribute WHERE attrelid = to_regclass('public.workspace') AND attname = 'project_id' AND NOT attisdropped AND attnotnull) AS required ` - if (state.workspace && state.required && !state.connector) { + if (state.workspace && (state.connector || !state.required || !state.project)) { throw new Error( - 'Project membership is already contracted; schema push from the compatibility release is unsupported. Use the current release for schema changes' + 'Existing Project databases must complete the Project script migration 0031 before schema push. Use the phased migration path for retained data; schema push cannot bypass the writer-drain requirement' ) } - if (!process.argv.includes('--prepare') && state.workspace && state.project && state.connector) { - // Reuse additive DDL so schema push preserves the same legacy membership state as upgrades. - const source = await readFile( - new URL('../migrations/0406_workspace_project_column.sql', import.meta.url), - 'utf8' - ) - const connection = await sql.reserve() - try { - for (const statement of source.split('--> statement-breakpoint')) { - if (statement.includes('DROP INDEX CONCURRENTLY IF EXISTS "workspace_project_id_id_idx"')) { - // Migration retries rebuild interrupted indexes; routine push keeps healthy indexes intact. - const [index] = await connection<{ healthy: boolean }[]>` - SELECT indisvalid AND indisready AS healthy FROM pg_index - WHERE indexrelid = to_regclass('public.workspace_project_id_id_idx') - ` - if (index?.healthy) continue - } - if (statement.trim()) await connection.unsafe(statement) - } - } finally { - connection.release() - } - logger.info('Nullable Project membership column prepared') + if (!process.argv.includes('--prepare') && state.workspace && state.project) { + /** Drizzle cannot express deferred FK timing; fresh push shares the migration finalizer. */ + await enforceProjectMembership(sql) + logger.info('Project membership validation and native constraints completed') } } catch (error) { logger.error('Project schema push stopped; resolve the error and retry', { diff --git a/packages/db/testing/workspace-fixtures.ts b/packages/db/testing/workspace-fixtures.ts new file mode 100644 index 00000000000..95480915b00 --- /dev/null +++ b/packages/db/testing/workspace-fixtures.ts @@ -0,0 +1,119 @@ +import type { db } from '@sim/db' +import { project, workspace } from '@sim/db/schema' +import { generateId } from '@sim/utils/id' +import { and, eq, inArray, notExists, type SQL, sql } from 'drizzle-orm' +import type { PgInsertValue } from 'drizzle-orm/pg-core' + +const fixtureProjects = new WeakMap>() + +type FixtureDatabase = typeof db +type FixtureTransaction = Parameters[0]>[0] +type WorkspaceRow = Omit< + PgInsertValue, + 'id' | 'projectId' | 'forkedFromWorkspaceId' +> & { + id: string + projectId?: string + forkedFromWorkspaceId?: string | null +} + +/** Seeds real environments with the Project membership required by the migrated schema. */ +export async function insertWorkspaceFixture( + database: FixtureDatabase | FixtureTransaction, + values: WorkspaceRow | WorkspaceRow[] +) { + const createdProjectIds = new Set() + const rows = await database.transaction(async (tx) => { + const inputs = Array.isArray(values) ? values : [values] + const byId = new Map(inputs.map((row) => [row.id, row])) + const children = new Map() + const ordered: WorkspaceRow[] = [] + for (const row of inputs) { + if (row.forkedFromWorkspaceId && byId.has(row.forkedFromWorkspaceId)) { + const siblings = children.get(row.forkedFromWorkspaceId) ?? [] + siblings.push(row) + children.set(row.forkedFromWorkspaceId, siblings) + } else ordered.push(row) + } + for (const row of ordered) ordered.push(...(children.get(row.id) ?? [])) + if (ordered.length !== inputs.length) throw new Error('Workspace fixture contains a fork cycle') + const inserted = new Map() + for (const row of ordered) { + const [parent] = row.forkedFromWorkspaceId + ? await tx + .select({ projectId: workspace.projectId }) + .from(workspace) + .where(eq(workspace.id, row.forkedFromWorkspaceId)) + : [] + if (row.forkedFromWorkspaceId && !parent) + throw new Error('Workspace fixture parent does not exist') + if (parent && row.projectId && row.projectId !== parent.projectId) + throw new Error('Workspace fixture cannot fork across Projects') + const projectId = row.projectId ?? parent?.projectId ?? generateId() + if (!parent && !createdProjectIds.has(projectId)) { + const [existing] = await tx + .select({ id: project.id }) + .from(project) + .where(eq(project.id, projectId)) + if (!existing) { + await tx.insert(project).values({ + id: projectId, + name: 'Fixture project', + ownerId: row.ownerId, + organizationId: row.organizationId, + }) + createdProjectIds.add(projectId) + } + } + const [created] = await tx + .insert(workspace) + .values({ ...row, projectId }) + .returning() + inserted.set(created.id, created) + } + for (const projectId of createdProjectIds) { + const family = [...inserted.values()].filter((row) => row.projectId === projectId) + const archivedAt = family.every((row) => row.archivedAt !== null) + ? new Date(Math.max(...family.map((row) => row.archivedAt?.getTime() ?? 0))) + : null + if (archivedAt) await tx.update(project).set({ archivedAt }).where(eq(project.id, projectId)) + } + return inputs.map((row) => { + const created = inserted.get(row.id) + if (!created) throw new Error('Workspace fixture insertion did not return a row') + return created + }) + }) + const owned = fixtureProjects.get(database) ?? new Set() + for (const id of createdProjectIds) owned.add(id) + fixtureProjects.set(database, owned) + return rows +} + +/** Removes fixture environments and their now-empty Projects in the same transaction. */ +export async function deleteWorkspaceFixture( + database: FixtureDatabase | FixtureTransaction, + condition: SQL | undefined +) { + if (!condition) throw new Error('Workspace fixture deletion requires a predicate') + await database.transaction(async (tx) => { + const rows = await tx + .select({ projectId: workspace.projectId }) + .from(workspace) + .where(condition) + await tx.delete(workspace).where(condition) + const owned = fixtureProjects.get(database) + const projectIds = rows.map((row) => row.projectId).filter((id) => owned?.has(id)) + if (projectIds.length) + await tx + .delete(project) + .where( + and( + inArray(project.id, projectIds), + notExists( + tx.select({ one: sql`1` }).from(workspace).where(eq(workspace.projectId, project.id)) + ) + ) + ) + }) +} diff --git a/packages/testing/src/mocks/database.mock.ts b/packages/testing/src/mocks/database.mock.ts index c2406d06b59..a075a48a5f4 100644 --- a/packages/testing/src/mocks/database.mock.ts +++ b/packages/testing/src/mocks/database.mock.ts @@ -151,10 +151,6 @@ function dequeueChainRows(tables: unknown[]): unknown[] | null { const queue = tableRowQueues.get(table) if (queue && queue.length > 0) return queue.shift() ?? null } - /** Default unit fixtures represent a migrated database after authority cutover. */ - if (tables.includes(schemaMock.projectMembershipRollout)) { - return [{ id: 'membership', phase: 'column' }] - } return null } @@ -244,18 +240,7 @@ const update = chainSpy() const set = chainSpy() const del = chainSpy() const returning = vi.fn(() => Promise.resolve([] as unknown[])) -const execute = vi.fn((statement?: unknown) => { - if ( - statement && - typeof statement === 'object' && - 'toSQL' in statement && - typeof statement.toSQL === 'function' && - statement.toSQL().sql.includes("to_regclass('public.project_membership_rollout')") - ) { - return Promise.resolve([{ marker: true, complete: false }] as unknown[]) - } - return Promise.resolve([] as unknown[]) -}) +const execute = vi.fn(() => Promise.resolve([] as unknown[])) const query = vi.fn(() => Promise.resolve([] as unknown[])) const onConflictDoUpdate = vi.fn(() => ({ returning }) as unknown as Promise) const onConflictDoNothing = vi.fn(() => ({ returning }) as unknown as Promise) diff --git a/packages/testing/src/mocks/schema-tables.generated.ts b/packages/testing/src/mocks/schema-tables.generated.ts index 284a7c9d00d..277f83992ea 100644 --- a/packages/testing/src/mocks/schema-tables.generated.ts +++ b/packages/testing/src/mocks/schema-tables.generated.ts @@ -489,6 +489,7 @@ export const GENERATED_SCHEMA_TABLES = { 'logoUrl', 'ownerId', 'organizationId', + 'organizationScopeKey', 'projectId', 'workspaceMode', 'billedAccountUserId', @@ -506,9 +507,16 @@ export const GENERATED_SCHEMA_TABLES = { 'createdAt', 'updatedAt', ], - project: ['id', 'name', 'organizationId', 'ownerId', 'archivedAt', 'createdAt', 'updatedAt'], - projectMembershipRollout: ['id', 'phase'], - projectWorkspace: ['projectId', 'workspaceId', 'createdAt'], + project: [ + 'id', + 'name', + 'organizationId', + 'organizationScopeKey', + 'ownerId', + 'archivedAt', + 'createdAt', + 'updatedAt', + ], workspaceForkResourceMap: [ 'id', 'childWorkspaceId', diff --git a/scripts/check-project-rollout.test.ts b/scripts/check-project-rollout.test.ts new file mode 100644 index 00000000000..c1052e7671b --- /dev/null +++ b/scripts/check-project-rollout.test.ts @@ -0,0 +1,114 @@ +import { spawnSync } from 'node:child_process' +import { mkdtemp, rm, writeFile } from 'node:fs/promises' +import { tmpdir } from 'node:os' +import { delimiter, join, resolve } from 'node:path' +import { describe, expect, it } from 'vitest' + +const digest = `sha256:${'a'.repeat(64)}` +const otherDigest = `sha256:${'b'.repeat(64)}` +const execution = (id: string, time: number, revision: string, status = 'Succeeded') => ({ + pipelineExecutionId: id, + startTime: new Date(time * 1000).toISOString(), + status, + sourceRevisions: [{ actionName: 'ECR_Source', revisionId: revision }], +}) +const current = execution('current', 2, digest) +const older = execution('older', 1, otherDigest) +const pending = execution('pending', 3, otherDigest, 'InProgress') + +const cases = [ + { + name: 'accepts the latest completed acknowledged release in unordered history', + histories: [ + [older, current], + [older, current], + ], + allowed: true, + }, + { + name: 'rejects a stale acknowledgement after a different release succeeds', + histories: [[execution('different', 3, otherDigest), current]], + allowed: false, + }, + { + name: 'rejects a newer pending release even when an older success appears first', + histories: [[current, pending]], + allowed: false, + }, + { + name: 'rejects a newer deployment discovered in the final unordered history', + histories: [[current], [current, pending]], + allowed: false, + }, +] as const + +/** Exercises the preflight CLI against deployment histories without contacting an AWS account. */ +describe('Project column rollout release acknowledgement', () => { + it.each(cases)('$name', async ({ histories, allowed }) => { + const directory = await mkdtemp(join(tmpdir(), 'project-rollout-')) + try { + const fixture = join(directory, 'history.json') + await writeFile(fixture, JSON.stringify({ histories, digest })) + await writeFile( + join(directory, 'aws'), + `#!/usr/bin/env python3 +import json, os, pathlib, sys +fixture = pathlib.Path(os.environ['PROJECT_ROLLOUT_FIXTURE']) +data = json.loads(fixture.read_text()) +args = sys.argv +if 'list-pipeline-executions' in args: + counter = fixture.with_suffix('.count') + index = int(counter.read_text()) if counter.exists() else 0 + counter.write_text(str(index + 1)) + result = {'pipelineExecutionSummaries': data['histories'][min(index, len(data['histories']) - 1)]} +elif 'get-deployment-group' in args: + result = {'deploymentGroupInfo': {'ecsServices': [{'clusterName': 'fixture', 'serviceName': 'fixture'}]}} +elif 'describe-services' in args: + result = {'services': [{'desiredCount': 1, 'runningCount': 1, 'pendingCount': 0}]} +elif 'list-tasks' in args: + result = {'taskArns': ['fixture-task'] if args[args.index('--desired-status') + 1] == 'RUNNING' else []} +elif 'describe-tasks' in args: + result = {'tasks': [{'lastStatus': 'RUNNING', 'desiredStatus': 'RUNNING', 'containers': [{'name': 'app', 'imageDigest': data['digest']}]}]} +else: + raise SystemExit('Unexpected AWS command') +print(json.dumps(result)) +`, + { mode: 0o700 } + ) + const result = spawnSync( + 'python3', + [ + resolve('.github/scripts/check-project-rollout.py'), + '--environment', + 'staging', + '--region', + 'fixture-region', + '--expected-image-digest', + digest, + ], + { + encoding: 'utf8', + timeout: 10_000, + env: { + ...process.env, + PATH: `${directory}${delimiter}${process.env.PATH}`, + PROJECT_ROLLOUT_FIXTURE: fixture, + }, + } + ) + if (allowed) { + expect(result.status, result.stderr).toBe(0) + expect(JSON.parse(result.stdout)).toMatchObject({ + ecsRetired: true, + pipelineExecutionId: 'current', + operatorAcknowledgedColumnWritersAndWorkers: true, + }) + } else { + expect(result.status, result.stdout).toBe(1) + expect(result.stderr).toContain('Project column rollout preflight refused:') + } + } finally { + await rm(directory, { recursive: true, force: true }) + } + }) +})