Protocol Reference

Every stdin line is one JSON object tagged by a required code. Every emitted line is an Agent-First Data envelope tagged by a top-level kind; see Output for the envelope shape.

Interface Boundary

This protocol is the only runtime interface.

Agent-facing reliability guarantees:

afpsql-readonly capability boundary

afpsql-readonly is a separately installed database-write guard. It shares the read/query, inspect, file/config, and typed transport implementation with afpsql. It is not a replacement for PostgreSQL role authorization and is not a general local-process sandbox.

Pathreadonly behavior
direct / SSH / container read permissionallowed
write, ssh-write, container-writeinvalid_request
pipe begin read-onlyallowed
pipe read-write begin or write queryinvalid_request
--mode psql, psql status/install/uninstallinvalid_request; use afpsql psql ...
skill status/install/uninstallallowed on the ordinary entrypoint
--stdout-file, --stderr-file, local --sql-fileallowed on the ordinary entrypoint
arbitrary explicit --*-secret-env NAMEallowed on the ordinary entrypoint
--*-secret-config FILE --*-secret-config-path DOT_PATHallowed on the ordinary entrypoint
SSH options and custom container runtimeallowed on the ordinary entrypoint
transaction control sent as query SQLinvalid_request; use typed pipe transaction requests

Every readonly rejection is a structured error event with error.code: "invalid_request" and a hint directing write work to afpsql. The client transaction is a safety belt, not an adversarial SQL sandbox. A whitelist for the ordinary executable grants caller-selected local file and env reads, process-spawning transport options, network access to caller-selected direct/SSH/container targets, and every row visible to the database role. For a server-enforced baseline, provision a non-owner login role:

CREATE ROLE app_reader LOGIN
  NOSUPERUSER NOCREATEDB NOCREATEROLE NOREPLICATION NOBYPASSRLS;
GRANT CONNECT ON DATABASE app TO app_reader;
GRANT USAGE ON SCHEMA app TO app_reader;
GRANT SELECT ON ALL TABLES IN SCHEMA app TO app_reader;
ALTER DEFAULT PRIVILEGES FOR ROLE app_owner IN SCHEMA app
  GRANT SELECT ON TABLES TO app_reader;

Keep the role out of writer/owner memberships and audit PUBLIC, function EXECUTE, SECURITY DEFINER, extensions, predefined roles, sequences, and RLS visibility separately.

Output redaction covers connection, configuration and control metadata only. It is not a guard on what a query returns: rows, column values, SQL text and the parameters shown to a human approver are all emitted exactly as the database gave them, including a column named api_key_secret. Redacting them would make afpsql report something other than what is in the database, which is the one thing this tool must never do — so a caller that must not see a value has to avoid selecting it. Redaction of connection metadata also does not mean connection secrets never leave the process.

PostgreSQL READ ONLY rejects temporary-object creation, sequence nextval, and writes performed inside SECURITY DEFINER functions. PostgreSQL still permits operations including NOTIFY and transaction-level advisory locks; constrain those through role/function audit, timeouts, concurrency limits, and lock/resource governance. Readonly rejects transaction-control SQL before execution because PostgreSQL can accept nested BEGIN/COMMIT; pipe clients must use the typed begin, commit, and rollback requests so afpsql owns transaction state.

Administrator-locked readonly profiles

On Unix, an executable basename afpsql-readonly-NAME selects exactly /etc/afpsql/readonly-profiles/NAME.json. NAME is limited to ASCII letters, digits, -, and _; the profile must be a regular root-owned file, no larger than 64 KiB, and not writable by group or others. For example:

{"host":"db.internal","port":5432,"user":"app_reader","dbname":"app","password_secret":"..."}

The file uses the flat session fields documented for pipe config. Because this is administrator-controlled configuration, it may contain custom container runtimes or SSH options such as IdentityFile/ProxyCommand. A locked profile executable rejects all connection/transport flags and all pipe sessions patches before they can replace the profile.

A locked profile is also the whole connection: it reads none of the connection environment variables that the ordinary readonly executable falls back to — AFPSQL_DSN_SECRET, AFPSQL_CONNINFO_SECRET, AFPSQL_HOST/PGHOST, AFPSQL_PORT/PGPORT, AFPSQL_USER/PGUSER, AFPSQL_DBNAME/PGDATABASE, AFPSQL_PASSWORD_SECRET/PGPASSWORD, and PGSSLMODE. Otherwise a caller who may only choose SQL could still move the endpoint, or downgrade its TLS, through the environment. Put every connection field the profile needs — including the password and any sslmode — in the profile file itself; use dsn_secret there when you need sslmode. Hosts needing a single target should authorize this distinct executable name, not infer a target by parsing shell arguments.

