Files
paliad/internal/db/migrations/070_paliadin_inline.up.sql
m 282e0bb237 feat(paliadin/migration-070): t-paliad-161 Slice A — schema for agent-suggested write path
Two coordinated additions:

1. paliad.approval_requests gets requester_kind text NOT NULL DEFAULT
   'user' CHECK ('user','agent') + agent_turn_id uuid REFERENCES
   paliadin_turns(turn_id) ON DELETE SET NULL. The xor-check pins
   (kind='agent' ↔ agent_turn_id IS NOT NULL) so agent rows can't lose
   provenance and user rows can't accidentally pick up a turn id.
   Existing rows backfill cleanly via the DEFAULT.

2. paliad.paliadin_turns.context jsonb — structured page-context
   payload (route_name + primary_entity + selection + view hints) the
   inline widget submits with every turn. Old page_origin column stays
   as the cosmetic URL field.

Idempotent — every ALTER uses IF NOT EXISTS and the constraints/index
are guarded by DO blocks. Verified live via BEGIN..ROLLBACK on the
production DB: cols + 3 constraints + index land cleanly, second apply
is a no-op, xor-check rejects ('agent', NULL).

Skipped the optional PaliadinRelay interface extraction per the design
doc's own §6.4 caveat: paliadin.go + paliadin_remote.go already share
the paliadinDB substrate cleanly; introducing an interface now would
duplicate without removing duplication. Reserves the seam for the
future API cutover without paying its cost today.

Refs: docs/design-paliadin-inline-2026-05-08.md §7.1, §4.2, §6.4.
2026-05-08 19:42:05 +02:00

107 lines
4.4 KiB
SQL

-- t-paliad-161: Inline Paliadin chat modal + agent-suggested write path.
--
-- Design: docs/design-paliadin-inline-2026-05-08.md §7.1 + §4.2.
--
-- Two coordinated additions:
--
-- 1. paliad.approval_requests gets two new columns marking which requests
-- were drafted by Paliadin on a user's behalf:
-- - requester_kind text — 'user' (direct create) | 'agent' (Paliadin
-- suggestion awaiting the user's review)
-- - agent_turn_id uuid — links back to the paliadin_turns row that
-- produced the suggestion. ON DELETE SET NULL
-- so audit rows survive turn-row purges.
-- The xor-check pins (kind='agent' ↔ agent_turn_id IS NOT NULL) so we
-- can't lose provenance on agent rows or accidentally tag user rows
-- with a turn id.
--
-- 2. paliad.paliadin_turns.context jsonb — the structured page-context
-- payload (route_name + primary_entity_type + primary_entity_id +
-- user_selection_text + view hints) the inline widget submits with
-- every turn. Old page_origin column stays as the cosmetic URL field.
--
-- Idempotent — every ALTER uses IF NOT EXISTS and the constraints/index
-- are guarded by DO blocks. Re-applying after a partial failure leaves
-- the same end state as a fresh run.
-- ============================================================================
-- 1. paliad.approval_requests.requester_kind + agent_turn_id
-- ============================================================================
ALTER TABLE paliad.approval_requests
ADD COLUMN IF NOT EXISTS requester_kind text NOT NULL DEFAULT 'user';
DO $$
BEGIN
IF NOT EXISTS (
SELECT 1 FROM pg_constraint
WHERE conname = 'approval_requests_requester_kind_check'
AND conrelid = 'paliad.approval_requests'::regclass
) THEN
ALTER TABLE paliad.approval_requests
ADD CONSTRAINT approval_requests_requester_kind_check
CHECK (requester_kind IN ('user', 'agent'));
END IF;
END$$;
ALTER TABLE paliad.approval_requests
ADD COLUMN IF NOT EXISTS agent_turn_id uuid;
DO $$
BEGIN
IF NOT EXISTS (
SELECT 1 FROM pg_constraint
WHERE conname = 'approval_requests_agent_turn_fk'
AND conrelid = 'paliad.approval_requests'::regclass
) THEN
ALTER TABLE paliad.approval_requests
ADD CONSTRAINT approval_requests_agent_turn_fk
FOREIGN KEY (agent_turn_id) REFERENCES paliad.paliadin_turns(turn_id)
ON DELETE SET NULL;
END IF;
END$$;
DO $$
BEGIN
IF NOT EXISTS (
SELECT 1 FROM pg_constraint
WHERE conname = 'approval_requests_agent_xor'
AND conrelid = 'paliad.approval_requests'::regclass
) THEN
ALTER TABLE paliad.approval_requests
ADD CONSTRAINT approval_requests_agent_xor
CHECK (
(requester_kind = 'agent' AND agent_turn_id IS NOT NULL)
OR (requester_kind = 'user' AND agent_turn_id IS NULL)
);
END IF;
END$$;
CREATE INDEX IF NOT EXISTS approval_requests_agent_turn_idx
ON paliad.approval_requests (agent_turn_id)
WHERE agent_turn_id IS NOT NULL;
COMMENT ON COLUMN paliad.approval_requests.requester_kind IS
'Who originated the request: ''user'' (direct user create) or '
'''agent'' (Paliadin drafted it from a chat turn, awaiting user '
'approval). Default ''user'' so existing audit rows backfill cleanly.';
COMMENT ON COLUMN paliad.approval_requests.agent_turn_id IS
'When requester_kind=''agent'', the paliadin_turns row the suggestion '
'came from. NULL otherwise. ON DELETE SET NULL so the audit record '
'survives if the turn row is later purged.';
-- ============================================================================
-- 2. paliad.paliadin_turns.context — structured page payload
-- ============================================================================
ALTER TABLE paliad.paliadin_turns
ADD COLUMN IF NOT EXISTS context jsonb;
COMMENT ON COLUMN paliad.paliadin_turns.context IS
'Structured page-context payload from the inline widget: route_name + '
'primary_entity_type + primary_entity_id + user_selection_text + '
'view_mode + filter_summary. NULL for legacy turns (PoC standalone '
'page predates the structured payload — page_origin is the only field '
'they carry). Design: docs/design-paliadin-inline-2026-05-08.md §4.';