fed8b2eed7
Backend release / release (push) Waiting to run
Bandit Security Scan / bandit_scan (push) Waiting to run
Build and push multi-arch DocsGPT Docker image / build (linux/amd64, ubuntu-latest, amd64) (push) Waiting to run
Build and push multi-arch DocsGPT Docker image / build (linux/arm64, ubuntu-24.04-arm, arm64) (push) Waiting to run
Build and push multi-arch DocsGPT Docker image / manifest (push) Blocked by required conditions
Build and push DocsGPT FE Docker image for development / build (linux/amd64, ubuntu-latest, amd64) (push) Waiting to run
Build and push DocsGPT FE Docker image for development / build (linux/arm64, ubuntu-24.04-arm, arm64) (push) Waiting to run
Build and push DocsGPT FE Docker image for development / manifest (push) Blocked by required conditions
Python linting / ruff (push) Waiting to run
Run python tests with pytest / Run tests and count coverage (3.12) (push) Waiting to run
React Widget Build / build (push) Waiting to run
928 lines
34 KiB
Python
928 lines
34 KiB
Python
"""0001 initial schema — consolidated baseline for user-data tables.
|
|
|
|
Revision ID: 0001_initial
|
|
Revises:
|
|
Create Date: 2026-04-13
|
|
"""
|
|
|
|
from typing import Sequence, Union
|
|
|
|
from alembic import op
|
|
|
|
|
|
revision: str = "0001_initial"
|
|
down_revision: Union[str, None] = None
|
|
branch_labels: Union[str, Sequence[str], None] = None
|
|
depends_on: Union[str, Sequence[str], None] = None
|
|
|
|
|
|
def upgrade() -> None:
|
|
# ------------------------------------------------------------------
|
|
# Extensions
|
|
# ------------------------------------------------------------------
|
|
op.execute('CREATE EXTENSION IF NOT EXISTS "pgcrypto";')
|
|
op.execute('CREATE EXTENSION IF NOT EXISTS "citext";')
|
|
|
|
# ------------------------------------------------------------------
|
|
# Trigger functions
|
|
# ------------------------------------------------------------------
|
|
op.execute(
|
|
"""
|
|
CREATE FUNCTION set_updated_at() RETURNS trigger
|
|
LANGUAGE plpgsql AS $$
|
|
BEGIN
|
|
NEW.updated_at = now();
|
|
RETURN NEW;
|
|
END;
|
|
$$;
|
|
"""
|
|
)
|
|
|
|
op.execute(
|
|
"""
|
|
CREATE FUNCTION ensure_user_exists() RETURNS trigger
|
|
LANGUAGE plpgsql AS $$
|
|
BEGIN
|
|
IF NEW.user_id IS NOT NULL THEN
|
|
INSERT INTO users (user_id) VALUES (NEW.user_id)
|
|
ON CONFLICT (user_id) DO NOTHING;
|
|
END IF;
|
|
RETURN NEW;
|
|
END;
|
|
$$;
|
|
"""
|
|
)
|
|
|
|
op.execute(
|
|
"""
|
|
CREATE FUNCTION cleanup_message_attachment_refs() RETURNS trigger
|
|
LANGUAGE plpgsql AS $$
|
|
BEGIN
|
|
UPDATE conversation_messages
|
|
SET attachments = array_remove(attachments, OLD.id)
|
|
WHERE OLD.id = ANY(attachments);
|
|
RETURN OLD;
|
|
END;
|
|
$$;
|
|
"""
|
|
)
|
|
|
|
op.execute(
|
|
"""
|
|
CREATE FUNCTION cleanup_agent_extra_source_refs() RETURNS trigger
|
|
LANGUAGE plpgsql AS $$
|
|
BEGIN
|
|
UPDATE agents
|
|
SET extra_source_ids = array_remove(extra_source_ids, OLD.id)
|
|
WHERE OLD.id = ANY(extra_source_ids);
|
|
RETURN OLD;
|
|
END;
|
|
$$;
|
|
"""
|
|
)
|
|
|
|
op.execute(
|
|
"""
|
|
CREATE FUNCTION cleanup_user_agent_prefs() RETURNS trigger
|
|
LANGUAGE plpgsql AS $$
|
|
DECLARE
|
|
agent_id_text text := OLD.id::text;
|
|
BEGIN
|
|
UPDATE users
|
|
SET agent_preferences = jsonb_set(
|
|
jsonb_set(
|
|
agent_preferences,
|
|
'{pinned}',
|
|
COALESCE((
|
|
SELECT jsonb_agg(e)
|
|
FROM jsonb_array_elements(
|
|
COALESCE(agent_preferences->'pinned', '[]'::jsonb)
|
|
) e
|
|
WHERE (e #>> '{}') <> agent_id_text
|
|
), '[]'::jsonb)
|
|
),
|
|
'{shared_with_me}',
|
|
COALESCE((
|
|
SELECT jsonb_agg(e)
|
|
FROM jsonb_array_elements(
|
|
COALESCE(agent_preferences->'shared_with_me', '[]'::jsonb)
|
|
) e
|
|
WHERE (e #>> '{}') <> agent_id_text
|
|
), '[]'::jsonb)
|
|
)
|
|
WHERE agent_preferences->'pinned' @> to_jsonb(agent_id_text)
|
|
OR agent_preferences->'shared_with_me' @> to_jsonb(agent_id_text);
|
|
RETURN OLD;
|
|
END;
|
|
$$;
|
|
"""
|
|
)
|
|
|
|
op.execute(
|
|
"""
|
|
CREATE FUNCTION conversation_messages_fill_user_id() RETURNS trigger
|
|
LANGUAGE plpgsql AS $$
|
|
BEGIN
|
|
IF NEW.user_id IS NULL THEN
|
|
SELECT user_id INTO NEW.user_id
|
|
FROM conversations
|
|
WHERE id = NEW.conversation_id;
|
|
END IF;
|
|
RETURN NEW;
|
|
END;
|
|
$$;
|
|
"""
|
|
)
|
|
|
|
# ------------------------------------------------------------------
|
|
# Tables
|
|
# ------------------------------------------------------------------
|
|
op.execute(
|
|
"""
|
|
CREATE TABLE users (
|
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
user_id TEXT NOT NULL UNIQUE,
|
|
agent_preferences JSONB NOT NULL
|
|
DEFAULT '{"pinned": [], "shared_with_me": []}'::jsonb,
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
|
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
|
);
|
|
"""
|
|
)
|
|
|
|
op.execute(
|
|
"""
|
|
CREATE TABLE prompts (
|
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
user_id TEXT NOT NULL,
|
|
name TEXT NOT NULL,
|
|
content TEXT NOT NULL,
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
|
updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
|
legacy_mongo_id TEXT
|
|
);
|
|
"""
|
|
)
|
|
|
|
op.execute(
|
|
"""
|
|
CREATE TABLE user_tools (
|
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
user_id TEXT NOT NULL,
|
|
name TEXT NOT NULL,
|
|
custom_name TEXT,
|
|
display_name TEXT,
|
|
description TEXT,
|
|
config JSONB NOT NULL DEFAULT '{}'::jsonb,
|
|
config_requirements JSONB NOT NULL DEFAULT '{}'::jsonb,
|
|
actions JSONB NOT NULL DEFAULT '[]'::jsonb,
|
|
status BOOLEAN NOT NULL DEFAULT true,
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
|
updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
|
legacy_mongo_id TEXT
|
|
);
|
|
"""
|
|
)
|
|
|
|
op.execute(
|
|
"""
|
|
CREATE TABLE token_usage (
|
|
id BIGSERIAL PRIMARY KEY,
|
|
user_id TEXT,
|
|
api_key TEXT,
|
|
agent_id UUID,
|
|
prompt_tokens INTEGER NOT NULL DEFAULT 0,
|
|
generated_tokens INTEGER NOT NULL DEFAULT 0,
|
|
timestamp TIMESTAMPTZ NOT NULL DEFAULT now(),
|
|
mongo_id TEXT
|
|
);
|
|
"""
|
|
)
|
|
op.execute(
|
|
"ALTER TABLE token_usage ADD CONSTRAINT token_usage_attribution_chk "
|
|
"CHECK (user_id IS NOT NULL OR api_key IS NOT NULL) NOT VALID;"
|
|
)
|
|
|
|
op.execute(
|
|
"""
|
|
CREATE TABLE user_logs (
|
|
id BIGSERIAL PRIMARY KEY,
|
|
user_id TEXT,
|
|
endpoint TEXT,
|
|
timestamp TIMESTAMPTZ NOT NULL DEFAULT now(),
|
|
data JSONB,
|
|
mongo_id TEXT
|
|
);
|
|
"""
|
|
)
|
|
|
|
op.execute(
|
|
"""
|
|
CREATE TABLE stack_logs (
|
|
id BIGSERIAL PRIMARY KEY,
|
|
activity_id TEXT NOT NULL,
|
|
endpoint TEXT,
|
|
level TEXT,
|
|
user_id TEXT,
|
|
api_key TEXT,
|
|
query TEXT,
|
|
stacks JSONB NOT NULL DEFAULT '[]'::jsonb,
|
|
timestamp TIMESTAMPTZ NOT NULL DEFAULT now(),
|
|
mongo_id TEXT
|
|
);
|
|
"""
|
|
)
|
|
|
|
op.execute(
|
|
"""
|
|
CREATE TABLE agent_folders (
|
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
user_id TEXT NOT NULL,
|
|
name TEXT NOT NULL,
|
|
description TEXT,
|
|
parent_id UUID REFERENCES agent_folders(id) ON DELETE SET NULL,
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
|
updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
|
legacy_mongo_id TEXT
|
|
);
|
|
"""
|
|
)
|
|
|
|
op.execute(
|
|
"""
|
|
CREATE TABLE sources (
|
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
user_id TEXT NOT NULL,
|
|
name TEXT NOT NULL,
|
|
language TEXT,
|
|
date TIMESTAMPTZ NOT NULL DEFAULT now(),
|
|
model TEXT,
|
|
type TEXT,
|
|
metadata JSONB NOT NULL DEFAULT '{}'::jsonb,
|
|
retriever TEXT,
|
|
sync_frequency TEXT,
|
|
tokens TEXT,
|
|
file_path TEXT,
|
|
remote_data JSONB,
|
|
directory_structure JSONB,
|
|
file_name_map JSONB,
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
|
updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
|
legacy_mongo_id TEXT
|
|
);
|
|
"""
|
|
)
|
|
|
|
op.execute(
|
|
"""
|
|
CREATE TABLE agents (
|
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
user_id TEXT NOT NULL,
|
|
name TEXT NOT NULL,
|
|
description TEXT,
|
|
agent_type TEXT,
|
|
status TEXT NOT NULL,
|
|
key CITEXT UNIQUE,
|
|
image TEXT,
|
|
source_id UUID REFERENCES sources(id) ON DELETE SET NULL,
|
|
extra_source_ids UUID[] NOT NULL DEFAULT '{}',
|
|
chunks INTEGER,
|
|
retriever TEXT,
|
|
prompt_id UUID REFERENCES prompts(id) ON DELETE SET NULL,
|
|
tools JSONB NOT NULL DEFAULT '[]'::jsonb,
|
|
json_schema JSONB,
|
|
models JSONB,
|
|
default_model_id TEXT,
|
|
folder_id UUID REFERENCES agent_folders(id) ON DELETE SET NULL,
|
|
workflow_id UUID,
|
|
limited_token_mode BOOLEAN NOT NULL DEFAULT false,
|
|
token_limit INTEGER,
|
|
limited_request_mode BOOLEAN NOT NULL DEFAULT false,
|
|
request_limit INTEGER,
|
|
allow_system_prompt_override BOOLEAN NOT NULL DEFAULT false,
|
|
shared BOOLEAN NOT NULL DEFAULT false,
|
|
shared_token CITEXT UNIQUE,
|
|
shared_metadata JSONB,
|
|
incoming_webhook_token CITEXT UNIQUE,
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
|
updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
|
last_used_at TIMESTAMPTZ,
|
|
legacy_mongo_id TEXT
|
|
);
|
|
"""
|
|
)
|
|
op.execute(
|
|
"ALTER TABLE token_usage ADD CONSTRAINT token_usage_agent_fk "
|
|
"FOREIGN KEY (agent_id) REFERENCES agents(id) ON DELETE SET NULL;"
|
|
)
|
|
|
|
op.execute(
|
|
"""
|
|
CREATE TABLE attachments (
|
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
user_id TEXT NOT NULL,
|
|
filename TEXT NOT NULL,
|
|
upload_path TEXT NOT NULL,
|
|
mime_type TEXT,
|
|
size BIGINT,
|
|
content TEXT,
|
|
token_count INTEGER,
|
|
openai_file_id TEXT,
|
|
google_file_uri TEXT,
|
|
metadata JSONB,
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
|
legacy_mongo_id TEXT
|
|
);
|
|
"""
|
|
)
|
|
|
|
op.execute(
|
|
"""
|
|
CREATE TABLE memories (
|
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
user_id TEXT NOT NULL,
|
|
tool_id UUID REFERENCES user_tools(id) ON DELETE CASCADE,
|
|
path TEXT NOT NULL,
|
|
content TEXT NOT NULL,
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
|
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
|
);
|
|
"""
|
|
)
|
|
|
|
op.execute(
|
|
"""
|
|
CREATE TABLE todos (
|
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
user_id TEXT NOT NULL,
|
|
tool_id UUID REFERENCES user_tools(id) ON DELETE CASCADE,
|
|
todo_id INTEGER,
|
|
title TEXT NOT NULL,
|
|
completed BOOLEAN NOT NULL DEFAULT false,
|
|
metadata JSONB NOT NULL DEFAULT '{}'::jsonb,
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
|
updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
|
legacy_mongo_id TEXT
|
|
);
|
|
"""
|
|
)
|
|
|
|
op.execute(
|
|
"""
|
|
CREATE TABLE notes (
|
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
user_id TEXT NOT NULL,
|
|
tool_id UUID REFERENCES user_tools(id) ON DELETE CASCADE,
|
|
title TEXT NOT NULL,
|
|
content TEXT NOT NULL,
|
|
metadata JSONB NOT NULL DEFAULT '{}'::jsonb,
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
|
updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
|
legacy_mongo_id TEXT
|
|
);
|
|
"""
|
|
)
|
|
|
|
op.execute(
|
|
"""
|
|
CREATE TABLE connector_sessions (
|
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
user_id TEXT NOT NULL,
|
|
provider TEXT NOT NULL,
|
|
server_url TEXT,
|
|
session_token TEXT UNIQUE,
|
|
user_email TEXT,
|
|
status TEXT,
|
|
token_info JSONB,
|
|
session_data JSONB NOT NULL DEFAULT '{}'::jsonb,
|
|
expires_at TIMESTAMPTZ,
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
|
legacy_mongo_id TEXT
|
|
);
|
|
"""
|
|
)
|
|
|
|
op.execute(
|
|
"""
|
|
CREATE TABLE conversations (
|
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
user_id TEXT NOT NULL,
|
|
agent_id UUID REFERENCES agents(id) ON DELETE SET NULL,
|
|
name TEXT,
|
|
api_key TEXT,
|
|
is_shared_usage BOOLEAN NOT NULL DEFAULT false,
|
|
shared_token TEXT,
|
|
date TIMESTAMPTZ NOT NULL DEFAULT now(),
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
|
updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
|
shared_with TEXT[] NOT NULL DEFAULT '{}'::text[],
|
|
compression_metadata JSONB,
|
|
legacy_mongo_id TEXT,
|
|
CONSTRAINT conversations_api_key_nonempty_chk
|
|
CHECK (api_key IS NULL OR api_key <> '')
|
|
);
|
|
"""
|
|
)
|
|
|
|
op.execute(
|
|
"""
|
|
CREATE TABLE conversation_messages (
|
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
conversation_id UUID NOT NULL REFERENCES conversations(id) ON DELETE CASCADE,
|
|
position INTEGER NOT NULL,
|
|
prompt TEXT,
|
|
response TEXT,
|
|
thought TEXT,
|
|
sources JSONB NOT NULL DEFAULT '[]'::jsonb,
|
|
tool_calls JSONB NOT NULL DEFAULT '[]'::jsonb,
|
|
attachments UUID[] NOT NULL DEFAULT '{}'::uuid[],
|
|
model_id TEXT,
|
|
message_metadata JSONB NOT NULL DEFAULT '{}'::jsonb,
|
|
feedback JSONB,
|
|
timestamp TIMESTAMPTZ NOT NULL DEFAULT now(),
|
|
user_id TEXT NOT NULL,
|
|
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
|
);
|
|
"""
|
|
)
|
|
|
|
op.execute(
|
|
"""
|
|
CREATE TABLE shared_conversations (
|
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
conversation_id UUID NOT NULL REFERENCES conversations(id) ON DELETE CASCADE,
|
|
user_id TEXT NOT NULL,
|
|
is_promptable BOOLEAN NOT NULL DEFAULT false,
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
|
uuid UUID NOT NULL,
|
|
first_n_queries INTEGER NOT NULL DEFAULT 0,
|
|
api_key TEXT,
|
|
prompt_id UUID REFERENCES prompts(id) ON DELETE SET NULL,
|
|
chunks INTEGER,
|
|
CONSTRAINT shared_conversations_api_key_nonempty_chk
|
|
CHECK (api_key IS NULL OR api_key <> '')
|
|
);
|
|
"""
|
|
)
|
|
|
|
op.execute(
|
|
"""
|
|
CREATE TABLE pending_tool_state (
|
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
conversation_id UUID NOT NULL REFERENCES conversations(id) ON DELETE CASCADE,
|
|
user_id TEXT NOT NULL,
|
|
messages JSONB NOT NULL,
|
|
pending_tool_calls JSONB NOT NULL,
|
|
tools_dict JSONB NOT NULL,
|
|
tool_schemas JSONB NOT NULL,
|
|
agent_config JSONB NOT NULL,
|
|
client_tools JSONB,
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
|
expires_at TIMESTAMPTZ NOT NULL
|
|
);
|
|
"""
|
|
)
|
|
|
|
op.execute(
|
|
"""
|
|
CREATE TABLE workflows (
|
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
user_id TEXT NOT NULL,
|
|
name TEXT NOT NULL,
|
|
description TEXT,
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
|
updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
|
current_graph_version INTEGER NOT NULL DEFAULT 1,
|
|
legacy_mongo_id TEXT
|
|
);
|
|
"""
|
|
)
|
|
# Backfill the agents.workflow_id FK now that workflows exists.
|
|
# The column was created without a FK (forward reference to a table
|
|
# that hadn't been declared yet); add the constraint here so workflow
|
|
# deletion still cascades through to agent unset.
|
|
op.execute(
|
|
"ALTER TABLE agents ADD CONSTRAINT agents_workflow_fk "
|
|
"FOREIGN KEY (workflow_id) REFERENCES workflows(id) ON DELETE SET NULL;"
|
|
)
|
|
|
|
op.execute(
|
|
"""
|
|
CREATE TABLE workflow_nodes (
|
|
id UUID DEFAULT gen_random_uuid() NOT NULL,
|
|
workflow_id UUID NOT NULL REFERENCES workflows(id) ON DELETE CASCADE,
|
|
graph_version INTEGER NOT NULL,
|
|
node_type TEXT NOT NULL,
|
|
config JSONB NOT NULL DEFAULT '{}'::jsonb,
|
|
node_id TEXT NOT NULL,
|
|
title TEXT,
|
|
description TEXT,
|
|
position JSONB NOT NULL DEFAULT '{"x": 0, "y": 0}'::jsonb,
|
|
legacy_mongo_id TEXT,
|
|
PRIMARY KEY (id),
|
|
CONSTRAINT workflow_nodes_id_wf_ver_key
|
|
UNIQUE (id, workflow_id, graph_version)
|
|
);
|
|
"""
|
|
)
|
|
|
|
op.execute(
|
|
"""
|
|
CREATE TABLE workflow_edges (
|
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
workflow_id UUID NOT NULL REFERENCES workflows(id) ON DELETE CASCADE,
|
|
graph_version INTEGER NOT NULL,
|
|
from_node_id UUID NOT NULL,
|
|
to_node_id UUID NOT NULL,
|
|
config JSONB NOT NULL DEFAULT '{}'::jsonb,
|
|
edge_id TEXT NOT NULL,
|
|
source_handle TEXT,
|
|
target_handle TEXT,
|
|
CONSTRAINT workflow_edges_from_node_fk
|
|
FOREIGN KEY (from_node_id, workflow_id, graph_version)
|
|
REFERENCES workflow_nodes(id, workflow_id, graph_version) ON DELETE CASCADE,
|
|
CONSTRAINT workflow_edges_to_node_fk
|
|
FOREIGN KEY (to_node_id, workflow_id, graph_version)
|
|
REFERENCES workflow_nodes(id, workflow_id, graph_version) ON DELETE CASCADE
|
|
);
|
|
"""
|
|
)
|
|
|
|
op.execute(
|
|
"""
|
|
CREATE TABLE workflow_runs (
|
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
workflow_id UUID NOT NULL REFERENCES workflows(id) ON DELETE CASCADE,
|
|
user_id TEXT NOT NULL,
|
|
status TEXT NOT NULL,
|
|
started_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
|
ended_at TIMESTAMPTZ,
|
|
result JSONB,
|
|
inputs JSONB,
|
|
steps JSONB NOT NULL DEFAULT '[]'::jsonb,
|
|
legacy_mongo_id TEXT,
|
|
CONSTRAINT workflow_runs_status_chk
|
|
CHECK (status IN ('pending', 'running', 'completed', 'failed'))
|
|
);
|
|
"""
|
|
)
|
|
|
|
# ------------------------------------------------------------------
|
|
# Indexes
|
|
# ------------------------------------------------------------------
|
|
op.execute("CREATE INDEX agent_folders_user_idx ON agent_folders (user_id);")
|
|
|
|
op.execute("CREATE INDEX agents_user_idx ON agents (user_id);")
|
|
op.execute("CREATE INDEX agents_shared_idx ON agents (shared) WHERE shared = true;")
|
|
op.execute("CREATE INDEX agents_status_idx ON agents (status);")
|
|
op.execute("CREATE INDEX agents_source_id_idx ON agents (source_id);")
|
|
op.execute("CREATE INDEX agents_prompt_id_idx ON agents (prompt_id);")
|
|
op.execute("CREATE INDEX agents_folder_id_idx ON agents (folder_id);")
|
|
op.execute(
|
|
"CREATE UNIQUE INDEX agents_legacy_mongo_id_uidx "
|
|
"ON agents (legacy_mongo_id) WHERE legacy_mongo_id IS NOT NULL;"
|
|
)
|
|
|
|
op.execute("CREATE INDEX attachments_user_idx ON attachments (user_id);")
|
|
op.execute(
|
|
"CREATE UNIQUE INDEX attachments_legacy_mongo_id_uidx "
|
|
"ON attachments (legacy_mongo_id) WHERE legacy_mongo_id IS NOT NULL;"
|
|
)
|
|
|
|
op.execute(
|
|
# MCP and OAuth connectors share the ``provider`` slot, so the
|
|
# dedup key is ``(user_id, server_url, provider)``: MCP rows
|
|
# differentiate by server_url (one per MCP server), OAuth rows
|
|
# have server_url = NULL and differentiate by provider alone.
|
|
# COALESCE lets NULL server_url participate in the constraint.
|
|
"CREATE UNIQUE INDEX connector_sessions_user_endpoint_uidx "
|
|
"ON connector_sessions (user_id, COALESCE(server_url, ''), provider);"
|
|
)
|
|
op.execute(
|
|
"CREATE INDEX connector_sessions_expiry_idx "
|
|
"ON connector_sessions (expires_at) WHERE expires_at IS NOT NULL;"
|
|
)
|
|
op.execute(
|
|
"CREATE INDEX connector_sessions_server_url_idx "
|
|
"ON connector_sessions (server_url) WHERE server_url IS NOT NULL;"
|
|
)
|
|
op.execute(
|
|
"CREATE UNIQUE INDEX connector_sessions_legacy_mongo_id_uidx "
|
|
"ON connector_sessions (legacy_mongo_id) WHERE legacy_mongo_id IS NOT NULL;"
|
|
)
|
|
|
|
op.execute(
|
|
"CREATE UNIQUE INDEX conversation_messages_conv_pos_uidx "
|
|
"ON conversation_messages (conversation_id, position);"
|
|
)
|
|
op.execute(
|
|
"CREATE INDEX conversation_messages_user_ts_idx "
|
|
"ON conversation_messages (user_id, timestamp DESC);"
|
|
)
|
|
|
|
op.execute("CREATE INDEX conversations_user_date_idx ON conversations (user_id, date DESC);")
|
|
op.execute("CREATE INDEX conversations_agent_idx ON conversations (agent_id);")
|
|
op.execute(
|
|
"CREATE UNIQUE INDEX conversations_shared_token_uidx "
|
|
"ON conversations (shared_token) WHERE shared_token IS NOT NULL;"
|
|
)
|
|
op.execute(
|
|
"CREATE INDEX conversations_api_key_date_idx "
|
|
"ON conversations (api_key, date DESC) WHERE api_key IS NOT NULL;"
|
|
)
|
|
op.execute(
|
|
"CREATE UNIQUE INDEX conversations_legacy_mongo_id_uidx "
|
|
"ON conversations (legacy_mongo_id) WHERE legacy_mongo_id IS NOT NULL;"
|
|
)
|
|
|
|
op.execute(
|
|
"CREATE UNIQUE INDEX memories_user_tool_path_uidx "
|
|
"ON memories (user_id, tool_id, path);"
|
|
)
|
|
op.execute(
|
|
"CREATE UNIQUE INDEX memories_user_path_null_tool_uidx "
|
|
"ON memories (user_id, path) WHERE tool_id IS NULL;"
|
|
)
|
|
op.execute(
|
|
"CREATE INDEX memories_path_prefix_idx "
|
|
"ON memories (user_id, tool_id, path text_pattern_ops);"
|
|
)
|
|
op.execute("CREATE INDEX memories_tool_id_idx ON memories (tool_id);")
|
|
|
|
op.execute("CREATE UNIQUE INDEX notes_user_tool_uidx ON notes (user_id, tool_id);")
|
|
op.execute("CREATE INDEX notes_tool_id_idx ON notes (tool_id);")
|
|
op.execute(
|
|
"CREATE UNIQUE INDEX notes_legacy_mongo_id_uidx "
|
|
"ON notes (legacy_mongo_id) WHERE legacy_mongo_id IS NOT NULL;"
|
|
)
|
|
|
|
op.execute(
|
|
"CREATE UNIQUE INDEX pending_tool_state_conv_user_uidx "
|
|
"ON pending_tool_state (conversation_id, user_id);"
|
|
)
|
|
op.execute(
|
|
"CREATE INDEX pending_tool_state_expires_idx ON pending_tool_state (expires_at);"
|
|
)
|
|
|
|
op.execute("CREATE INDEX prompts_user_id_idx ON prompts (user_id);")
|
|
op.execute(
|
|
"CREATE UNIQUE INDEX prompts_legacy_mongo_id_uidx "
|
|
"ON prompts (legacy_mongo_id) WHERE legacy_mongo_id IS NOT NULL;"
|
|
)
|
|
|
|
op.execute("CREATE INDEX shared_conversations_user_idx ON shared_conversations (user_id);")
|
|
op.execute("CREATE INDEX shared_conversations_conv_idx ON shared_conversations (conversation_id);")
|
|
op.execute(
|
|
"CREATE INDEX shared_conversations_prompt_id_idx ON shared_conversations (prompt_id);"
|
|
)
|
|
op.execute(
|
|
"CREATE UNIQUE INDEX shared_conversations_uuid_uidx ON shared_conversations (uuid);"
|
|
)
|
|
op.execute(
|
|
"CREATE UNIQUE INDEX shared_conversations_dedup_uidx "
|
|
"ON shared_conversations (conversation_id, user_id, is_promptable, first_n_queries, COALESCE(api_key, ''));"
|
|
)
|
|
|
|
op.execute("CREATE INDEX sources_user_idx ON sources (user_id);")
|
|
op.execute(
|
|
"CREATE UNIQUE INDEX sources_legacy_mongo_id_uidx "
|
|
"ON sources (legacy_mongo_id) WHERE legacy_mongo_id IS NOT NULL;"
|
|
)
|
|
op.execute(
|
|
"CREATE UNIQUE INDEX user_tools_legacy_mongo_id_uidx "
|
|
"ON user_tools (legacy_mongo_id) WHERE legacy_mongo_id IS NOT NULL;"
|
|
)
|
|
op.execute(
|
|
"CREATE UNIQUE INDEX agent_folders_legacy_mongo_id_uidx "
|
|
"ON agent_folders (legacy_mongo_id) WHERE legacy_mongo_id IS NOT NULL;"
|
|
)
|
|
op.execute("CREATE INDEX agent_folders_parent_idx ON agent_folders (parent_id);")
|
|
op.execute("CREATE INDEX agents_workflow_idx ON agents (workflow_id);")
|
|
|
|
op.execute('CREATE INDEX stack_logs_timestamp_idx ON stack_logs ("timestamp" DESC);')
|
|
op.execute('CREATE INDEX stack_logs_user_ts_idx ON stack_logs (user_id, "timestamp" DESC);')
|
|
op.execute('CREATE INDEX stack_logs_level_ts_idx ON stack_logs (level, "timestamp" DESC);')
|
|
op.execute("CREATE INDEX stack_logs_activity_idx ON stack_logs (activity_id);")
|
|
op.execute(
|
|
"CREATE UNIQUE INDEX stack_logs_mongo_id_uidx "
|
|
"ON stack_logs (mongo_id) WHERE mongo_id IS NOT NULL;"
|
|
)
|
|
|
|
op.execute("CREATE INDEX todos_user_tool_idx ON todos (user_id, tool_id);")
|
|
op.execute("CREATE INDEX todos_tool_id_idx ON todos (tool_id);")
|
|
op.execute(
|
|
"CREATE UNIQUE INDEX todos_legacy_mongo_id_uidx "
|
|
"ON todos (legacy_mongo_id) WHERE legacy_mongo_id IS NOT NULL;"
|
|
)
|
|
op.execute(
|
|
"CREATE UNIQUE INDEX todos_tool_todo_id_uidx "
|
|
"ON todos (tool_id, todo_id) WHERE todo_id IS NOT NULL;"
|
|
)
|
|
|
|
op.execute('CREATE INDEX token_usage_user_ts_idx ON token_usage (user_id, "timestamp" DESC);')
|
|
op.execute('CREATE INDEX token_usage_key_ts_idx ON token_usage (api_key, "timestamp" DESC);')
|
|
op.execute('CREATE INDEX token_usage_agent_ts_idx ON token_usage (agent_id, "timestamp" DESC);')
|
|
op.execute(
|
|
"CREATE UNIQUE INDEX token_usage_mongo_id_uidx "
|
|
"ON token_usage (mongo_id) WHERE mongo_id IS NOT NULL;"
|
|
)
|
|
|
|
op.execute('CREATE INDEX user_logs_user_ts_idx ON user_logs (user_id, "timestamp" DESC);')
|
|
op.execute(
|
|
"CREATE UNIQUE INDEX user_logs_mongo_id_uidx "
|
|
"ON user_logs (mongo_id) WHERE mongo_id IS NOT NULL;"
|
|
)
|
|
|
|
op.execute("CREATE INDEX user_tools_user_id_idx ON user_tools (user_id);")
|
|
|
|
op.execute("CREATE INDEX workflow_edges_from_node_idx ON workflow_edges (from_node_id);")
|
|
op.execute("CREATE INDEX workflow_edges_to_node_idx ON workflow_edges (to_node_id);")
|
|
op.execute(
|
|
"CREATE UNIQUE INDEX workflow_edges_wf_ver_eid_uidx "
|
|
"ON workflow_edges (workflow_id, graph_version, edge_id);"
|
|
)
|
|
|
|
op.execute(
|
|
"CREATE UNIQUE INDEX workflow_nodes_wf_ver_nid_uidx "
|
|
"ON workflow_nodes (workflow_id, graph_version, node_id);"
|
|
)
|
|
op.execute(
|
|
"CREATE UNIQUE INDEX workflow_nodes_legacy_mongo_id_uidx "
|
|
"ON workflow_nodes (legacy_mongo_id) WHERE legacy_mongo_id IS NOT NULL;"
|
|
)
|
|
|
|
op.execute("CREATE INDEX workflow_runs_workflow_idx ON workflow_runs (workflow_id);")
|
|
op.execute("CREATE INDEX workflow_runs_user_idx ON workflow_runs (user_id);")
|
|
op.execute(
|
|
"CREATE INDEX workflow_runs_status_started_idx "
|
|
"ON workflow_runs (status, started_at DESC);"
|
|
)
|
|
op.execute(
|
|
"CREATE UNIQUE INDEX workflow_runs_legacy_mongo_id_uidx "
|
|
"ON workflow_runs (legacy_mongo_id) WHERE legacy_mongo_id IS NOT NULL;"
|
|
)
|
|
|
|
op.execute("CREATE INDEX workflows_user_idx ON workflows (user_id);")
|
|
op.execute(
|
|
"CREATE UNIQUE INDEX workflows_legacy_mongo_id_uidx "
|
|
"ON workflows (legacy_mongo_id) WHERE legacy_mongo_id IS NOT NULL;"
|
|
)
|
|
|
|
# ------------------------------------------------------------------
|
|
# user_id foreign keys (deferrable so backfills can stage rows)
|
|
# ------------------------------------------------------------------
|
|
user_fk_tables = (
|
|
"agent_folders",
|
|
"agents",
|
|
"attachments",
|
|
"connector_sessions",
|
|
"conversation_messages",
|
|
"conversations",
|
|
"memories",
|
|
"notes",
|
|
"pending_tool_state",
|
|
"prompts",
|
|
"shared_conversations",
|
|
"sources",
|
|
"stack_logs",
|
|
"todos",
|
|
"token_usage",
|
|
"user_logs",
|
|
"user_tools",
|
|
"workflow_runs",
|
|
"workflows",
|
|
)
|
|
for table in user_fk_tables:
|
|
op.execute(
|
|
f"ALTER TABLE {table} "
|
|
f"ADD CONSTRAINT {table}_user_id_fk "
|
|
f"FOREIGN KEY (user_id) REFERENCES users(user_id) "
|
|
f"ON DELETE RESTRICT DEFERRABLE INITIALLY IMMEDIATE;"
|
|
)
|
|
|
|
# ------------------------------------------------------------------
|
|
# Triggers
|
|
# ------------------------------------------------------------------
|
|
updated_at_tables = (
|
|
"agent_folders",
|
|
"agents",
|
|
"conversation_messages",
|
|
"conversations",
|
|
"memories",
|
|
"notes",
|
|
"prompts",
|
|
"sources",
|
|
"todos",
|
|
"user_tools",
|
|
"users",
|
|
"workflows",
|
|
)
|
|
for table in updated_at_tables:
|
|
op.execute(
|
|
f"CREATE TRIGGER {table}_set_updated_at "
|
|
f"BEFORE UPDATE ON {table} "
|
|
f"FOR EACH ROW WHEN (OLD.* IS DISTINCT FROM NEW.*) "
|
|
f"EXECUTE FUNCTION set_updated_at();"
|
|
)
|
|
|
|
ensure_user_tables = (
|
|
"agent_folders",
|
|
"agents",
|
|
"attachments",
|
|
"connector_sessions",
|
|
"conversation_messages",
|
|
"conversations",
|
|
"memories",
|
|
"notes",
|
|
"pending_tool_state",
|
|
"prompts",
|
|
"shared_conversations",
|
|
"sources",
|
|
"stack_logs",
|
|
"todos",
|
|
"token_usage",
|
|
"user_logs",
|
|
"user_tools",
|
|
"workflow_runs",
|
|
"workflows",
|
|
)
|
|
for table in ensure_user_tables:
|
|
op.execute(
|
|
f"CREATE TRIGGER {table}_ensure_user "
|
|
f"BEFORE INSERT OR UPDATE OF user_id ON {table} "
|
|
f"FOR EACH ROW EXECUTE FUNCTION ensure_user_exists();"
|
|
)
|
|
|
|
op.execute(
|
|
"CREATE TRIGGER conversation_messages_fill_user "
|
|
"BEFORE INSERT ON conversation_messages "
|
|
"FOR EACH ROW EXECUTE FUNCTION conversation_messages_fill_user_id();"
|
|
)
|
|
|
|
op.execute(
|
|
"CREATE TRIGGER attachments_cleanup_message_refs "
|
|
"AFTER DELETE ON attachments "
|
|
"FOR EACH ROW EXECUTE FUNCTION cleanup_message_attachment_refs();"
|
|
)
|
|
op.execute(
|
|
"CREATE TRIGGER agents_cleanup_user_prefs "
|
|
"AFTER DELETE ON agents "
|
|
"FOR EACH ROW EXECUTE FUNCTION cleanup_user_agent_prefs();"
|
|
)
|
|
op.execute(
|
|
"CREATE TRIGGER sources_cleanup_agent_extra_refs "
|
|
"AFTER DELETE ON sources "
|
|
"FOR EACH ROW EXECUTE FUNCTION cleanup_agent_extra_source_refs();"
|
|
)
|
|
|
|
# ------------------------------------------------------------------
|
|
# Seed sentinel __system__ user (system/template sources attribute here)
|
|
# ------------------------------------------------------------------
|
|
op.execute(
|
|
"INSERT INTO users (user_id) VALUES ('__system__') "
|
|
"ON CONFLICT (user_id) DO NOTHING;"
|
|
)
|
|
|
|
|
|
def downgrade() -> None:
|
|
# Nuclear downgrade: drop everything this migration created. The
|
|
# ordering drops FK-bearing children before parents; CASCADE would
|
|
# also work but explicit ordering is easier to reason about in code
|
|
# review.
|
|
tables_in_drop_order = (
|
|
"workflow_edges",
|
|
"workflow_runs",
|
|
"workflow_nodes",
|
|
"workflows",
|
|
"pending_tool_state",
|
|
"shared_conversations",
|
|
"conversation_messages",
|
|
"conversations",
|
|
"connector_sessions",
|
|
"notes",
|
|
"todos",
|
|
"memories",
|
|
"attachments",
|
|
"agents",
|
|
"sources",
|
|
"agent_folders",
|
|
"stack_logs",
|
|
"user_logs",
|
|
"token_usage",
|
|
"user_tools",
|
|
"prompts",
|
|
"users",
|
|
)
|
|
for table in tables_in_drop_order:
|
|
op.execute(f"DROP TABLE IF EXISTS {table} CASCADE;")
|
|
|
|
for fn in (
|
|
"conversation_messages_fill_user_id",
|
|
"cleanup_user_agent_prefs",
|
|
"cleanup_agent_extra_source_refs",
|
|
"cleanup_message_attachment_refs",
|
|
"ensure_user_exists",
|
|
"set_updated_at",
|
|
):
|
|
op.execute(f"DROP FUNCTION IF EXISTS {fn}();")
|