Locked profiles currently require Unix owner/mode checks. On other platforms, use OS-native ACLs plus an equivalent wrapper policy; the generic readonly executable continues to allow caller-selected targets as documented above.

Connection secret sources

Each secret slot has three mutually exclusive explicit sources:

SlotDirectEnvironmentConfig file
DSN URI--dsn--dsn env:NAME--dsn file:FILE#DOT_PATH
libpq conninfo--conninfo--conninfo env:NAME--conninfo file:FILE#DOT_PATH
password--password--password env:NAME--password file:FILE#DOT_PATH

Config files may be JSON (.json), TOML (.toml), YAML (.yaml/.yml), dotenv (.env, .env.*, *.env), or INI (.ini). A config source is one typed argument containing both the file and dot path.

The grammar is AFDATA’s, declared on each of the three arguments, so the canonical registry refuses a malformed or unaccepted source while it resolves argv — cli_invalid_argument_value at exit 2, before any file is opened — and the psql-compatible translator, which parses its own argv, refuses the same set through the same parser. Stream sources (stdin, fd:N, prompt) are deliberately not accepted here: afpsql reads SQL on stdin, and blocking on a terminal is a hang for an agent. The path must resolve to a non-empty string; the value is returned verbatim — never trimmed, coerced from another type, or URL-decoded, so percent-encode reserved characters in a DSN (%40 for @). A dot path resolving to a number or a boolean is refused rather than coerced: a credential is text. Note that a double-quoted dotenv value or a TOML basic "..." string still undergoes that format’s own escape processing (\t→tab, \\\); use an unquoted or single-quoted '...' value for raw bytes.

The file is read once during startup, before database or transport work. An explicit config source overrides AFPSQL_* and libpq PG* fallbacks for that slot. Pipe reconnects reuse the resolved value and do not observe file changes. Pipe config requests cannot submit file/path references in protocol v1.

Output::Config serializes configured dsn_secret, conninfo_secret, and password_secret as *** for JSON, YAML, and plain output regardless of whether the original source was direct, env, or config. Startup logs may report safe source metadata (kind and, for config, file/dot-path), never the resolved value.

Input (stdin)

query

Execute one SQL statement.

FieldRequiredDescription
codeyes"query"
idyesclient correlation id
sessionnosession id; default session if omitted
sqlyesSQL text
paramsnopositional bind values
optionsnoquery behavior

options fields:

FieldDefaultDescription
stream_rowsfalsestream rows as result_rows events
batch_rows1000max rows per result_rows event
batch_bytes262144soft byte target per streamed batch
statement_timeout_msconfig defaultper-query statement timeout
lock_timeout_msconfig defaultper-query lock timeout
permissionnative/pipe transport defaultread, write, ssh-read, ssh-write, container-read, or container-write
inline_max_rowsconfig defaultinline row cap for non-streaming
inline_max_bytesconfig defaultinline payload bytes cap for non-streaming

In native CLI and pipe mode, permission defaults to read for direct sessions, ssh-read for sessions using afpsql SSH transport, and container-read for sessions using afpsql container transport. Read permissions run in PostgreSQL read-only transactions. Direct writes require write; SSH writes require ssh-write; container writes require container-write.

Parameter Binding Rules

  1. Dynamic values should be passed via params with $1..$N placeholders.
  2. Placeholder count must equal params length (validated from prepared-statement metadata, not SQL text scanning).
  3. Client-side count/shape/local binding conversion failures return error.code: "invalid_params".
  4. PostgreSQL server conversion/execution failures return code: "sql_error" with the original SQLSTATE.

Driver-side type mapping (prepared statement parameter OIDs):

Unsupported:

CLI mapping notes:

config

Partial runtime config update. Echoes full config afterward.

FieldRequiredDescription
codeyes"config"
default_sessionnodefault session name
sessionsnosession connection definitions
inline_max_rowsnoglobal inline row limit
inline_max_bytesnoglobal inline payload bytes limit
statement_timeout_msnoglobal statement timeout
lock_timeout_msnoglobal lock timeout
lognoenabled log categories

Session connection shape supports:

