made tiny updates

This commit is contained in:
SinachPat
2026-05-04 12:30:56 +01:00
parent 3f029e15c2
commit 5b2d918c13
47 changed files with 2597 additions and 521 deletions
+52 -39
View File
@@ -1,46 +1,59 @@
-- Origin Graph — Artboard Ancestry Materialized View
-- Migration: 002
-- Pre-computes all ancestor/descendant relationships so the app never needs
-- recursive CTEs at query time. Updated automatically on artboards INSERT.
-- ── Migration 002: Artboard Ancestry Guard ───────────────────────────────────
-- The artboard_ancestry closure table and its maintenance trigger are already
-- created in migration 001 (initial schema). This migration is a safe, idempotent
-- guard that ensures the trigger function and trigger exist as expected.
--
-- IMPORTANT: An earlier draft of this file attempted to create a MATERIALIZED
-- VIEW named artboard_ancestry, which would conflict with the table created in
-- 001. That approach has been superseded: the trigger-based closure table from
-- 001 is maintained incrementally (O(depth) per INSERT) which is far cheaper
-- than a full REFRESH MATERIALIZED VIEW CONCURRENTLY on every artboard insert.
-- Do NOT re-introduce the materialized view approach.
CREATE MATERIALIZED VIEW artboard_ancestry AS
WITH RECURSIVE ancestry(artboard_id, ancestor_id, depth) AS (
-- Base: each artboard is at depth 0 relative to itself
SELECT id AS artboard_id, id AS ancestor_id, 0 AS depth
FROM artboards
-- ── Ensure the ancestry maintenance trigger function exists ───────────────────
-- Uses CREATE OR REPLACE so the migration is idempotent; safe to re-run.
UNION ALL
-- Recurse: walk up the parent chain
SELECT a.id AS artboard_id, anc.ancestor_id, anc.depth + 1
FROM artboards a
JOIN ancestry anc ON a.parent_artboard_id = anc.artboard_id
)
SELECT artboard_id, ancestor_id, depth
FROM ancestry
WHERE artboard_id <> ancestor_id -- exclude self-reference
ORDER BY artboard_id, depth;
CREATE UNIQUE INDEX artboard_ancestry_pk
ON artboard_ancestry(artboard_id, ancestor_id);
CREATE INDEX artboard_ancestry_ancestor_idx
ON artboard_ancestry(ancestor_id);
-- ── Refresh trigger ───────────────────────────────────────────────────────────
-- Refreshes the materialized view concurrently whenever an artboard is
-- inserted. CONCURRENTLY requires the unique index above — it allows reads
-- to continue during refresh.
CREATE OR REPLACE FUNCTION refresh_artboard_ancestry()
CREATE OR REPLACE FUNCTION maintain_artboard_ancestry()
RETURNS TRIGGER LANGUAGE plpgsql AS $$
BEGIN
REFRESH MATERIALIZED VIEW CONCURRENTLY artboard_ancestry;
RETURN NULL;
-- Self-row: every artboard is its own ancestor at depth 0.
INSERT INTO artboard_ancestry (artboard_id, ancestor_id, depth)
VALUES (NEW.id, NEW.id, 0)
ON CONFLICT DO NOTHING;
-- Inherit all ancestors of the parent at depth + 1.
IF NEW.parent_artboard_id IS NOT NULL THEN
INSERT INTO artboard_ancestry (artboard_id, ancestor_id, depth)
SELECT NEW.id, ancestor_id, depth + 1
FROM artboard_ancestry
WHERE artboard_id = NEW.parent_artboard_id
ON CONFLICT DO NOTHING;
END IF;
RETURN NEW;
END;
$$;
CREATE TRIGGER trg_artboard_ancestry_refresh
AFTER INSERT OR UPDATE OF parent_artboard_id ON artboards
FOR EACH STATEMENT
EXECUTE FUNCTION refresh_artboard_ancestry();
-- ── Ensure the trigger is attached ───────────────────────────────────────────
-- DROP + recreate is the idempotent way to ensure the trigger is attached
-- exactly once; IF NOT EXISTS for triggers requires PG 17+ so use this pattern.
DROP TRIGGER IF EXISTS artboards_ancestry_insert ON artboards;
CREATE TRIGGER artboards_ancestry_insert
AFTER INSERT ON artboards
FOR EACH ROW EXECUTE FUNCTION maintain_artboard_ancestry();
-- ── RPC helper: get all descendants of an artboard ───────────────────────────
-- Complements getArtboardAncestors() in origin-graph/queries.ts.
-- Returns direct + transitive descendants, ordered nearest-first.
CREATE OR REPLACE FUNCTION get_artboard_descendants(p_artboard_id UUID)
RETURNS TABLE (artboard_id UUID, depth INTEGER)
LANGUAGE SQL STABLE AS $$
SELECT artboard_id, depth
FROM artboard_ancestry
WHERE ancestor_id = p_artboard_id
AND artboard_id <> p_artboard_id -- exclude self
ORDER BY depth, artboard_id;
$$;
+127
View File
@@ -0,0 +1,127 @@
-- ── Migration 006: Full-Text Search ──────────────────────────────────────────
-- Adds tsvector generated columns and GIN indexes to artboards and origins so
-- the natural-language cross-artboard query feature (Layer 10 / Phase 3) can do
-- weighted full-text search without re-scanning jsonb at query time.
--
-- search_artboards(query, workspace_id) is the primary search entry point.
-- It returns artboard rows ranked by text relevance using ts_rank_cd.
-- ── 1. Generated tsvector column on artboards ─────────────────────────────────
-- Covers: artboard name (weight A), route / renderUrl from metadata (weight B),
-- and the raw metadata_jsonb text (weight C).
ALTER TABLE artboards
ADD COLUMN IF NOT EXISTS search_vector tsvector
GENERATED ALWAYS AS (
setweight(to_tsvector('english', coalesce(name, '')), 'A') ||
setweight(to_tsvector('english', coalesce(metadata_jsonb::text, '')), 'C')
) STORED;
CREATE INDEX IF NOT EXISTS artboards_search_vector_idx
ON artboards USING GIN (search_vector);
-- ── 2. Generated tsvector column on origins ───────────────────────────────────
-- Covers: source_ref (weight A), source_metadata_jsonb text (weight B).
ALTER TABLE origins
ADD COLUMN IF NOT EXISTS search_vector tsvector
GENERATED ALWAYS AS (
setweight(to_tsvector('english', coalesce(source_ref, '')), 'A') ||
setweight(to_tsvector('english', coalesce(source_metadata_jsonb::text, '')), 'B')
) STORED;
CREATE INDEX IF NOT EXISTS origins_search_vector_idx
ON origins USING GIN (search_vector);
-- ── 3. Generated tsvector on intent_diffs ────────────────────────────────────
-- Covers: summary (weight A), changes_jsonb text (weight C).
-- Allows searching "show me every diff about the navigation bar" style queries.
ALTER TABLE intent_diffs
ADD COLUMN IF NOT EXISTS search_vector tsvector
GENERATED ALWAYS AS (
setweight(to_tsvector('english', coalesce(summary, '')), 'A') ||
setweight(to_tsvector('english', coalesce(changes_jsonb::text, '')), 'C')
) STORED;
CREATE INDEX IF NOT EXISTS intent_diffs_search_vector_idx
ON intent_diffs USING GIN (search_vector);
-- ── 4. search_artboards RPC ───────────────────────────────────────────────────
-- Parameters:
-- p_workspace_id — scope results to a single workspace
-- p_query — plain-text search string (converted to tsquery internally)
-- p_limit — max rows to return (default 20)
--
-- Returns artboards ranked by text relevance. Joins to origins and intent_diffs
-- to boost matches found in provenance or diff history.
--
-- Plain-to-tsquery converts arbitrary user text into a safe tsquery, allowing
-- partial words and multi-word phrases without syntax errors.
CREATE OR REPLACE FUNCTION search_artboards(
p_workspace_id UUID,
p_query TEXT,
p_limit INTEGER DEFAULT 20
)
RETURNS TABLE (
id UUID,
workspace_id UUID,
project_id UUID,
name TEXT,
origin_id UUID,
parent_artboard_id UUID,
metadata_jsonb JSONB,
created_at TIMESTAMPTZ,
updated_at TIMESTAMPTZ,
rank FLOAT4
)
LANGUAGE SQL STABLE AS $$
WITH q AS (
SELECT plainto_tsquery('english', p_query) AS tsq
)
SELECT
a.id,
a.workspace_id,
a.project_id,
a.name,
a.origin_id,
a.parent_artboard_id,
a.metadata_jsonb,
a.created_at,
a.updated_at,
-- Combine artboard rank with a bonus for matching origins or diffs
(
ts_rank_cd(a.search_vector, q.tsq, 32) * 1.5 +
coalesce((
SELECT max(ts_rank_cd(d.search_vector, q.tsq, 32))
FROM intent_diffs d
WHERE d.artboard_id = a.id
AND d.search_vector @@ q.tsq
), 0) +
coalesce((
SELECT ts_rank_cd(o.search_vector, q.tsq, 32)
FROM origins o
WHERE o.id = a.origin_id
AND o.search_vector @@ q.tsq
), 0)
) AS rank
FROM artboards a, q
WHERE
a.workspace_id = p_workspace_id
AND (
a.search_vector @@ q.tsq
OR EXISTS (
SELECT 1 FROM intent_diffs d
WHERE d.artboard_id = a.id
AND d.search_vector @@ q.tsq
)
OR EXISTS (
SELECT 1 FROM origins o
WHERE o.id = a.origin_id
AND o.search_vector @@ q.tsq
)
)
ORDER BY rank DESC, a.updated_at DESC
LIMIT p_limit;
$$;
+111
View File
@@ -0,0 +1,111 @@
-- ── Migration 007: Spec Compliance ───────────────────────────────────────────
-- Adds the columns and constraint values that were specified in the Layer 4-5
-- design but omitted from migration 001. All additions are additive (nullable
-- columns, expanded CHECK constraint value sets) so existing rows remain valid.
--
-- Tables touched:
-- origins — source_id, source_url, screenshot_url
-- intent_diffs — session_id, before_screenshot, after_screenshot,
-- exported_code + expanded status CHECK
-- design_language_files — is_active, created_by
-- artboards — route, remote_url, width, height, created_by
-- + expanded origin type CHECK via origins table
-- ── 1. origins — provenance fields ───────────────────────────────────────────
-- The spec required source_id (e.g. Linear issue ID, Git SHA, Slack message TS),
-- a canonical source_url, and a screenshot_url for the origin artifact.
-- All nullable — not every origin type has all three fields.
ALTER TABLE origins
ADD COLUMN IF NOT EXISTS source_id text,
ADD COLUMN IF NOT EXISTS source_url text,
ADD COLUMN IF NOT EXISTS screenshot_url text;
-- Expand the origin type CHECK to include the spec's lowercase values alongside
-- the existing uppercase ones. PostgreSQL CHECK is re-evaluated on INSERT/UPDATE
-- only, so existing rows are unaffected.
ALTER TABLE origins
DROP CONSTRAINT IF EXISTS origins_type_check;
ALTER TABLE origins
ADD CONSTRAINT origins_type_check
CHECK (type IN (
-- Original uppercase set
'GIT_COMMIT', 'LINEAR_ISSUE', 'SLACK_MESSAGE', 'URL', 'FORK',
-- Spec Layer 4 lowercase set
'route', 'linear', 'git', 'slack', 'feedback', 'fork', 'manual',
-- Canonical aliases
'ROUTE', 'FEEDBACK', 'MANUAL'
));
-- ── 2. intent_diffs — lifecycle fields ───────────────────────────────────────
-- session_id links a diff back to the agent session that produced it.
-- before/after screenshots enable the visual diff view in the export panel.
-- exported_code stores the generated TypeScript/JSX expressing the changes.
ALTER TABLE intent_diffs
ADD COLUMN IF NOT EXISTS session_id text,
ADD COLUMN IF NOT EXISTS before_screenshot text,
ADD COLUMN IF NOT EXISTS after_screenshot text,
ADD COLUMN IF NOT EXISTS exported_code text;
-- Expand the status CHECK to include all values used by the spec and the UI.
-- Original: DRAFT, EXPORTED, IMPLEMENTED, BLOCKED
-- Spec adds: acknowledged, rejected (lowercase in spec, uppercase in app)
-- App uses: REVIEWED, APPLIED (from Inspector STATUS_COLOR map)
ALTER TABLE intent_diffs
DROP CONSTRAINT IF EXISTS intent_diffs_status_check;
ALTER TABLE intent_diffs
ADD CONSTRAINT intent_diffs_status_check
CHECK (status IN (
'DRAFT', 'EXPORTED', 'IMPLEMENTED', 'BLOCKED',
'ACKNOWLEDGED', 'REJECTED',
'REVIEWED', 'APPLIED'
));
-- Update the get_diffs_by_status RPC to accept the full value set.
-- The function body is unchanged; only the type comment is refreshed.
CREATE OR REPLACE FUNCTION get_diffs_by_status(p_workspace_id uuid, p_status text)
RETURNS SETOF intent_diffs
LANGUAGE SQL STABLE AS $$
SELECT d.*
FROM intent_diffs d
JOIN artboards a ON a.id = d.artboard_id
WHERE a.workspace_id = p_workspace_id
AND d.status = p_status
ORDER BY d.created_at DESC;
$$;
-- ── 3. design_language_files — activation + ownership ────────────────────────
-- is_active provides the spec's boolean gate for "is this DLF in effect?"
-- The existing schema uses version + max(version) query for the active file;
-- is_active supplements that pattern and also enables instant deactivation
-- without incrementing the version.
-- created_by stores the Clerk user ID of the uploader (audit trail).
ALTER TABLE design_language_files
ADD COLUMN IF NOT EXISTS is_active boolean NOT NULL DEFAULT true,
ADD COLUMN IF NOT EXISTS created_by text;
-- Index for the "get active DLF for workspace" query pattern.
CREATE INDEX IF NOT EXISTS design_language_files_is_active_idx
ON design_language_files (workspace_id, is_active, version DESC);
-- ── 4. artboards — explicit dimensional + provenance columns ─────────────────
-- The spec modelled these as first-class columns. The existing schema stores
-- them in metadata_jsonb instead. Adding as nullable explicit columns lets both
-- patterns coexist: old rows keep metadata_jsonb, new rows can set either/both.
-- No NOT NULL constraints so zero migration cost for existing rows.
ALTER TABLE artboards
ADD COLUMN IF NOT EXISTS route text,
ADD COLUMN IF NOT EXISTS remote_url text,
ADD COLUMN IF NOT EXISTS width integer,
ADD COLUMN IF NOT EXISTS height integer,
ADD COLUMN IF NOT EXISTS created_by text;
-- Partial index: quickly find all artboards with an explicit route set.
CREATE INDEX IF NOT EXISTS artboards_route_idx
ON artboards (workspace_id, route)
WHERE route IS NOT NULL;
@@ -0,0 +1,197 @@
-- ── Migration 008: Spec schema alignment ──────────────────────────────────────
-- Aligns the database schema with the Layer 4 spec requirements.
-- All changes are additive or rename-only; no data is destroyed.
--
-- Changes:
-- 1. artboards.width / height — add NOT NULL DEFAULT per spec (1440 / 900)
-- 2. artboards.parent_id — spec names this column parent_id (not parent_artboard_id);
-- add parent_id as a generated column alias
-- 3. origins.artboard_id — spec requires the FK to live on origins (not artboards)
-- 4. intent_diffs: rename summary → aggregate_summary + rename changes_jsonb → changes
-- 5. intent_diffs.session_id — make NOT NULL per spec (was nullable in migration 007)
-- 6. agent_sessions.agent_type — add lowercase check values per spec
-- 7. RLS: add permissive workspace-membership allow policies
-- ── 1. artboards width/height defaults ───────────────────────────────────────
-- Backfill nulls introduced by migration 007, then add NOT NULL + DEFAULT.
update artboards set width = 1440 where width is null;
update artboards set height = 900 where height is null;
alter table artboards
alter column width set not null,
alter column width set default 1440,
alter column height set not null,
alter column height set default 900;
-- created_by: spec says NOT NULL; backfill with '' then constrain
update artboards set created_by = '' where created_by is null;
alter table artboards alter column created_by set not null;
alter table artboards alter column created_by set default '';
-- ── 2. artboards.parent_id alias ─────────────────────────────────────────────
-- Spec uses parent_id; existing code uses parent_artboard_id. Add parent_id as
-- a nullable FK that mirrors parent_artboard_id for spec-compliant consumers.
-- Both columns will co-exist during the transition.
do $$ begin
if not exists (
select 1 from information_schema.columns
where table_name = 'artboards' and column_name = 'parent_id'
) then
alter table artboards add column parent_id uuid references artboards(id);
-- Backfill from the existing column
update artboards set parent_id = parent_artboard_id where parent_artboard_id is not null;
end if;
end $$;
-- ── 3. origins.artboard_id (spec FK direction) ───────────────────────────────
-- Spec: origins.artboard_id uuid not null references artboards(id) on delete cascade
-- Existing: artboards.origin_id (reverse FK).
-- We add the spec-required column as nullable (existing origin rows have no artboard_id
-- until they are re-linked). New origins created via spec-compliant code must set it.
do $$ begin
if not exists (
select 1 from information_schema.columns
where table_name = 'origins' and column_name = 'artboard_id'
) then
alter table origins add column artboard_id uuid references artboards(id) on delete cascade;
-- Best-effort backfill: find artboards pointing to each origin
update origins o
set artboard_id = a.id
from artboards a
where a.origin_id = o.id;
end if;
end $$;
-- ── 4. intent_diffs: rename columns to spec names ────────────────────────────
-- Rename changes_jsonb → changes (spec column name)
do $$ begin
if exists (
select 1 from information_schema.columns
where table_name = 'intent_diffs' and column_name = 'changes_jsonb'
) and not exists (
select 1 from information_schema.columns
where table_name = 'intent_diffs' and column_name = 'changes'
) then
alter table intent_diffs rename column changes_jsonb to changes;
end if;
end $$;
-- Rename summary → aggregate_summary (spec column name)
do $$ begin
if exists (
select 1 from information_schema.columns
where table_name = 'intent_diffs' and column_name = 'summary'
) and not exists (
select 1 from information_schema.columns
where table_name = 'intent_diffs' and column_name = 'aggregate_summary'
) then
alter table intent_diffs rename column summary to aggregate_summary;
end if;
end $$;
-- ── 5. intent_diffs.session_id NOT NULL ──────────────────────────────────────
-- Spec: session_id text not null. Backfill existing nulls, then constrain.
update intent_diffs set session_id = '' where session_id is null;
alter table intent_diffs alter column session_id set not null;
alter table intent_diffs alter column session_id set default '';
-- Also: author_id was NOT NULL in the spec from migration 001 — ensure created_by
-- exists on design_language_files as NOT NULL (backfill then constrain)
update design_language_files set created_by = '' where created_by is null;
alter table design_language_files alter column created_by set not null;
alter table design_language_files alter column created_by set default '';
-- ── 6. agent_sessions: expand agent_type to include spec lowercase values ─────
-- Spec: 'cursor', 'claude-code', 'generic' (lowercase, hyphenated)
-- Existing: uppercase 'CURSOR', 'CLAUDE_CODE', 'GENERIC' + original set
-- Drop and re-create the check constraint to include both sets.
alter table agent_sessions drop constraint if exists agent_sessions_agent_type_check;
alter table agent_sessions add constraint agent_sessions_agent_type_check
check (agent_type in (
'cursor', 'claude-code', 'generic', -- spec lowercase
'CURSOR', 'CLAUDE_CODE', 'GENERIC' -- existing uppercase
));
-- ── 7. RLS workspace-membership allow policies ────────────────────────────────
-- Spec: "workspace members can access" via auth.uid()::text = owner_id
-- These are the permissive policies required by Layer 4. The deny-all-anon
-- policies from migration 003 remain and complement these.
-- workspaces: owner can access their own workspace
drop policy if exists "workspace owner access" on workspaces;
create policy "workspace owner access" on workspaces
for all
using (owner_id = auth.uid()::text);
-- artboards: members of the workspace can access
drop policy if exists "workspace member artboard access" on artboards;
create policy "workspace member artboard access" on artboards
for all
using (
workspace_id in (
select id from workspaces where owner_id = auth.uid()::text
)
or workspace_id in (
select workspace_id from team_members where user_id = auth.uid()::text
)
);
-- origins: accessible when the linked artboard is accessible
drop policy if exists "workspace member origin access" on origins;
create policy "workspace member origin access" on origins
for all
using (
id in (
select origin_id from artboards
where workspace_id in (
select id from workspaces where owner_id = auth.uid()::text
union
select workspace_id from team_members where user_id = auth.uid()::text
)
and origin_id is not null
)
);
-- intent_diffs: accessible when the linked artboard is accessible
drop policy if exists "workspace member diff access" on intent_diffs;
create policy "workspace member diff access" on intent_diffs
for all
using (
artboard_id in (
select id from artboards
where workspace_id in (
select id from workspaces where owner_id = auth.uid()::text
union
select workspace_id from team_members where user_id = auth.uid()::text
)
)
);
-- design_language_files: accessible within the workspace
drop policy if exists "workspace member dlf access" on design_language_files;
create policy "workspace member dlf access" on design_language_files
for all
using (
workspace_id in (
select id from workspaces where owner_id = auth.uid()::text
union
select workspace_id from team_members where user_id = auth.uid()::text
)
);
-- agent_sessions: accessible when the linked artboard is accessible
drop policy if exists "workspace member session access" on agent_sessions;
create policy "workspace member session access" on agent_sessions
for all
using (
artboard_id in (
select id from artboards
where workspace_id in (
select id from workspaces where owner_id = auth.uid()::text
union
select workspace_id from team_members where user_id = auth.uid()::text
)
)
);
@@ -0,0 +1,22 @@
-- migration 009 — Add spec-canonical lowercase status values to intent_diffs CHECK
-- Spec Layer 4: canonical status values are lowercase:
-- 'draft', 'exported', 'acknowledged', 'implemented', 'rejected'
-- The current CHECK (from migration 007) only allows uppercase values.
-- New code (Inspector.tsx, CompletionZone.tsx) inserts lowercase 'draft'.
-- This migration expands the CHECK to accept both cases so both old and new
-- code work without a breaking schema change.
ALTER TABLE intent_diffs
DROP CONSTRAINT IF EXISTS intent_diffs_status_check;
ALTER TABLE intent_diffs
ADD CONSTRAINT intent_diffs_status_check
CHECK (status IN (
-- Spec-canonical lowercase (Layer 4)
'draft', 'exported', 'acknowledged', 'implemented', 'rejected',
-- Legacy uppercase (migration 001, migration 007) — kept for compat
'DRAFT', 'EXPORTED', 'IMPLEMENTED', 'BLOCKED',
'ACKNOWLEDGED', 'REJECTED',
-- UI-driven additions (Inspector STATUS_COLOR map)
'REVIEWED', 'APPLIED'
));
@@ -0,0 +1,53 @@
-- ── Migration 010: Fix origins RLS policy ────────────────────────────────────
--
-- Problem (introduced in migration 008):
-- The "workspace member origin access" policy only checks the OLD FK direction
-- (artboards.origin_id → origins.id). Migration 008 added the SPEC-required FK
-- column origins.artboard_id, but the policy was not updated. Any origin created
-- via spec-compliant code (setting origins.artboard_id) is therefore invisible
-- to authenticated users — the SELECT returns zero rows even though the insert
-- succeeds.
--
-- Fix:
-- Rewrite the policy to accept either FK direction:
-- 1. NEW (spec): origins.artboard_id is in the user's accessible artboard set.
-- 2. OLD (legacy): origins.id appears in artboards.origin_id for accessible boards.
-- The OR allows both legacy data and spec-compliant data to be readable without
-- requiring a destructive data migration.
-- ─────────────────────────────────────────────────────────────────────────────
-- Helper CTE: IDs of all artboards accessible to the calling user.
-- Referenced by both branches of the OR below.
drop policy if exists "workspace member origin access" on origins;
create policy "workspace member origin access" on origins
for all
using (
-- ── Branch 1: spec-compliant origins (origins.artboard_id FK) ───────────
-- New origins set origins.artboard_id = <artboard uuid>; the origin is
-- accessible iff that artboard is in the user's workspace.
artboard_id in (
select id from artboards
where workspace_id in (
select id from workspaces where owner_id = auth.uid()::text
union
select workspace_id from team_members where user_id = auth.uid()::text
)
)
or
-- ── Branch 2: legacy origins (artboards.origin_id FK) ───────────────────
-- Old origins are referenced from artboards via artboards.origin_id.
-- Still need to work so existing data stays accessible while backfill
-- populates origins.artboard_id (migration 008 best-effort backfill).
id in (
select origin_id from artboards
where workspace_id in (
select id from workspaces where owner_id = auth.uid()::text
union
select workspace_id from team_members where user_id = auth.uid()::text
)
and origin_id is not null
)
);
@@ -0,0 +1,37 @@
-- ── Migration 011: Add route column to artboards FTS tsvector ────────────────
--
-- Problem:
-- Migration 006 created artboards.search_vector with only:
-- name (weight A) || metadata_jsonb::text (weight C)
-- The comment in 006 says route should be at weight B, but route didn't exist
-- until migration 007. Generated column expressions cannot be altered in-place
-- (pre-PG17) — the column must be dropped and re-added.
--
-- Fix:
-- Drop and recreate artboards.search_vector to include:
-- name (weight A) — exact component/screen name matches rank highest
-- route (weight B) — URL route matches rank highly (added by migration 007)
-- metadata_jsonb (weight C) — catch-all metadata text
--
-- Drop/re-add regenerates values for all existing rows automatically since
-- the column is GENERATED ALWAYS AS (stored computed column).
-- ─────────────────────────────────────────────────────────────────────────────
-- Drop the existing generated column (and its GIN index, which depends on it)
DROP INDEX IF EXISTS artboards_search_vector_idx;
ALTER TABLE artboards
DROP COLUMN IF EXISTS search_vector;
-- Re-add with route at weight B
ALTER TABLE artboards
ADD COLUMN search_vector tsvector
GENERATED ALWAYS AS (
setweight(to_tsvector('english', coalesce(name, '')), 'A') ||
setweight(to_tsvector('english', coalesce(route, '')), 'B') ||
setweight(to_tsvector('english', coalesce(metadata_jsonb::text, '')), 'C')
) STORED;
-- Rebuild the GIN index on the updated column
CREATE INDEX artboards_search_vector_idx
ON artboards USING GIN (search_vector);