Files
pikasTech-HWLAB/internal/db/migrations/0008_workbench_kafka_realtime.sql
T

107 lines
4.3 KiB
PL/PgSQL

-- Versioned v8 upgrade for immutable Workbench replay and the AgentRun Kafka projector.
ALTER TABLE workbench_projection_outbox ADD COLUMN IF NOT EXISTS outbox_event_id TEXT;
ALTER TABLE workbench_projection_outbox ADD COLUMN IF NOT EXISTS entity_family TEXT;
ALTER TABLE workbench_projection_outbox ADD COLUMN IF NOT EXISTS entity_id TEXT;
-- Pre-v8 rows did not store immutable fact payloads. Mark them as cutover rows so
-- recovery advances its cursor without fabricating message/turn/trace entities.
UPDATE workbench_projection_outbox
SET outbox_event_id = COALESCE(outbox_event_id, 'legacy:' || outbox_seq::text),
entity_family = COALESCE(entity_family, 'legacy'),
entity_id = COALESCE(entity_id, 'legacy:' || outbox_seq::text)
WHERE outbox_event_id IS NULL OR entity_family IS NULL OR entity_id IS NULL;
ALTER TABLE workbench_projection_outbox ALTER COLUMN outbox_event_id SET NOT NULL;
ALTER TABLE workbench_projection_outbox ALTER COLUMN entity_family SET NOT NULL;
ALTER TABLE workbench_projection_outbox ALTER COLUMN entity_id SET NOT NULL;
DROP INDEX IF EXISTS idx_workbench_projection_outbox_event_id;
CREATE UNIQUE INDEX idx_workbench_projection_outbox_event_id ON workbench_projection_outbox(outbox_event_id);
CREATE TABLE IF NOT EXISTS workbench_kafka_inbox (
source_topic TEXT NOT NULL,
source_partition INTEGER NOT NULL,
source_offset BIGINT NOT NULL,
source_key TEXT,
source_event_id TEXT,
input_sha256 TEXT NOT NULL,
trace_id TEXT,
session_id TEXT,
run_id TEXT,
command_id TEXT,
source_seq INTEGER NOT NULL DEFAULT 0,
status TEXT NOT NULL DEFAULT 'processing',
projected_seq INTEGER,
envelope_json TEXT NOT NULL DEFAULT '{}',
error_json TEXT NOT NULL DEFAULT '{}',
created_at TEXT NOT NULL,
updated_at TEXT NOT NULL,
projected_at TEXT,
PRIMARY KEY (source_topic, source_partition, source_offset)
);
CREATE UNIQUE INDEX IF NOT EXISTS idx_workbench_kafka_inbox_source_event ON workbench_kafka_inbox(source_topic, source_event_id) WHERE source_event_id IS NOT NULL;
CREATE INDEX IF NOT EXISTS idx_workbench_kafka_inbox_status_updated ON workbench_kafka_inbox(status, updated_at, source_topic, source_partition, source_offset);
CREATE INDEX IF NOT EXISTS idx_workbench_kafka_inbox_trace ON workbench_kafka_inbox(trace_id, source_seq);
CREATE TABLE IF NOT EXISTS workbench_kafka_dlq (
dlq_seq BIGSERIAL PRIMARY KEY,
source_topic TEXT NOT NULL,
source_partition INTEGER NOT NULL,
source_offset BIGINT NOT NULL,
source_event_id TEXT,
input_sha256 TEXT NOT NULL,
error_code TEXT NOT NULL,
error_message TEXT NOT NULL,
envelope_json TEXT NOT NULL DEFAULT '{}',
created_at TEXT NOT NULL
);
CREATE UNIQUE INDEX IF NOT EXISTS idx_workbench_kafka_dlq_transport ON workbench_kafka_dlq(source_topic, source_partition, source_offset);
CREATE TABLE IF NOT EXISTS hwlab_kafka_outbox (
outbox_seq BIGSERIAL PRIMARY KEY,
event_id TEXT NOT NULL UNIQUE,
topic TEXT NOT NULL,
partition_key TEXT NOT NULL,
payload_json TEXT NOT NULL,
headers_json TEXT NOT NULL DEFAULT '{}',
attempt_count INTEGER NOT NULL DEFAULT 0,
next_attempt_at TEXT NOT NULL,
lease_owner TEXT,
lease_expires_at TEXT,
published_at TEXT,
last_error TEXT,
created_at TEXT NOT NULL,
updated_at TEXT NOT NULL
);
CREATE INDEX IF NOT EXISTS idx_hwlab_kafka_outbox_due ON hwlab_kafka_outbox(published_at, next_attempt_at, lease_expires_at, outbox_seq);
CREATE OR REPLACE FUNCTION notify_hwlab_workbench_projection() RETURNS trigger AS $$
BEGIN
PERFORM pg_notify(
'hwlab_workbench_projection',
json_build_object(
'sessionId', NEW.session_id,
'traceId', NEW.trace_id,
'outboxSeq', NEW.outbox_seq
)::text
);
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
DROP TRIGGER IF EXISTS trg_notify_hwlab_workbench_projection ON workbench_projection_outbox;
CREATE TRIGGER trg_notify_hwlab_workbench_projection
AFTER INSERT ON workbench_projection_outbox
FOR EACH ROW EXECUTE FUNCTION notify_hwlab_workbench_projection();
INSERT INTO hwlab_schema_migrations (id, schema_version, applied_at, migration_json)
VALUES (
'0008_workbench_kafka_realtime',
'runtime-durable-postgres-v8',
CURRENT_TIMESTAMP,
'{"path":"internal/db/migrations/0008_workbench_kafka_realtime.sql","runtime":"cloud-api"}'
)
ON CONFLICT (id) DO UPDATE SET
schema_version = EXCLUDED.schema_version,
migration_json = EXCLUDED.migration_json;