session_info is ordered through the named session’s FIFO. While an explicit transaction is open it reports configured defaults without probing PostgreSQL, so identity fields may be null; this avoids adding savepoints or otherwise mutating the caller’s transaction.

Supported TLS settings supplied in dsn_secret or conninfo_secret are honored. afpsql currently accepts sslmode=disable/prefer/require; unsupported libpq TLS modes/options such as verify-ca, verify-full, sslrootcert, sslcert, and sslkey fail with structured errors and hints.

SSH transport fields start an OpenSSH stdio bridge before connecting. SSH accepts dsn_secret, conninfo_secret, or discrete connection fields. afpsql parses DSN/conninfo locally, interprets its PostgreSQL endpoint from the final SSH host, and preserves authentication, database, startup-option, and supported TLS settings when connecting through the bridge. The source must resolve to one PostgreSQL host and port; multi-host failover lists return a structured non-retryable connection error.

The bridge is carried over the ssh child process’s stdin/stdout, so afpsql opens no local listening port and no other process on the workstation can reach the connection. In exchange, the host running the bridge needs sh plus one of python3, python, or perl — any one is enough, and they are tried in that order. A host with none of them fails with exit 127 and a message naming all three. For --ssh-via chains the bridge runs on the final --ssh host, and only that host needs an interpreter.

Container transport fields start a no-TTY exec bridge through one driver (apple, docker, podman, nerdctl, compose, or kubectl) and run a small stdio bridge inside the container. The apple family invokes the Apple container CLI. The PostgreSQL host/port or Unix socket is interpreted inside the container. Container transport can use dsn_secret, conninfo_secret, or discrete connection fields and likewise requires one PostgreSQL endpoint.

The driver is not named separately: it is the family of fields used. Each field is scoped to one driver by its own name, so a driver cannot be asked for an option it does not have — there is no kubectl_user because kubectl exec has no exec-as-user option, and no podman_context because Podman selects no context. kubectl_container picks one container in a multi-container pod and is emitted as -c CTR before --. AFPSQL_CONTAINER_COMPOSE_FILE may supply colon-separated Compose files when no compose_files are configured.

Fields from two families cannot be combined: two families name two drivers, so the session is rejected with an error naming both offending fields.

When a driver family is combined with ssh, afpsql uses SSH to run the container exec command on the remote host, then bridges from inside the container. In this combined mode, only ssh and ssh_options apply; SSH socket and sudo bridge fields are for non-container SSH transport. The permission family remains container (container-read / container-write).

CLI translation notes:

cancel

Cancel a queued or running query by id.

{"code":"cancel","id":"q-123"}

When the database connection is already executing the query, afpsql sends a PostgreSQL server-side cancel request. When the query is still queued, afpsql removes it before execution. Cancellation is still race-prone: a query may finish normally before the cancel request is processed.

ping

Health check.

{"code":"ping"}

close

Graceful shutdown.

{"code":"close"}

session_info

Pipe-mode introspection request. Returns the named session’s resolved transport, permission default, and runtime limits so an agent can discover what it is connected to without probing via failing queries.

FieldRequiredDescription
codeyes"session_info"
idnoclient correlation id
sessionnosession id; default session if omitted

Unknown session names return kind:"error" with error.code:"invalid_request" and a hint pointing to config.

begin / commit / rollback

Pipe-mode explicit transactions. Without these, every query is wrapped in its own implicit BEGIN..COMMIT, so multi-statement atomicity requires jamming everything into one SQL string. After begin, subsequent query events on the same session run inside the open transaction until a matching commit or rollback.

{"code":"begin","id":"b1","session":"default","read_only":false,"permission":"write"}
{"code":"commit","id":"c1","session":"default"}
{"code":"rollback","id":"rb1","session":"default"}
FieldRequiredDescription
codeyes"begin", "commit", or "rollback"
idnoclient correlation id, echoed on the response
sessionnosession id; default session if omitted
read_onlyno, begin onlydefaults to true; set explicitly to false for a read-write transaction
permissionno, begin onlyrequired when read_only:false; use the matching write / ssh-write / container-write permission

The response is a code:"result" event with command_tag set to "BEGIN", "COMMIT", or "ROLLBACK". Failures (e.g. begin while already in a tx, commit with no open tx, or PostgreSQL errors) surface as error or sql_error.

Per-query failures inside an explicit transaction are wrapped in a savepoint and rolled back individually, so the user’s outer transaction is NOT aborted by a single bad query — the agent can retry or move on without losing prior progress. Send rollback to discard the whole transaction or commit to persist the work done so far.

