mirror of
https://gh-proxy.org/https://github.com/klinsmaya/multica.git
synced 2026-11-09 01:46:46 +08:00
99 lines
4.8 KiB
SQL
99 lines
4.8 KiB
SQL
-- name: CreateTaskMessage :one
|
|
INSERT INTO task_message (id, task_id, seq, type, tool, content, input, output, output_truncated, call_id)
|
|
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10)
|
|
RETURNING *;
|
|
|
|
-- name: CreateTaskMessages :many
|
|
-- Batch variant of CreateTaskMessage: persists a whole daemon-reported batch in
|
|
-- ONE statement — therefore one round trip and, more importantly, one commit
|
|
-- instead of one per message. Commit acknowledgement (IO:XactSync) is ~94% of
|
|
-- this SQL's load in production, so the commit count is the thing being
|
|
-- optimized; the round trip is a bonus.
|
|
--
|
|
-- The rows arrive as parallel arrays rather than as one jsonb document, even
|
|
-- though a jsonb document is the tidier Go side. content and output routinely
|
|
-- carry tens of KB and occasionally megabytes, and wrapping them in JSON makes
|
|
-- the server escape every byte a second time and Postgres parse the whole
|
|
-- envelope back out — measured at 1.2x the old per-row insert at 128KB and
|
|
-- ~1.8x at 1MB, i.e. a regression on exactly the most expensive requests, which
|
|
-- are also the ones least likely to be batched. Native text[] elements are
|
|
-- length-prefixed by the wire protocol, so they cost neither pass.
|
|
--
|
|
-- NULLIF is what makes per-row NULL expressible through a non-nullable []string
|
|
-- (the Go type sqlc gives a text[] parameter): it reproduces, exactly, the
|
|
-- `pgtype.Text{Valid: x != ""}` mapping the single-row query carries — empty
|
|
-- string means SQL NULL. input is passed as text and cast here for the same
|
|
-- reason; it is the one column that genuinely has to be parsed as JSON, because
|
|
-- it is a jsonb column. created_at also rides this transport so an older daemon
|
|
-- can omit its event time and keep the database-time fallback. output_truncated
|
|
-- uses the same trick because it is a genuinely tri-state boolean (NULL = the
|
|
-- reporting daemon did not measure it), and a bool[] parameter becomes a Go
|
|
-- []bool, which has no way to spell the third state.
|
|
--
|
|
-- Callers MUST still run the Postgres text sanitizer first. A NUL anywhere in
|
|
-- the batch fails the whole statement (GH #7098) — that is inherent to batching
|
|
-- into one statement, not to the parameter shape.
|
|
--
|
|
-- Atomicity is a deliberate side effect, not just a speedup: the per-message
|
|
-- loop this replaces could persist part of a batch and then fail, leaving the
|
|
-- transcript with a prefix of the batch and no way to complete it — the daemon
|
|
-- does not retry this endpoint. One statement makes the batch all-or-nothing,
|
|
-- which buys consistency; a batch that fails is still lost whole, so closing
|
|
-- the gap for real needs a retry plus a (task_id, seq) uniqueness rule.
|
|
--
|
|
-- The ORDER BY is a contract, not decoration. A bare `INSERT ... RETURNING`
|
|
-- has no defined row order, and the caller republishes these rows as realtime
|
|
-- events in the order they arrive — the per-row loop this replaces implicitly
|
|
-- published in request order, so the ordering has to be restored explicitly or
|
|
-- subscribers can see a batch out of order. seq is assigned by the daemon and
|
|
-- increases within a batch, so it is the request order.
|
|
WITH incoming AS (
|
|
-- Several single-argument unnest calls in one SELECT list expand in
|
|
-- lockstep (PostgreSQL 10+ set-returning-function semantics), which is the
|
|
-- same row-wise zip the multi-argument unnest(a, b, ...) form gives — but
|
|
-- sqlc's analyzer only knows the single-argument signature, so this is the
|
|
-- shape that survives code generation.
|
|
SELECT
|
|
unnest(sqlc.arg('ids')::uuid[]) AS id,
|
|
unnest(sqlc.arg('seqs')::int4[]) AS seq,
|
|
unnest(sqlc.arg('types')::text[]) AS type,
|
|
unnest(sqlc.arg('tools')::text[]) AS tool,
|
|
unnest(sqlc.arg('call_ids')::text[]) AS call_id,
|
|
unnest(sqlc.arg('contents')::text[]) AS content,
|
|
unnest(sqlc.arg('inputs')::text[]) AS input,
|
|
unnest(sqlc.arg('outputs')::text[]) AS output,
|
|
unnest(sqlc.arg('created_ats')::text[]) AS created_at,
|
|
unnest(sqlc.arg('output_truncations')::text[]) AS output_truncated
|
|
), inserted AS (
|
|
INSERT INTO task_message (id, task_id, seq, type, tool, content, input, output, created_at, output_truncated, call_id)
|
|
SELECT
|
|
m.id,
|
|
sqlc.arg('task_id')::uuid,
|
|
m.seq,
|
|
m.type,
|
|
NULLIF(m.tool, ''),
|
|
NULLIF(m.content, ''),
|
|
NULLIF(m.input, '')::jsonb,
|
|
NULLIF(m.output, ''),
|
|
COALESCE(NULLIF(m.created_at, '')::timestamptz, now()),
|
|
NULLIF(m.output_truncated, '')::bool,
|
|
NULLIF(m.call_id, '')
|
|
FROM incoming AS m
|
|
RETURNING *
|
|
)
|
|
SELECT * FROM inserted ORDER BY seq ASC;
|
|
|
|
-- name: ListTaskMessages :many
|
|
SELECT * FROM task_message
|
|
WHERE task_id = $1
|
|
ORDER BY seq ASC;
|
|
|
|
-- name: ListTaskMessagesSince :many
|
|
SELECT * FROM task_message
|
|
WHERE task_id = $1 AND seq > $2
|
|
ORDER BY seq ASC;
|
|
|
|
-- name: DeleteTaskMessages :exec
|
|
DELETE FROM task_message
|
|
WHERE task_id = $1;
|