2026-05-15 07:04:11 -07:00
|
|
|
-- Initial migration for shared-memory.
|
|
|
|
|
-- Sets up extensions, enum types, tables, generated columns, and indexes
|
|
|
|
|
-- required for memory storage + hybrid search (Phase 2 populates the
|
|
|
|
|
-- embedding column; FTS works in Phase 1).
|
|
|
|
|
|
|
|
|
|
-- =============================================================================
|
|
|
|
|
-- Extensions
|
|
|
|
|
-- =============================================================================
|
|
|
|
|
|
|
|
|
|
CREATE EXTENSION IF NOT EXISTS "pgcrypto"; -- gen_random_uuid()
|
|
|
|
|
CREATE EXTENSION IF NOT EXISTS "vector"; -- pgvector
|
|
|
|
|
CREATE EXTENSION IF NOT EXISTS "pg_trgm"; -- trigram index for tag fuzzy match
|
|
|
|
|
|
|
|
|
|
-- =============================================================================
|
|
|
|
|
-- Enums
|
|
|
|
|
-- =============================================================================
|
|
|
|
|
|
|
|
|
|
CREATE TYPE "memory_scope" AS ENUM ('project', 'user');
|
|
|
|
|
CREATE TYPE "memory_visibility" AS ENUM ('private', 'shared', 'team');
|
|
|
|
|
CREATE TYPE "audit_actor" AS ENUM ('mcp', 'web', 'system');
|
|
|
|
|
|
|
|
|
|
-- =============================================================================
|
|
|
|
|
-- users
|
|
|
|
|
-- =============================================================================
|
|
|
|
|
|
|
|
|
|
CREATE TABLE "users" (
|
|
|
|
|
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
|
|
|
"oidc_sub" text NOT NULL,
|
|
|
|
|
"oidc_iss" text NOT NULL,
|
|
|
|
|
"email" text,
|
|
|
|
|
"name" text,
|
|
|
|
|
"picture" text,
|
|
|
|
|
"created_at" timestamptz NOT NULL DEFAULT now(),
|
|
|
|
|
"last_seen_at" timestamptz NOT NULL DEFAULT now()
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
CREATE UNIQUE INDEX "users_iss_sub_uq" ON "users" ("oidc_iss", "oidc_sub");
|
|
|
|
|
|
|
|
|
|
-- =============================================================================
|
|
|
|
|
-- projects
|
|
|
|
|
-- =============================================================================
|
|
|
|
|
|
|
|
|
|
CREATE TABLE "projects" (
|
|
|
|
|
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
|
|
|
"user_id" uuid NOT NULL REFERENCES "users"("id") ON DELETE CASCADE,
|
|
|
|
|
"key" varchar(200) NOT NULL,
|
|
|
|
|
"display_name" text,
|
|
|
|
|
"created_at" timestamptz NOT NULL DEFAULT now(),
|
|
|
|
|
"updated_at" timestamptz NOT NULL DEFAULT now()
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
CREATE UNIQUE INDEX "projects_user_key_uq" ON "projects" ("user_id", "key");
|
|
|
|
|
|
|
|
|
|
-- =============================================================================
|
|
|
|
|
-- memories
|
|
|
|
|
-- =============================================================================
|
|
|
|
|
|
|
|
|
|
CREATE TABLE "memories" (
|
|
|
|
|
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
|
|
|
"user_id" uuid NOT NULL REFERENCES "users"("id") ON DELETE CASCADE,
|
|
|
|
|
"project_id" uuid REFERENCES "projects"("id") ON DELETE SET NULL,
|
|
|
|
|
"scope" memory_scope NOT NULL DEFAULT 'project',
|
|
|
|
|
"visibility" memory_visibility NOT NULL DEFAULT 'private',
|
|
|
|
|
"content" text NOT NULL,
|
|
|
|
|
"tags" text[] NOT NULL DEFAULT ARRAY[]::text[],
|
|
|
|
|
"embedding" vector(384),
|
|
|
|
|
"content_tsv" tsvector GENERATED ALWAYS AS (to_tsvector('english', coalesce("content", ''))) STORED,
|
|
|
|
|
"created_at" timestamptz NOT NULL DEFAULT now(),
|
|
|
|
|
"updated_at" timestamptz NOT NULL DEFAULT now(),
|
|
|
|
|
"deleted_at" timestamptz,
|
|
|
|
|
-- Scope/project_id consistency: project scope requires a project_id,
|
|
|
|
|
-- user scope forbids one.
|
|
|
|
|
CONSTRAINT "memories_scope_project_chk"
|
|
|
|
|
CHECK (
|
|
|
|
|
(scope = 'project' AND project_id IS NOT NULL)
|
|
|
|
|
OR (scope = 'user' AND project_id IS NULL)
|
|
|
|
|
)
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
CREATE INDEX "memories_user_idx" ON "memories" ("user_id");
|
|
|
|
|
CREATE INDEX "memories_project_idx" ON "memories" ("project_id");
|
|
|
|
|
CREATE INDEX "memories_created_idx" ON "memories" ("created_at" DESC);
|
|
|
|
|
|
|
|
|
|
-- GIN index for full-text search over the generated tsvector column.
|
|
|
|
|
CREATE INDEX "memories_content_tsv_idx" ON "memories" USING GIN ("content_tsv");
|
|
|
|
|
|
2026-05-15 08:08:22 -07:00
|
|
|
-- GIN index on tags for tag-set containment queries (`tags @> ARRAY[...]`).
|
|
|
|
|
-- pg_trgm is loaded for future fuzzy text search on `content`, not tags.
|
|
|
|
|
CREATE INDEX "memories_tags_idx" ON "memories" USING GIN ("tags");
|
2026-05-15 07:04:11 -07:00
|
|
|
|
|
|
|
|
-- IVFFlat vector index. Lists=100 is a reasonable starting point; tune later
|
|
|
|
|
-- once we have real volume. Note: the index requires data to be useful — it's
|
|
|
|
|
-- created here so embeddings written in Phase 2 are indexed automatically.
|
|
|
|
|
CREATE INDEX "memories_embedding_idx" ON "memories"
|
|
|
|
|
USING ivfflat ("embedding" vector_cosine_ops) WITH (lists = 100);
|
|
|
|
|
|
|
|
|
|
-- =============================================================================
|
|
|
|
|
-- snippets
|
|
|
|
|
-- =============================================================================
|
|
|
|
|
|
|
|
|
|
CREATE TABLE "snippets" (
|
|
|
|
|
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
|
|
|
"user_id" uuid NOT NULL REFERENCES "users"("id") ON DELETE CASCADE,
|
|
|
|
|
"name" varchar(200) NOT NULL,
|
|
|
|
|
"body" text NOT NULL,
|
|
|
|
|
"description" text,
|
|
|
|
|
"tags" text[] NOT NULL DEFAULT ARRAY[]::text[],
|
|
|
|
|
"created_at" timestamptz NOT NULL DEFAULT now(),
|
|
|
|
|
"updated_at" timestamptz NOT NULL DEFAULT now()
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
CREATE UNIQUE INDEX "snippets_user_name_uq" ON "snippets" ("user_id", "name");
|
|
|
|
|
CREATE INDEX "snippets_tags_idx" ON "snippets" USING GIN ("tags");
|
|
|
|
|
|
|
|
|
|
-- =============================================================================
|
|
|
|
|
-- audit_log
|
|
|
|
|
-- =============================================================================
|
|
|
|
|
|
|
|
|
|
CREATE TABLE "audit_log" (
|
|
|
|
|
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
|
|
|
"user_id" uuid REFERENCES "users"("id") ON DELETE SET NULL,
|
|
|
|
|
"actor" audit_actor NOT NULL,
|
|
|
|
|
"action" text NOT NULL,
|
|
|
|
|
"entity_type" text,
|
|
|
|
|
"entity_id" uuid,
|
|
|
|
|
"payload" jsonb,
|
|
|
|
|
"created_at" timestamptz NOT NULL DEFAULT now()
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
CREATE INDEX "audit_user_idx" ON "audit_log" ("user_id");
|
|
|
|
|
CREATE INDEX "audit_created_idx" ON "audit_log" ("created_at" DESC);
|
|
|
|
|
|
|
|
|
|
-- =============================================================================
|
|
|
|
|
-- updated_at triggers
|
|
|
|
|
-- =============================================================================
|
|
|
|
|
|
|
|
|
|
CREATE OR REPLACE FUNCTION set_updated_at() RETURNS trigger AS $$
|
|
|
|
|
BEGIN
|
|
|
|
|
NEW.updated_at = now();
|
|
|
|
|
RETURN NEW;
|
|
|
|
|
END;
|
|
|
|
|
$$ LANGUAGE plpgsql;
|
|
|
|
|
|
|
|
|
|
CREATE TRIGGER projects_set_updated_at BEFORE UPDATE ON "projects"
|
|
|
|
|
FOR EACH ROW EXECUTE FUNCTION set_updated_at();
|
|
|
|
|
|
|
|
|
|
CREATE TRIGGER memories_set_updated_at BEFORE UPDATE ON "memories"
|
|
|
|
|
FOR EACH ROW EXECUTE FUNCTION set_updated_at();
|
|
|
|
|
|
|
|
|
|
CREATE TRIGGER snippets_set_updated_at BEFORE UPDATE ON "snippets"
|
|
|
|
|
FOR EACH ROW EXECUTE FUNCTION set_updated_at();
|