Every query inside a read-write explicit transaction must repeat the matching write permission. A query that declares a read permission is rejected before SQL execution. A read-only explicit transaction remains enforced by PostgreSQL, including when a query supplies a write permission.

Tx control runs through the same session FIFO as query, so the order an agent writes events to stdin is the order PostgreSQL sees them.

Output

Every output line is an Agent-First Data envelope: a top-level kind with the event payload nested under the matching key, and trace as a top-level sibling. The selected --output-to policy determines which process stream carries it.

{"kind": "result", "result": { ...payload... }, "trace": { ...timing... }}

kind is one of result, progress, error, or log. The business event name is the payload’s own code field (except log, which drops code and adds timestamp_epoch_ms). The per-event tables below list payload fields — the object nested under the envelope key — so code/id/columns/… arrive as result.code/result.id/… on the wire. Each table also lists trace for reference, but it is the top-level sibling shown above, not nested in the payload.

Event (payload.code)Envelope kindPayload key
result, result_end, dry_run, session_info, config, pong, closeresultresult
result_start, result_rowsprogressprogress
sql_error, errorerrorerror
logloglog

result

Small result returned inline.

FieldDescription
code"result"
idquery id
sessionsession used
command_tagNormalized command tag (ROWS N / EXECUTE N)
columnscolumn metadata array
rowsresult rows
row_countrow count actually emitted (the prefix size when truncated)
truncatedoptional; true when rows is a prefix of the full result
truncated_at_rowsoptional; inline row cap that fired
truncated_at_bytesoptional; inline byte cap that fired
tracetiming and counters

When truncated: true, the underlying SQL still executed in full. For UPDATE ... RETURNING, this means the writes happened and the RETURNING projection delivered to the agent is the first N rows. To collect the full result, narrow the query with WHERE or switch to --stream-rows.

Bounded row results are produced through a CTE/JSON wrapper. PostgreSQL does not guarantee that an outer read preserves ordering established only inside that CTE, so callers must not treat the returned prefix as a contractual ordered prefix—even when the submitted query contains ORDER BY. If exact ordering is part of correctness, return an explicit ordinal/key and sort or verify the rows in the caller.

result_start

Start of streamed result.

FieldDescription
code"result_start"
idquery id
sessionsession used
columnscolumn metadata

result_rows

One streamed row batch.

FieldDescription
code"result_rows"
idquery id
rowsrow objects for this batch
rows_batch_countrows in batch

result_end

End of streamed result.

FieldDescription
code"result_end"
idquery id
sessionsession used
command_tagNormalized command tag (ROWS N / EXECUTE N)
traceincludes duration_ms, row_count, payload_bytes

dry_run

Emitted instead of executing the SQL when --dry-run is passed. The server prepares the statement inside a transaction that is rolled back, so this also validates table/column existence and placeholder counts without side effects.

FieldDescription
code"dry_run"
idoptional client correlation id
sqlthe SQL that would have been executed
paramsthe params that would have been bound, in JSON-encoded form
sessionsession that would have been used
param_typesinferred PostgreSQL types for $1, $2, … in placeholder order
columnsoutput column metadata (empty for non-SELECT statements)
tracetiming and counters

If preparation fails, afpsql emits sql_error (PostgreSQL diagnostic) or error (placeholder-count mismatch / connect failure) with the same shape as a normal query, and exits non-zero.

sql_error

Database execution error.

FieldDescription
code"sql_error"
idquery id
sessionsession used
sqlstateSQLSTATE (23505, 42P01, …)
messageprimary error message
detailoptional detail
hintoptional hint
positionoptional SQL character position
tracetiming and counters

error

Client/runtime/protocol error.

FieldDescription
codemachine-readable code
messagehuman-readable detail
sqlstateoptional SQLSTATE when PostgreSQL rejects connection setup
detailoptional PostgreSQL detail for connection setup failures
hintoptional remediation hint
retryablewhether retry may succeed
tracetiming and counters (top-level sibling)

Canonical error.code values:

internal_error means the failure is afpsql’s own, not the request’s and not PostgreSQL’s; the hint asks the caller to retry and then restart the session. Treat it as a bug report, not as something to work around.

An invocation rejected before anything ran carries the parser’s own classification instead, always on stderr and always exiting 2: cli_unknown_command, cli_unknown_argument, cli_unregistered_combination, cli_missing_argument_value, cli_invalid_argument_value, cli_duplicate_argument, cli_unexpected_positional, cli_invalid_utf8.

