Structured SQL errors
Every /v1/query and /v1/batch failure ships a structured
errorDetail next to the human-readable error string. Agents can
branch on kind and self-correct without parsing the message.
interface SqlErrorDetail { kind: | "unique_violation" | "not_null_violation" | "fk_violation" | "check_violation" | "unknown_table" | "unknown_column" | "syntax_error" | "type_mismatch" | "database_locked" | "too_many_params" | "readonly" | "unknown"; message: string; table?: string; column?: string; columns?: Array<{ table: string; column: string }>; constraint?: string; near?: string; hint?: string;}hint is verb-aware. A unique_violation on INSERT suggests
ON CONFLICT DO UPDATE; on UPDATE it just states the conflict.
PerSQLError.detail carries the envelope:
import { PerSQL, PerSQLError } from "@persql/sdk";
try { await db.query("INSERT INTO users(email) VALUES (?)", ["taken@x.com"]);} catch (e) { if (e instanceof PerSQLError && e.detail?.kind === "unique_violation") { // Switch to UPDATE without re-prompting the model. await db.query("UPDATE users SET ... WHERE email = ?", ["taken@x.com"]); }}The query and batch tools return JSON-shaped errors when an
envelope is available:
{ "error": "UNIQUE constraint failed: users.email", "errorDetail": { "kind": "unique_violation", "table": "users", "column": "email", "hint": "Row with this key already exists — INSERT ... ON CONFLICT DO UPDATE or use UPDATE." }}Batch attribution
Section titled “Batch attribution”A batch failure is reported by SQLite without saying which statement broke. The server picks a culprit by matching the table referenced in the error against the statements in the batch and attaches the envelope to that one. If no match, it falls back to the first mutating statement.