-- 0016_views.sql -- -- Phase 5i Slice D: persistent saved views. -- -- A saved view bundles (filter + view_type + sort + group_by) under a -- name. Page-agnostic per m's Q2 pick (2026-05-26) — the view doesn't -- own a route; `is_default_for` lets one view become the auto-applied -- default for a given page. -- -- Singleton user; no `user_id` column. If multi-user ever lands, the -- two partial unique indexes below need a `(user_id, …)` prefix. CREATE TABLE IF NOT EXISTS projax.views ( id uuid PRIMARY KEY DEFAULT gen_random_uuid(), name text NOT NULL, description text, filter_json jsonb NOT NULL DEFAULT '{}'::jsonb, view_type text NOT NULL, sort_field text, sort_dir text, group_by text, pinned boolean NOT NULL DEFAULT false, is_default_for text, created_at timestamptz NOT NULL DEFAULT now(), updated_at timestamptz NOT NULL DEFAULT now(), deleted_at timestamptz, CONSTRAINT views_view_type_chk CHECK (view_type IN ('card','list','calendar','kanban','timeline')), CONSTRAINT views_sort_dir_chk CHECK (sort_dir IS NULL OR sort_dir IN ('asc','desc')), CONSTRAINT views_kanban_needs_group CHECK (view_type <> 'kanban' OR group_by IS NOT NULL), CONSTRAINT views_default_for_chk CHECK (is_default_for IS NULL OR is_default_for IN ('tree','dashboard','calendar','timeline')) ); -- Case-insensitive uniqueness on the visible name. Soft-deleted rows are -- exempt so a re-create after delete doesn't collide. CREATE UNIQUE INDEX IF NOT EXISTS views_name_uniq ON projax.views (lower(name)) WHERE deleted_at IS NULL; -- One default view per page. The handler should clear the prior default in -- the same transaction as setting a new one; the index defends against any -- code path that forgets. CREATE UNIQUE INDEX IF NOT EXISTS views_default_for_uniq ON projax.views (is_default_for) WHERE is_default_for IS NOT NULL AND deleted_at IS NULL; -- updated_at trigger mirrors the items table pattern. CREATE OR REPLACE FUNCTION projax.views_touch_updated_at() RETURNS trigger LANGUAGE plpgsql AS $$ BEGIN NEW.updated_at := now(); RETURN NEW; END; $$; DROP TRIGGER IF EXISTS views_touch_updated_at ON projax.views; CREATE TRIGGER views_touch_updated_at BEFORE UPDATE ON projax.views FOR EACH ROW EXECUTE FUNCTION projax.views_touch_updated_at(); DO $own$ BEGIN IF EXISTS (SELECT 1 FROM pg_roles WHERE rolname = 'projax_admin') THEN EXECUTE 'ALTER TABLE projax.views OWNER TO projax_admin'; EXECUTE 'ALTER FUNCTION projax.views_touch_updated_at() OWNER TO projax_admin'; EXECUTE 'GRANT SELECT, INSERT, UPDATE, DELETE ON projax.views TO projax_admin'; END IF; END $own$;