For connection setup failures, kind remains "error" and error.code remains "connect_failed". If PostgreSQL returns a server diagnostic during startup (for example password auth failure, missing role/database, too many connections, or cannot-connect-now), afpsql also includes sqlstate plus PostgreSQL diagnostic fields and a SQLSTATE-specific hint.

session_info

Response to a session_info request.

FieldDescription
code"session_info"
idoptional client correlation id
sessionresolved session name
transport_kind"direct", "ssh", or "container"
permission_defaulttransport-default permission ("read", "ssh-read", or "container-read")
stream_rows_defaultsession’s default stream_rows value
batch_rowsresolved batch_rows default
batch_bytesresolved batch_bytes default
inline_max_rowsresolved inline row cap
inline_max_bytesresolved inline payload byte cap
statement_timeout_msresolved statement timeout
lock_timeout_msresolved lock timeout
databaseoptional PostgreSQL database name (from probe or config)
useroptional PostgreSQL role (from probe or config)
hostoptional server host (from probe or config)
portoptional server port (from probe or config)
server_versionoptional PostgreSQL server version (from probe)
tracetiming and counters

If the probe SELECT succeeds during session_info, database/user/host/ port/server_version reflect what the PostgreSQL server itself reports. If the probe fails (typically because connection setup itself fails), the fields fall back to the resolved session config and server_version is omitted. Probe failures do not cause session_info to error.

Other output codes

codeMeaning
configfull runtime config echo
pongping response with counters
closeshutdown acknowledgement
logoptional runtime diagnostic event (enabled by log config/categories)

log event fields:

Startup log events include version, parsed/summarized args, and selected environment fallback presence metadata (key plus present). They intentionally omit raw argv, raw environment values, and config snapshots. Bind values are summarized as param_count, not logged as plaintext.

log category matching (from config.log / --log):

transport.selected is emitted once when a new session connection is opened and the transport log category (or all / *) is enabled. Its chain summarizes the selected boundary, for example ssh:user@server -> docker exec pg -> tcp 127.0.0.1:5432.

mode.permission_default_changed is emitted under the mode log category whenever --mode psql bypasses the native read-only default, so agents can see when psql-compat translation has dropped the write boundary.

connect.libpq_env_fallback is emitted under the connect log category when libpq PG* environment variables (PGHOST, PGPORT, PGUSER, PGDATABASE, PGPASSWORD, PGSSLMODE) fill connection fields that were not provided via flags or secrets, listing which variables were used.

Panel delivery (--mode)

Every afpsql ui panel — schema, table, indexes, connections, plan — takes an optional --mode window|session. window opens an isolated browser window on this machine; session registers the panel only, for a person who is not here to reach with afui session serve. Priority when --mode is absent is AFUI_DELIVERY (also window or session), then window — AFUI’s own UiDeliveryPlan::resolve rule, applied here rather than reimplemented. An AFUI_DELIVERY value neither command recognizes is rejected, listing the legal words, rather than silently defaulting to a window nobody is there to see.

--mode on a panel is a different axis from afpsql --mode cli|pipe|psql: the two never appear on the same command line, since ui has no runtime mode of its own, and each is scoped to its own command in the registry.

The kit’s third delivery, link — a URL another device can open directly, with no afui binary involved in reaching it — is not offered here, and does not appear in a panel’s --help. link binds every interface and advertises the result on the network with no tunnel; for rows read out of a database that is a wider, unencrypted exit than --stdout-file, which an administrator-locked readonly profile already refuses (see above) for the same reason it refuses every panel outright, regardless of --mode. window never leaves this screen; session stays on loopback and is reached only through afui’s own registry, so neither carries that exposure.

The ui_ready progress event every panel emits carries AFUI’s own delivery facts beside the panel’s: mode, naming the delivery with the same word --mode and AFUI_DELIVERY accept, and session_id, the identity that panel is listed under in afui session list. Which delivery reached a person changes nothing about what ui plan accepts as an answer: only Completed(Approve) — pressing the approve control — runs the statement, and a closed window, a remotely-refused session, and a lapsed credential are all still a refusal. The ending field on a ui plan result reports approved or refused for a decision and AFUI’s own closed or expired for every other way a panel stops.

