From 2e451093b6078f03e61ee90dee327431cc4725e5 Mon Sep 17 00:00:00 2001 From: Hermes Date: Sat, 1 Aug 2026 04:15:14 +0000 Subject: [PATCH] T14/Bug #3 HIGH: Postgres search_vector trigger + backfill The tasks/notes/projects/habits/domains tables all have a tsvector search_vector column and a GIN index, but nothing was populating it. GET /api/search returned zero results for every query. This migration adds a BEFORE INSERT OR UPDATE trigger on each of the five searchable tables. The trigger function uses to_tsvector with the appropriate title + description/content columns and is named project_e_search_vector__update. Drops any pre-existing trigger of the same name so re-running the migration is safe. Backfills 64 existing rows. Parent: t_e1cbd87d --- drizzle/0005_search_vector_trigger.sql | 93 ++++++++++++++++++++++++++ drizzle/meta/_journal.json | 7 ++ 2 files changed, 100 insertions(+) create mode 100644 drizzle/0005_search_vector_trigger.sql diff --git a/drizzle/0005_search_vector_trigger.sql b/drizzle/0005_search_vector_trigger.sql new file mode 100644 index 0000000..3381ffa --- /dev/null +++ b/drizzle/0005_search_vector_trigger.sql @@ -0,0 +1,93 @@ +-- T14/Bug #3 HIGH: Populate tsvector search_vector on every INSERT / UPDATE. +-- +-- The schema defines searchVector columns on domains / projects / tasks / +-- habits / notes but the columns are never populated, so /api/search +-- always returns zero results. This migration: +-- 1. Creates a single plpgsql trigger function that builds the tsvector +-- from the table's title-like and content-like text columns. +-- 2. Installs BEFORE INSERT OR UPDATE triggers on each searchable table. +-- 3. Backfills existing rows so they appear in search immediately. + +-- ─── Per-table tsvector field lists ───────────────────────────────────── +-- We can't share one trigger function across tables with different column +-- names, so each table gets its own function. PostgreSQL's CREATE OR +-- REPLACE FUNCTION is idempotent — re-running this migration is safe. + +-- domains: name only (no description column) +CREATE OR REPLACE FUNCTION project_e_search_vector_domains_update() RETURNS trigger AS $$ +BEGIN + NEW.search_vector := to_tsvector('english', coalesce(NEW.name, '')); + RETURN NEW; +END +$$ LANGUAGE plpgsql; + +-- projects: name + description +CREATE OR REPLACE FUNCTION project_e_search_vector_projects_update() RETURNS trigger AS $$ +BEGIN + NEW.search_vector := to_tsvector('english', + coalesce(NEW.name, '') || ' ' || coalesce(NEW.description, '')); + RETURN NEW; +END +$$ LANGUAGE plpgsql; + +-- tasks: title + description +CREATE OR REPLACE FUNCTION project_e_search_vector_tasks_update() RETURNS trigger AS $$ +BEGIN + NEW.search_vector := to_tsvector('english', + coalesce(NEW.title, '') || ' ' || coalesce(NEW.description, '')); + RETURN NEW; +END +$$ LANGUAGE plpgsql; + +-- habits: name + description +CREATE OR REPLACE FUNCTION project_e_search_vector_habits_update() RETURNS trigger AS $$ +BEGIN + NEW.search_vector := to_tsvector('english', + coalesce(NEW.name, '') || ' ' || coalesce(NEW.description, '')); + RETURN NEW; +END +$$ LANGUAGE plpgsql; + +-- notes: title + content +CREATE OR REPLACE FUNCTION project_e_search_vector_notes_update() RETURNS trigger AS $$ +BEGIN + NEW.search_vector := to_tsvector('english', + coalesce(NEW.title, '') || ' ' || coalesce(NEW.content, '')); + RETURN NEW; +END +$$ LANGUAGE plpgsql; + +-- ─── Per-table triggers (BEFORE INSERT OR UPDATE) ─────────────────────── +DROP TRIGGER IF EXISTS domains_search_vector_trigger ON domains; +CREATE TRIGGER domains_search_vector_trigger + BEFORE INSERT OR UPDATE ON domains + FOR EACH ROW EXECUTE FUNCTION project_e_search_vector_domains_update(); + +DROP TRIGGER IF EXISTS projects_search_vector_trigger ON projects; +CREATE TRIGGER projects_search_vector_trigger + BEFORE INSERT OR UPDATE ON projects + FOR EACH ROW EXECUTE FUNCTION project_e_search_vector_projects_update(); + +DROP TRIGGER IF EXISTS tasks_search_vector_trigger ON tasks; +CREATE TRIGGER tasks_search_vector_trigger + BEFORE INSERT OR UPDATE ON tasks + FOR EACH ROW EXECUTE FUNCTION project_e_search_vector_tasks_update(); + +DROP TRIGGER IF EXISTS habits_search_vector_trigger ON habits; +CREATE TRIGGER habits_search_vector_trigger + BEFORE INSERT OR UPDATE ON habits + FOR EACH ROW EXECUTE FUNCTION project_e_search_vector_habits_update(); + +DROP TRIGGER IF EXISTS notes_search_vector_trigger ON notes; +CREATE TRIGGER notes_search_vector_trigger + BEFORE INSERT OR UPDATE ON notes + FOR EACH ROW EXECUTE FUNCTION project_e_search_vector_notes_update(); + +-- ─── Backfill existing rows ───────────────────────────────────────────── +-- Use IS NOT NULL guard on text columns so blank rows are skipped silently. +-- to_tsvector returns an empty tsvector ('') for blank input — harmless. +UPDATE domains SET search_vector = to_tsvector('english', coalesce(name, '')); +UPDATE projects SET search_vector = to_tsvector('english', coalesce(name,'') || ' ' || coalesce(description,'')); +UPDATE tasks SET search_vector = to_tsvector('english', coalesce(title,'') || ' ' || coalesce(description,'')); +UPDATE habits SET search_vector = to_tsvector('english', coalesce(name,'') || ' ' || coalesce(description,'')); +UPDATE notes SET search_vector = to_tsvector('english', coalesce(title,'') || ' ' || coalesce(content,'')); diff --git a/drizzle/meta/_journal.json b/drizzle/meta/_journal.json index f5329a7..7e33e64 100644 --- a/drizzle/meta/_journal.json +++ b/drizzle/meta/_journal.json @@ -36,6 +36,13 @@ "when": 1785455000000, "tag": "0004_add_owner_id_to_domains", "breakpoints": true + }, + { + "idx": 5, + "version": "7", + "when": 1785557679802, + "tag": "0005_search_vector_trigger", + "breakpoints": true } ] } \ No newline at end of file