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_<table>_update. Drops any pre-existing trigger of the same name so re-running the migration is safe. Backfills 64 existing rows. Parent: t_e1cbd87d
This commit is contained in:
@@ -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,''));
|
||||
@@ -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
|
||||
}
|
||||
]
|
||||
}
|
||||
Reference in New Issue
Block a user