ui connections renders one opening snapshot before registration so a broken database connection fails on the agent’s stream. After that, periodic queries are observation-driven: a visible page renews a short lease over the existing AFUI runtime, and polling stops within 30 seconds after every page is gone or hidden. The retained snapshot remains available, and polling resumes when a page becomes visible again. These runtime calls do not count as AFUI attention and do not extend a remote page’s lifetime.

Replacing a panel (afui frontend)

Every afpsql ui panel is a MiniJinja template rendered against a typed document, and any of them can be replaced. AFUI owns where an override lives and whether it is trusted; afpsql owns what the files mean.

ui_kindPanel
schema_inspectafpsql ui schema
table_inspectafpsql ui table
index_inspectafpsql ui indexes
connection_monitorafpsql ui connections
plan_confirmafpsql ui plan

Install one with afui frontend init --provider-id afpsql --ui-kind <KIND> and turn it on with afui frontend enable. ui_api_version is 4.

Files an override may supply, each independently — a file it does not supply comes from afpsql, so replacing one page keeps the rest:

PathWhat it is
templates/page.html.j2The panel body for this ui_kind
templates/layout.html.j2The frame every page extends
templates/table.html.j2The result table partial
templates/relation.html.j2One relation’s card, columns, constraints, indexes and triggers
templates/decided.html.j2What plan_confirm shows after an answer
templates/<path>.j2An additional partial, including nested paths, discovered at runtime and available by its full path
style.cssThe stylesheet
assets/**Stylesheets, images and fonts, served from the session origin

A replacement page.html.j2 or decided.html.j2 that wants afpsql’s frame extends it by the same path a person sees in their own frontend directory: {% extends "templates/layout.html.j2" %}, and likewise {% include "templates/table.html.j2" %} for the result table or {% include "templates/relation.html.j2" %} for whichever document.relations entry is in scope. Including one you have not supplied gets afpsql’s own.

Templates render against document, which carries everything the panel worked out — already counted, sorted, classified and formatted. Reorder it, regroup it, drop what you do not want; you cannot arrive at a different answer than the one the agent read, because the panel and afpsql inspect run the same SQL.

The three inspection panels run one query that returns eight kinds of object unioned into one set of generic columns — the shape afpsql inspect prints, and the shape an agent wants. What their documents carry is that result already read back apart: document.relations is one entry per relation with its columns, constraints, indexes and triggers attached, its facts worded (owner, size, a row estimate that says “never analyzed” rather than -1), and its catalog codes translated into the tags beside each entry. Constraint rows that only restate a column’s own not null are not carried, because every column already answers that. document.table is still the untouched result set, so a page can show both.

Three things an override cannot do, and they are enforced rather than requested:

A frontend afpsql cannot load is an error naming safe mode (ui_frontend_incompatible, ui_frontend_unreadable, ui_frontend_unsafe, ui_frontend_template, ui_frontend_incomplete), never a quietly substituted built-in page: no window opens and nothing runs. AFUI_SAFE_MODE=1 ignores every override. A workspace frontend that has not been enabled is skipped in silence by design — the ui_ready progress event carries ui_frontend_id only when an override is actually serving.

Runtime Safety Limits

Pipe mode applies hard protocol limits before executing a request:

Environment Fallback

Optional runtime fallback variables:

Variables from two driver families are rejected the same way flags from two families are: they name two drivers.

Administrator-locked readonly profiles ignore every connection, SSH, and container environment fallback.

Standard PostgreSQL environment fallback (lower precedence):

Example: Small Result

Input:

{"code":"query","id":"q1","sql":"select 1 as n"}

Output:

{"kind":"result","result":{"code":"result","id":"q1","command_tag":"ROWS 1","columns":[{"name":"n","type":"int4"}],"rows":[{"n":1}],"row_count":1},"trace":{"duration_ms":2}}

Example: Streamed Result

Input:

{"code":"query","id":"q2","sql":"select * from big_table where id > $1","params":[100],"options":{"stream_rows":true,"batch_rows":1000}}

Output:

{"kind":"progress","progress":{"code":"result_start","id":"q2","columns":[{"name":"id","type":"int8"},{"name":"name","type":"text"}],"message":"query result stream started"},"trace":{}}
{"kind":"progress","progress":{"code":"result_rows","id":"q2","rows":[{"id":101,"name":"a"},{"id":102,"name":"b"}],"rows_batch_count":2,"message":"query result rows"},"trace":{}}
{"kind":"result","result":{"code":"result_end","id":"q2","command_tag":"ROWS 200000"},"trace":{"duration_ms":443,"row_count":200000,"payload_bytes":34199211}}