-- 014_notes.sql — Notes table with full-text search -- -- Notes are user-scoped persistent documents. The LLM can create, search, -- and update them via built-in tools (note_create, note_search, etc.). -- Full-text search uses PostgreSQL tsvector — zero additional infrastructure. -- -- DROP first: if a previous deployment created an incomplete version of -- this table (e.g. missing search_vector), IF NOT EXISTS would skip and -- the indexes/trigger would fail. Safe because 014 was never recorded -- in schema_migrations — any existing data is from a failed attempt. DROP TABLE IF EXISTS notes CASCADE; CREATE TABLE notes ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE, title VARCHAR(500) NOT NULL, content TEXT DEFAULT '', folder_path TEXT DEFAULT '/', tags TEXT[] DEFAULT '{}', metadata JSONB DEFAULT '{}', source_channel_id UUID REFERENCES channels(id) ON DELETE SET NULL, search_vector TSVECTOR, created_at TIMESTAMP DEFAULT NOW(), updated_at TIMESTAMP DEFAULT NOW() ); CREATE INDEX idx_notes_user ON notes(user_id); CREATE INDEX idx_notes_search ON notes USING GIN(search_vector); CREATE INDEX idx_notes_folder ON notes(user_id, folder_path); CREATE INDEX idx_notes_tags ON notes USING GIN(tags); CREATE INDEX idx_notes_updated ON notes(user_id, updated_at DESC); -- Auto-update search vector from title + content CREATE OR REPLACE FUNCTION notes_search_update_fn() RETURNS TRIGGER AS $$ BEGIN NEW.search_vector := setweight(to_tsvector('pg_catalog.english', COALESCE(NEW.title, '')), 'A') || setweight(to_tsvector('pg_catalog.english', COALESCE(NEW.content, '')), 'B'); NEW.updated_at := NOW(); RETURN NEW; END; $$ LANGUAGE plpgsql; -- Drop first to allow re-run DROP TRIGGER IF EXISTS notes_search_update ON notes; CREATE TRIGGER notes_search_update BEFORE INSERT OR UPDATE OF title, content ON notes FOR EACH ROW EXECUTE FUNCTION notes_search_update_fn();