-- ============================================================================= -- Life OS - Release 1 COMPLETE Schema -- Self-hosted PostgreSQL 16 on defiant-01 (Hetzner) -- Database: lifeos_dev -- Generated from Architecture Design Document v2.0 -- ============================================================================= -- Extensions CREATE EXTENSION IF NOT EXISTS "pgcrypto"; -- ============================================================================= -- LOOKUP TABLE: Context Types -- ============================================================================= CREATE TABLE context_types ( id SERIAL PRIMARY KEY, value TEXT NOT NULL UNIQUE, label TEXT NOT NULL, description TEXT, is_system BOOLEAN NOT NULL DEFAULT true, sort_order INTEGER NOT NULL DEFAULT 0, is_deleted BOOLEAN NOT NULL DEFAULT false, created_at TIMESTAMPTZ NOT NULL DEFAULT now() ); -- ============================================================================= -- CORE HIERARCHY -- ============================================================================= CREATE TABLE domains ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), name TEXT NOT NULL, color TEXT, description TEXT, icon TEXT, sort_order INTEGER NOT NULL DEFAULT 0, is_deleted BOOLEAN NOT NULL DEFAULT false, deleted_at TIMESTAMPTZ, search_vector TSVECTOR, created_at TIMESTAMPTZ NOT NULL DEFAULT now(), updated_at TIMESTAMPTZ NOT NULL DEFAULT now() ); CREATE TABLE areas ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), domain_id UUID NOT NULL REFERENCES domains(id) ON DELETE CASCADE, name TEXT NOT NULL, description TEXT, icon TEXT, color TEXT, status TEXT NOT NULL DEFAULT 'active', sort_order INTEGER NOT NULL DEFAULT 0, is_deleted BOOLEAN NOT NULL DEFAULT false, deleted_at TIMESTAMPTZ, search_vector TSVECTOR, created_at TIMESTAMPTZ NOT NULL DEFAULT now(), updated_at TIMESTAMPTZ NOT NULL DEFAULT now() ); CREATE TABLE projects ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), domain_id UUID NOT NULL REFERENCES domains(id) ON DELETE CASCADE, area_id UUID REFERENCES areas(id) ON DELETE SET NULL, name TEXT NOT NULL, description TEXT, status TEXT NOT NULL DEFAULT 'active', priority INTEGER NOT NULL DEFAULT 3, start_date DATE, target_date DATE, completed_at TIMESTAMPTZ, color TEXT, tags TEXT[], sort_order INTEGER NOT NULL DEFAULT 0, is_deleted BOOLEAN NOT NULL DEFAULT false, deleted_at TIMESTAMPTZ, search_vector TSVECTOR, created_at TIMESTAMPTZ NOT NULL DEFAULT now(), updated_at TIMESTAMPTZ NOT NULL DEFAULT now() ); -- Forward-declare releases for tasks.release_id FK CREATE TABLE releases ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), name TEXT NOT NULL, version_label TEXT, description TEXT, status TEXT NOT NULL DEFAULT 'planned', target_date DATE, released_at DATE, release_notes TEXT, tags TEXT[], sort_order INTEGER NOT NULL DEFAULT 0, is_deleted BOOLEAN NOT NULL DEFAULT false, deleted_at TIMESTAMPTZ, search_vector TSVECTOR, created_at TIMESTAMPTZ NOT NULL DEFAULT now(), updated_at TIMESTAMPTZ NOT NULL DEFAULT now() ); -- Forward-declare contacts for tasks.waiting_for_contact_id FK CREATE TABLE contacts ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), first_name TEXT NOT NULL, last_name TEXT, company TEXT, role TEXT, email TEXT, phone TEXT, notes TEXT, tags TEXT[], sort_order INTEGER NOT NULL DEFAULT 0, is_deleted BOOLEAN NOT NULL DEFAULT false, deleted_at TIMESTAMPTZ, search_vector TSVECTOR, created_at TIMESTAMPTZ NOT NULL DEFAULT now(), updated_at TIMESTAMPTZ NOT NULL DEFAULT now() ); CREATE TABLE tasks ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), domain_id UUID REFERENCES domains(id) ON DELETE CASCADE, area_id UUID REFERENCES areas(id) ON DELETE SET NULL, project_id UUID REFERENCES projects(id) ON DELETE SET NULL, release_id UUID REFERENCES releases(id) ON DELETE SET NULL, parent_id UUID REFERENCES tasks(id) ON DELETE SET NULL, title TEXT NOT NULL, description TEXT, priority INTEGER NOT NULL DEFAULT 3, status TEXT NOT NULL DEFAULT 'open', due_date DATE, deadline TIMESTAMPTZ, recurrence TEXT, estimated_minutes INTEGER, energy_required TEXT, context TEXT, is_custom_context BOOLEAN NOT NULL DEFAULT false, waiting_for_contact_id UUID REFERENCES contacts(id) ON DELETE SET NULL, waiting_since DATE, import_batch_id UUID, tags TEXT[], sort_order INTEGER NOT NULL DEFAULT 0, is_deleted BOOLEAN NOT NULL DEFAULT false, deleted_at TIMESTAMPTZ, completed_at TIMESTAMPTZ, search_vector TSVECTOR, created_at TIMESTAMPTZ NOT NULL DEFAULT now(), updated_at TIMESTAMPTZ NOT NULL DEFAULT now() ); -- ============================================================================= -- KNOWLEDGE MANAGEMENT -- ============================================================================= CREATE TABLE note_folders ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), parent_id UUID REFERENCES note_folders(id) ON DELETE CASCADE, name TEXT NOT NULL, auto_generated BOOLEAN NOT NULL DEFAULT false, sort_order INTEGER NOT NULL DEFAULT 0, is_deleted BOOLEAN NOT NULL DEFAULT false, deleted_at TIMESTAMPTZ, created_at TIMESTAMPTZ NOT NULL DEFAULT now(), updated_at TIMESTAMPTZ NOT NULL DEFAULT now() ); -- Forward-declare meetings for notes.meeting_id FK CREATE TABLE meetings ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), parent_id UUID REFERENCES meetings(id) ON DELETE SET NULL, title TEXT NOT NULL, meeting_date DATE NOT NULL, start_at TIMESTAMPTZ, end_at TIMESTAMPTZ, location TEXT, status TEXT NOT NULL DEFAULT 'scheduled', priority INTEGER, recurrence TEXT, agenda TEXT, transcript TEXT, notes_body TEXT, tags TEXT[], sort_order INTEGER NOT NULL DEFAULT 0, is_deleted BOOLEAN NOT NULL DEFAULT false, deleted_at TIMESTAMPTZ, search_vector TSVECTOR, created_at TIMESTAMPTZ NOT NULL DEFAULT now(), updated_at TIMESTAMPTZ NOT NULL DEFAULT now() ); CREATE TABLE notes ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), domain_id UUID REFERENCES domains(id) ON DELETE CASCADE, project_id UUID REFERENCES projects(id) ON DELETE SET NULL, folder_id UUID REFERENCES note_folders(id) ON DELETE SET NULL, meeting_id UUID REFERENCES meetings(id) ON DELETE SET NULL, title TEXT NOT NULL, body TEXT, content_format TEXT NOT NULL DEFAULT 'rich', is_meeting_note BOOLEAN NOT NULL DEFAULT false, tags TEXT[], sort_order INTEGER NOT NULL DEFAULT 0, is_deleted BOOLEAN NOT NULL DEFAULT false, deleted_at TIMESTAMPTZ, search_vector TSVECTOR, created_at TIMESTAMPTZ NOT NULL DEFAULT now(), updated_at TIMESTAMPTZ NOT NULL DEFAULT now() ); CREATE TABLE decisions ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), title TEXT NOT NULL, rationale TEXT, status TEXT NOT NULL DEFAULT 'proposed', impact TEXT NOT NULL DEFAULT 'medium', decided_at DATE, meeting_id UUID REFERENCES meetings(id) ON DELETE SET NULL, superseded_by_id UUID REFERENCES decisions(id) ON DELETE SET NULL, tags TEXT[], sort_order INTEGER NOT NULL DEFAULT 0, is_deleted BOOLEAN NOT NULL DEFAULT false, deleted_at TIMESTAMPTZ, search_vector TSVECTOR, created_at TIMESTAMPTZ NOT NULL DEFAULT now(), updated_at TIMESTAMPTZ NOT NULL DEFAULT now() ); CREATE TABLE lists ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), domain_id UUID REFERENCES domains(id) ON DELETE CASCADE, area_id UUID REFERENCES areas(id) ON DELETE SET NULL, project_id UUID REFERENCES projects(id) ON DELETE SET NULL, name TEXT NOT NULL, list_type TEXT NOT NULL DEFAULT 'checklist', description TEXT, tags TEXT[], sort_order INTEGER NOT NULL DEFAULT 0, is_deleted BOOLEAN NOT NULL DEFAULT false, deleted_at TIMESTAMPTZ, search_vector TSVECTOR, created_at TIMESTAMPTZ NOT NULL DEFAULT now(), updated_at TIMESTAMPTZ NOT NULL DEFAULT now() ); CREATE TABLE list_items ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), list_id UUID NOT NULL REFERENCES lists(id) ON DELETE CASCADE, parent_item_id UUID REFERENCES list_items(id) ON DELETE SET NULL, content TEXT NOT NULL, completed BOOLEAN NOT NULL DEFAULT false, completed_at TIMESTAMPTZ, sort_order INTEGER NOT NULL DEFAULT 0, is_deleted BOOLEAN NOT NULL DEFAULT false, deleted_at TIMESTAMPTZ, created_at TIMESTAMPTZ NOT NULL DEFAULT now(), updated_at TIMESTAMPTZ NOT NULL DEFAULT now() ); CREATE TABLE links ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), domain_id UUID REFERENCES domains(id) ON DELETE CASCADE, area_id UUID REFERENCES areas(id) ON DELETE SET NULL, project_id UUID REFERENCES projects(id) ON DELETE SET NULL, label TEXT NOT NULL, url TEXT NOT NULL, description TEXT, tags TEXT[], sort_order INTEGER NOT NULL DEFAULT 0, is_deleted BOOLEAN NOT NULL DEFAULT false, deleted_at TIMESTAMPTZ, search_vector TSVECTOR, created_at TIMESTAMPTZ NOT NULL DEFAULT now(), updated_at TIMESTAMPTZ NOT NULL DEFAULT now() ); CREATE TABLE files ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), filename TEXT NOT NULL, original_filename TEXT NOT NULL, storage_path TEXT NOT NULL, mime_type TEXT, size_bytes INTEGER, description TEXT, tags TEXT[], sort_order INTEGER NOT NULL DEFAULT 0, is_deleted BOOLEAN NOT NULL DEFAULT false, deleted_at TIMESTAMPTZ, search_vector TSVECTOR, created_at TIMESTAMPTZ NOT NULL DEFAULT now(), updated_at TIMESTAMPTZ NOT NULL DEFAULT now() ); -- ============================================================================= -- SYSTEM LEVEL: Appointments -- ============================================================================= CREATE TABLE appointments ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), title TEXT NOT NULL, description TEXT, location TEXT, start_at TIMESTAMPTZ NOT NULL, end_at TIMESTAMPTZ, all_day BOOLEAN NOT NULL DEFAULT false, recurrence TEXT, tags TEXT[], sort_order INTEGER NOT NULL DEFAULT 0, is_deleted BOOLEAN NOT NULL DEFAULT false, deleted_at TIMESTAMPTZ, search_vector TSVECTOR, created_at TIMESTAMPTZ NOT NULL DEFAULT now(), updated_at TIMESTAMPTZ NOT NULL DEFAULT now() ); -- ============================================================================= -- SYSTEM LEVEL: Milestones -- ============================================================================= CREATE TABLE milestones ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), release_id UUID REFERENCES releases(id) ON DELETE SET NULL, project_id UUID REFERENCES projects(id) ON DELETE SET NULL, name TEXT NOT NULL, target_date DATE NOT NULL, completed_at DATE, sort_order INTEGER NOT NULL DEFAULT 0, is_deleted BOOLEAN NOT NULL DEFAULT false, deleted_at TIMESTAMPTZ, created_at TIMESTAMPTZ NOT NULL DEFAULT now(), updated_at TIMESTAMPTZ NOT NULL DEFAULT now() ); -- ============================================================================= -- SYSTEM LEVEL: Processes -- ============================================================================= CREATE TABLE processes ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), name TEXT NOT NULL, description TEXT, process_type TEXT NOT NULL DEFAULT 'checklist', category TEXT, status TEXT NOT NULL DEFAULT 'draft', tags TEXT[], sort_order INTEGER NOT NULL DEFAULT 0, is_deleted BOOLEAN NOT NULL DEFAULT false, deleted_at TIMESTAMPTZ, search_vector TSVECTOR, created_at TIMESTAMPTZ NOT NULL DEFAULT now(), updated_at TIMESTAMPTZ NOT NULL DEFAULT now() ); CREATE TABLE process_steps ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), process_id UUID NOT NULL REFERENCES processes(id) ON DELETE CASCADE, title TEXT NOT NULL, instructions TEXT, expected_output TEXT, estimated_days INTEGER, context TEXT, sort_order INTEGER NOT NULL DEFAULT 0, is_deleted BOOLEAN NOT NULL DEFAULT false, deleted_at TIMESTAMPTZ, created_at TIMESTAMPTZ NOT NULL DEFAULT now(), updated_at TIMESTAMPTZ NOT NULL DEFAULT now() ); CREATE TABLE process_runs ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), process_id UUID NOT NULL REFERENCES processes(id) ON DELETE CASCADE, title TEXT NOT NULL, status TEXT NOT NULL DEFAULT 'not_started', process_type TEXT NOT NULL, task_generation TEXT NOT NULL DEFAULT 'all_at_once', project_id UUID REFERENCES projects(id) ON DELETE SET NULL, contact_id UUID REFERENCES contacts(id) ON DELETE SET NULL, started_at TIMESTAMPTZ, completed_at TIMESTAMPTZ, sort_order INTEGER NOT NULL DEFAULT 0, is_deleted BOOLEAN NOT NULL DEFAULT false, deleted_at TIMESTAMPTZ, created_at TIMESTAMPTZ NOT NULL DEFAULT now(), updated_at TIMESTAMPTZ NOT NULL DEFAULT now() ); CREATE TABLE process_run_steps ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), run_id UUID NOT NULL REFERENCES process_runs(id) ON DELETE CASCADE, title TEXT NOT NULL, instructions TEXT, status TEXT NOT NULL DEFAULT 'pending', completed_by_id UUID REFERENCES contacts(id) ON DELETE SET NULL, completed_at TIMESTAMPTZ, notes TEXT, sort_order INTEGER NOT NULL DEFAULT 0, is_deleted BOOLEAN NOT NULL DEFAULT false, deleted_at TIMESTAMPTZ, created_at TIMESTAMPTZ NOT NULL DEFAULT now(), updated_at TIMESTAMPTZ NOT NULL DEFAULT now() ); -- ============================================================================= -- SYSTEM LEVEL: Daily Focus -- ============================================================================= CREATE TABLE daily_focus ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), focus_date DATE NOT NULL, task_id UUID NOT NULL REFERENCES tasks(id) ON DELETE CASCADE, slot INTEGER, completed BOOLEAN NOT NULL DEFAULT false, note TEXT, sort_order INTEGER NOT NULL DEFAULT 0, is_deleted BOOLEAN NOT NULL DEFAULT false, deleted_at TIMESTAMPTZ, created_at TIMESTAMPTZ NOT NULL DEFAULT now() ); -- ============================================================================= -- SYSTEM LEVEL: Capture Queue -- ============================================================================= CREATE TABLE capture ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), raw_text TEXT NOT NULL, processed BOOLEAN NOT NULL DEFAULT false, converted_to_type TEXT, converted_to_id UUID, area_id UUID REFERENCES areas(id) ON DELETE SET NULL, project_id UUID REFERENCES projects(id) ON DELETE SET NULL, list_id UUID REFERENCES lists(id) ON DELETE SET NULL, import_batch_id UUID, sort_order INTEGER NOT NULL DEFAULT 0, is_deleted BOOLEAN NOT NULL DEFAULT false, deleted_at TIMESTAMPTZ, created_at TIMESTAMPTZ NOT NULL DEFAULT now() ); -- ============================================================================= -- SYSTEM LEVEL: Task Templates -- ============================================================================= CREATE TABLE task_templates ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), name TEXT NOT NULL, description TEXT, priority INTEGER, estimated_minutes INTEGER, energy_required TEXT, context TEXT, tags TEXT[], sort_order INTEGER NOT NULL DEFAULT 0, is_deleted BOOLEAN NOT NULL DEFAULT false, deleted_at TIMESTAMPTZ, created_at TIMESTAMPTZ NOT NULL DEFAULT now(), updated_at TIMESTAMPTZ NOT NULL DEFAULT now() ); CREATE TABLE task_template_items ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), template_id UUID NOT NULL REFERENCES task_templates(id) ON DELETE CASCADE, title TEXT NOT NULL, sort_order INTEGER NOT NULL DEFAULT 0, is_deleted BOOLEAN NOT NULL DEFAULT false, deleted_at TIMESTAMPTZ, created_at TIMESTAMPTZ NOT NULL DEFAULT now(), updated_at TIMESTAMPTZ NOT NULL DEFAULT now() ); -- ============================================================================= -- TIME MANAGEMENT -- ============================================================================= CREATE TABLE time_entries ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), task_id UUID NOT NULL REFERENCES tasks(id) ON DELETE CASCADE, start_at TIMESTAMPTZ NOT NULL, end_at TIMESTAMPTZ, duration_minutes INTEGER, notes TEXT, is_deleted BOOLEAN NOT NULL DEFAULT false, deleted_at TIMESTAMPTZ, created_at TIMESTAMPTZ NOT NULL DEFAULT now() ); CREATE TABLE time_blocks ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), task_id UUID REFERENCES tasks(id) ON DELETE SET NULL, title TEXT NOT NULL, context TEXT, energy TEXT, start_at TIMESTAMPTZ NOT NULL, end_at TIMESTAMPTZ NOT NULL, is_deleted BOOLEAN NOT NULL DEFAULT false, deleted_at TIMESTAMPTZ, created_at TIMESTAMPTZ NOT NULL DEFAULT now(), updated_at TIMESTAMPTZ NOT NULL DEFAULT now() ); CREATE TABLE time_budgets ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), domain_id UUID NOT NULL REFERENCES domains(id) ON DELETE CASCADE, weekly_hours DECIMAL NOT NULL, effective_from DATE NOT NULL, is_deleted BOOLEAN NOT NULL DEFAULT false, deleted_at TIMESTAMPTZ, created_at TIMESTAMPTZ NOT NULL DEFAULT now() ); -- ============================================================================= -- SYSTEM LEVEL: Weblink Directory -- ============================================================================= CREATE TABLE weblink_folders ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), parent_id UUID REFERENCES weblink_folders(id) ON DELETE CASCADE, name TEXT NOT NULL, auto_generated BOOLEAN NOT NULL DEFAULT false, sort_order INTEGER NOT NULL DEFAULT 0, is_deleted BOOLEAN NOT NULL DEFAULT false, deleted_at TIMESTAMPTZ, created_at TIMESTAMPTZ NOT NULL DEFAULT now(), updated_at TIMESTAMPTZ NOT NULL DEFAULT now() ); CREATE TABLE weblinks ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), label TEXT NOT NULL, url TEXT NOT NULL, description TEXT, tags TEXT[], sort_order INTEGER NOT NULL DEFAULT 0, is_deleted BOOLEAN NOT NULL DEFAULT false, deleted_at TIMESTAMPTZ, search_vector TSVECTOR, created_at TIMESTAMPTZ NOT NULL DEFAULT now(), updated_at TIMESTAMPTZ NOT NULL DEFAULT now() ); -- ============================================================================= -- SYSTEM LEVEL: Reminders (polymorphic) -- ============================================================================= CREATE TABLE reminders ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), entity_type TEXT NOT NULL, entity_id UUID NOT NULL, remind_at TIMESTAMPTZ NOT NULL, note TEXT, delivered BOOLEAN NOT NULL DEFAULT false, is_deleted BOOLEAN NOT NULL DEFAULT false, deleted_at TIMESTAMPTZ, created_at TIMESTAMPTZ NOT NULL DEFAULT now() ); -- ============================================================================= -- UNIVERSAL: Dependencies (polymorphic DAG) -- ============================================================================= CREATE TABLE dependencies ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), blocker_type TEXT NOT NULL, blocker_id UUID NOT NULL, dependent_type TEXT NOT NULL, dependent_id UUID NOT NULL, dependency_type TEXT NOT NULL DEFAULT 'finish_to_start', lag_days INTEGER NOT NULL DEFAULT 0, note TEXT, is_deleted BOOLEAN NOT NULL DEFAULT false, deleted_at TIMESTAMPTZ, created_at TIMESTAMPTZ NOT NULL DEFAULT now(), UNIQUE (blocker_type, blocker_id, dependent_type, dependent_id, dependency_type), CHECK (NOT (blocker_type = dependent_type AND blocker_id = dependent_id)) ); -- ============================================================================= -- JUNCTION TABLES -- ============================================================================= -- Notes <-> Projects (M2M) CREATE TABLE note_projects ( note_id UUID NOT NULL REFERENCES notes(id) ON DELETE CASCADE, project_id UUID NOT NULL REFERENCES projects(id) ON DELETE CASCADE, is_primary BOOLEAN NOT NULL DEFAULT false, created_at TIMESTAMPTZ NOT NULL DEFAULT now(), PRIMARY KEY (note_id, project_id) ); -- Notes <-> Notes (wiki graph) CREATE TABLE note_links ( source_note_id UUID NOT NULL REFERENCES notes(id) ON DELETE CASCADE, target_note_id UUID NOT NULL REFERENCES notes(id) ON DELETE CASCADE, created_at TIMESTAMPTZ NOT NULL DEFAULT now(), PRIMARY KEY (source_note_id, target_note_id) ); -- Files <-> any entity (polymorphic M2M) CREATE TABLE file_mappings ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), file_id UUID NOT NULL REFERENCES files(id) ON DELETE CASCADE, context_type TEXT NOT NULL, context_id UUID NOT NULL, created_at TIMESTAMPTZ NOT NULL DEFAULT now(), UNIQUE (file_id, context_type, context_id) ); -- Releases <-> Projects (M2M) CREATE TABLE release_projects ( release_id UUID NOT NULL REFERENCES releases(id) ON DELETE CASCADE, project_id UUID NOT NULL REFERENCES projects(id) ON DELETE CASCADE, created_at TIMESTAMPTZ NOT NULL DEFAULT now(), PRIMARY KEY (release_id, project_id) ); -- Releases <-> Domains (M2M) CREATE TABLE release_domains ( release_id UUID NOT NULL REFERENCES releases(id) ON DELETE CASCADE, domain_id UUID NOT NULL REFERENCES domains(id) ON DELETE CASCADE, created_at TIMESTAMPTZ NOT NULL DEFAULT now(), PRIMARY KEY (release_id, domain_id) ); -- Contacts <-> Tasks CREATE TABLE contact_tasks ( contact_id UUID NOT NULL REFERENCES contacts(id) ON DELETE CASCADE, task_id UUID NOT NULL REFERENCES tasks(id) ON DELETE CASCADE, role TEXT, created_at TIMESTAMPTZ NOT NULL DEFAULT now(), PRIMARY KEY (contact_id, task_id) ); -- Contacts <-> Projects CREATE TABLE contact_projects ( contact_id UUID NOT NULL REFERENCES contacts(id) ON DELETE CASCADE, project_id UUID NOT NULL REFERENCES projects(id) ON DELETE CASCADE, role TEXT, created_at TIMESTAMPTZ NOT NULL DEFAULT now(), PRIMARY KEY (contact_id, project_id) ); -- Contacts <-> Lists CREATE TABLE contact_lists ( contact_id UUID NOT NULL REFERENCES contacts(id) ON DELETE CASCADE, list_id UUID NOT NULL REFERENCES lists(id) ON DELETE CASCADE, role TEXT, created_at TIMESTAMPTZ NOT NULL DEFAULT now(), PRIMARY KEY (contact_id, list_id) ); -- Contacts <-> List Items CREATE TABLE contact_list_items ( contact_id UUID NOT NULL REFERENCES contacts(id) ON DELETE CASCADE, list_item_id UUID NOT NULL REFERENCES list_items(id) ON DELETE CASCADE, role TEXT, created_at TIMESTAMPTZ NOT NULL DEFAULT now(), PRIMARY KEY (contact_id, list_item_id) ); -- Contacts <-> Appointments CREATE TABLE contact_appointments ( contact_id UUID NOT NULL REFERENCES contacts(id) ON DELETE CASCADE, appointment_id UUID NOT NULL REFERENCES appointments(id) ON DELETE CASCADE, role TEXT, created_at TIMESTAMPTZ NOT NULL DEFAULT now(), PRIMARY KEY (contact_id, appointment_id) ); -- Contacts <-> Meetings CREATE TABLE contact_meetings ( contact_id UUID NOT NULL REFERENCES contacts(id) ON DELETE CASCADE, meeting_id UUID NOT NULL REFERENCES meetings(id) ON DELETE CASCADE, role TEXT, created_at TIMESTAMPTZ NOT NULL DEFAULT now(), PRIMARY KEY (contact_id, meeting_id) ); -- Decisions <-> Projects CREATE TABLE decision_projects ( decision_id UUID NOT NULL REFERENCES decisions(id) ON DELETE CASCADE, project_id UUID NOT NULL REFERENCES projects(id) ON DELETE CASCADE, created_at TIMESTAMPTZ NOT NULL DEFAULT now(), PRIMARY KEY (decision_id, project_id) ); -- Decisions <-> Contacts CREATE TABLE decision_contacts ( decision_id UUID NOT NULL REFERENCES decisions(id) ON DELETE CASCADE, contact_id UUID NOT NULL REFERENCES contacts(id) ON DELETE CASCADE, role TEXT, created_at TIMESTAMPTZ NOT NULL DEFAULT now(), PRIMARY KEY (decision_id, contact_id) ); -- Meetings <-> Tasks CREATE TABLE meeting_tasks ( meeting_id UUID NOT NULL REFERENCES meetings(id) ON DELETE CASCADE, task_id UUID NOT NULL REFERENCES tasks(id) ON DELETE CASCADE, source TEXT, created_at TIMESTAMPTZ NOT NULL DEFAULT now(), PRIMARY KEY (meeting_id, task_id) ); -- Process Run Steps <-> Tasks CREATE TABLE process_run_tasks ( run_step_id UUID NOT NULL REFERENCES process_run_steps(id) ON DELETE CASCADE, task_id UUID NOT NULL REFERENCES tasks(id) ON DELETE CASCADE, created_at TIMESTAMPTZ NOT NULL DEFAULT now(), PRIMARY KEY (run_step_id, task_id) ); -- Weblinks <-> Folders (M2M) CREATE TABLE folder_weblinks ( folder_id UUID NOT NULL REFERENCES weblink_folders(id) ON DELETE CASCADE, weblink_id UUID NOT NULL REFERENCES weblinks(id) ON DELETE CASCADE, sort_order INTEGER NOT NULL DEFAULT 0, created_at TIMESTAMPTZ NOT NULL DEFAULT now(), PRIMARY KEY (folder_id, weblink_id) ); -- ============================================================================= -- INDEXES -- ============================================================================= -- Sort order indexes CREATE INDEX idx_domains_sort ON domains(sort_order); CREATE INDEX idx_areas_sort ON areas(domain_id, sort_order); CREATE INDEX idx_projects_sort ON projects(domain_id, sort_order); CREATE INDEX idx_projects_area_sort ON projects(area_id, sort_order); CREATE INDEX idx_tasks_project_sort ON tasks(project_id, sort_order); CREATE INDEX idx_tasks_parent_sort ON tasks(parent_id, sort_order); CREATE INDEX idx_tasks_domain_sort ON tasks(domain_id, sort_order); CREATE INDEX idx_list_items_sort ON list_items(list_id, sort_order); CREATE INDEX idx_list_items_parent_sort ON list_items(parent_item_id, sort_order); CREATE INDEX idx_weblink_folders_sort ON weblink_folders(parent_id, sort_order); -- Lookup indexes CREATE INDEX idx_tasks_status ON tasks(status); CREATE INDEX idx_tasks_due_date ON tasks(due_date); CREATE INDEX idx_tasks_priority ON tasks(priority); CREATE INDEX idx_projects_status ON projects(status); CREATE INDEX idx_daily_focus_date ON daily_focus(focus_date); CREATE INDEX idx_appointments_start ON appointments(start_at); CREATE INDEX idx_capture_processed ON capture(processed); CREATE INDEX idx_file_mappings_context ON file_mappings(context_type, context_id); CREATE INDEX idx_dependencies_blocker ON dependencies(blocker_type, blocker_id); CREATE INDEX idx_dependencies_dependent ON dependencies(dependent_type, dependent_id); CREATE INDEX idx_reminders_entity ON reminders(entity_type, entity_id); CREATE INDEX idx_time_entries_task ON time_entries(task_id); CREATE INDEX idx_meetings_date ON meetings(meeting_date); -- Full-text search GIN indexes CREATE INDEX idx_domains_search ON domains USING GIN(search_vector); CREATE INDEX idx_areas_search ON areas USING GIN(search_vector); CREATE INDEX idx_projects_search ON projects USING GIN(search_vector); CREATE INDEX idx_tasks_search ON tasks USING GIN(search_vector); CREATE INDEX idx_notes_search ON notes USING GIN(search_vector); CREATE INDEX idx_contacts_search ON contacts USING GIN(search_vector); CREATE INDEX idx_meetings_search ON meetings USING GIN(search_vector); CREATE INDEX idx_decisions_search ON decisions USING GIN(search_vector); CREATE INDEX idx_lists_search ON lists USING GIN(search_vector); CREATE INDEX idx_links_search ON links USING GIN(search_vector); CREATE INDEX idx_files_search ON files USING GIN(search_vector); CREATE INDEX idx_weblinks_search ON weblinks USING GIN(search_vector); CREATE INDEX idx_processes_search ON processes USING GIN(search_vector); CREATE INDEX idx_appointments_search ON appointments USING GIN(search_vector); -- ============================================================================= -- SEARCH VECTOR TRIGGERS -- ============================================================================= CREATE OR REPLACE FUNCTION update_search_vector() RETURNS trigger AS $$ BEGIN NEW.search_vector := to_tsvector('pg_catalog.english', coalesce(NEW.title, '') || ' ' || coalesce(NEW.description, '') || ' ' || coalesce(NEW.name, '') || ' ' || coalesce(array_to_string(NEW.tags, ' '), '') ); RETURN NEW; EXCEPTION WHEN undefined_column THEN -- Fallback for tables with different column names RETURN NEW; END; $$ LANGUAGE plpgsql; -- Per-table triggers with correct columns CREATE OR REPLACE FUNCTION update_domains_search() RETURNS trigger AS $$ BEGIN NEW.search_vector := to_tsvector('pg_catalog.english', coalesce(NEW.name, '')); RETURN NEW; END; $$ LANGUAGE plpgsql; CREATE TRIGGER trg_domains_search BEFORE INSERT OR UPDATE ON domains FOR EACH ROW EXECUTE FUNCTION update_domains_search(); CREATE OR REPLACE FUNCTION update_areas_search() RETURNS trigger AS $$ BEGIN NEW.search_vector := to_tsvector('pg_catalog.english', coalesce(NEW.name, '') || ' ' || coalesce(NEW.description, '')); RETURN NEW; END; $$ LANGUAGE plpgsql; CREATE TRIGGER trg_areas_search BEFORE INSERT OR UPDATE ON areas FOR EACH ROW EXECUTE FUNCTION update_areas_search(); CREATE OR REPLACE FUNCTION update_projects_search() RETURNS trigger AS $$ BEGIN NEW.search_vector := to_tsvector('pg_catalog.english', coalesce(NEW.name, '') || ' ' || coalesce(NEW.description, '') || ' ' || coalesce(array_to_string(NEW.tags, ' '), '')); RETURN NEW; END; $$ LANGUAGE plpgsql; CREATE TRIGGER trg_projects_search BEFORE INSERT OR UPDATE ON projects FOR EACH ROW EXECUTE FUNCTION update_projects_search(); CREATE OR REPLACE FUNCTION update_tasks_search() RETURNS trigger AS $$ BEGIN NEW.search_vector := to_tsvector('pg_catalog.english', coalesce(NEW.title, '') || ' ' || coalesce(NEW.description, '') || ' ' || coalesce(array_to_string(NEW.tags, ' '), '')); RETURN NEW; END; $$ LANGUAGE plpgsql; CREATE TRIGGER trg_tasks_search BEFORE INSERT OR UPDATE ON tasks FOR EACH ROW EXECUTE FUNCTION update_tasks_search(); CREATE OR REPLACE FUNCTION update_notes_search() RETURNS trigger AS $$ BEGIN NEW.search_vector := to_tsvector('pg_catalog.english', coalesce(NEW.title, '') || ' ' || coalesce(NEW.body, '') || ' ' || coalesce(array_to_string(NEW.tags, ' '), '')); RETURN NEW; END; $$ LANGUAGE plpgsql; CREATE TRIGGER trg_notes_search BEFORE INSERT OR UPDATE ON notes FOR EACH ROW EXECUTE FUNCTION update_notes_search(); CREATE OR REPLACE FUNCTION update_contacts_search() RETURNS trigger AS $$ BEGIN NEW.search_vector := to_tsvector('pg_catalog.english', coalesce(NEW.first_name, '') || ' ' || coalesce(NEW.last_name, '') || ' ' || coalesce(NEW.company, '') || ' ' || coalesce(NEW.email, '') || ' ' || coalesce(array_to_string(NEW.tags, ' '), '')); RETURN NEW; END; $$ LANGUAGE plpgsql; CREATE TRIGGER trg_contacts_search BEFORE INSERT OR UPDATE ON contacts FOR EACH ROW EXECUTE FUNCTION update_contacts_search(); CREATE OR REPLACE FUNCTION update_meetings_search() RETURNS trigger AS $$ BEGIN NEW.search_vector := to_tsvector('pg_catalog.english', coalesce(NEW.title, '') || ' ' || coalesce(NEW.agenda, '') || ' ' || coalesce(NEW.notes_body, '') || ' ' || coalesce(array_to_string(NEW.tags, ' '), '')); RETURN NEW; END; $$ LANGUAGE plpgsql; CREATE TRIGGER trg_meetings_search BEFORE INSERT OR UPDATE ON meetings FOR EACH ROW EXECUTE FUNCTION update_meetings_search(); CREATE OR REPLACE FUNCTION update_decisions_search() RETURNS trigger AS $$ BEGIN NEW.search_vector := to_tsvector('pg_catalog.english', coalesce(NEW.title, '') || ' ' || coalesce(NEW.rationale, '') || ' ' || coalesce(array_to_string(NEW.tags, ' '), '')); RETURN NEW; END; $$ LANGUAGE plpgsql; CREATE TRIGGER trg_decisions_search BEFORE INSERT OR UPDATE ON decisions FOR EACH ROW EXECUTE FUNCTION update_decisions_search(); CREATE OR REPLACE FUNCTION update_lists_search() RETURNS trigger AS $$ BEGIN NEW.search_vector := to_tsvector('pg_catalog.english', coalesce(NEW.name, '') || ' ' || coalesce(NEW.description, '') || ' ' || coalesce(array_to_string(NEW.tags, ' '), '')); RETURN NEW; END; $$ LANGUAGE plpgsql; CREATE TRIGGER trg_lists_search BEFORE INSERT OR UPDATE ON lists FOR EACH ROW EXECUTE FUNCTION update_lists_search(); CREATE OR REPLACE FUNCTION update_links_search() RETURNS trigger AS $$ BEGIN NEW.search_vector := to_tsvector('pg_catalog.english', coalesce(NEW.label, '') || ' ' || coalesce(NEW.url, '') || ' ' || coalesce(NEW.description, '') || ' ' || coalesce(array_to_string(NEW.tags, ' '), '')); RETURN NEW; END; $$ LANGUAGE plpgsql; CREATE TRIGGER trg_links_search BEFORE INSERT OR UPDATE ON links FOR EACH ROW EXECUTE FUNCTION update_links_search(); CREATE OR REPLACE FUNCTION update_files_search() RETURNS trigger AS $$ BEGIN NEW.search_vector := to_tsvector('pg_catalog.english', coalesce(NEW.original_filename, '') || ' ' || coalesce(NEW.description, '') || ' ' || coalesce(array_to_string(NEW.tags, ' '), '')); RETURN NEW; END; $$ LANGUAGE plpgsql; CREATE TRIGGER trg_files_search BEFORE INSERT OR UPDATE ON files FOR EACH ROW EXECUTE FUNCTION update_files_search(); CREATE OR REPLACE FUNCTION update_weblinks_search() RETURNS trigger AS $$ BEGIN NEW.search_vector := to_tsvector('pg_catalog.english', coalesce(NEW.label, '') || ' ' || coalesce(NEW.url, '') || ' ' || coalesce(NEW.description, '') || ' ' || coalesce(array_to_string(NEW.tags, ' '), '')); RETURN NEW; END; $$ LANGUAGE plpgsql; CREATE TRIGGER trg_weblinks_search BEFORE INSERT OR UPDATE ON weblinks FOR EACH ROW EXECUTE FUNCTION update_weblinks_search(); CREATE OR REPLACE FUNCTION update_processes_search() RETURNS trigger AS $$ BEGIN NEW.search_vector := to_tsvector('pg_catalog.english', coalesce(NEW.name, '') || ' ' || coalesce(NEW.description, '') || ' ' || coalesce(array_to_string(NEW.tags, ' '), '')); RETURN NEW; END; $$ LANGUAGE plpgsql; CREATE TRIGGER trg_processes_search BEFORE INSERT OR UPDATE ON processes FOR EACH ROW EXECUTE FUNCTION update_processes_search(); CREATE OR REPLACE FUNCTION update_appointments_search() RETURNS trigger AS $$ BEGIN NEW.search_vector := to_tsvector('pg_catalog.english', coalesce(NEW.title, '') || ' ' || coalesce(NEW.description, '') || ' ' || coalesce(NEW.location, '') || ' ' || coalesce(array_to_string(NEW.tags, ' '), '')); RETURN NEW; END; $$ LANGUAGE plpgsql; CREATE TRIGGER trg_appointments_search BEFORE INSERT OR UPDATE ON appointments FOR EACH ROW EXECUTE FUNCTION update_appointments_search(); CREATE OR REPLACE FUNCTION update_releases_search() RETURNS trigger AS $$ BEGIN NEW.search_vector := to_tsvector('pg_catalog.english', coalesce(NEW.name, '') || ' ' || coalesce(NEW.description, '') || ' ' || coalesce(NEW.version_label, '') || ' ' || coalesce(array_to_string(NEW.tags, ' '), '')); RETURN NEW; END; $$ LANGUAGE plpgsql; CREATE TRIGGER trg_releases_search BEFORE INSERT OR UPDATE ON releases FOR EACH ROW EXECUTE FUNCTION update_releases_search(); -- ============================================================================= -- SEED DATA: Context Types -- ============================================================================= INSERT INTO context_types (value, label, is_system, sort_order) VALUES ('deep_work', 'Deep Work', true, 10), ('quick', 'Quick', true, 20), ('waiting', 'Waiting', true, 30), ('someday', 'Someday', true, 40), ('meeting', 'Meeting', true, 50), ('errand', 'Errand', true, 60);