PostgreSQL psql — Interactive Terminal: MainLoop, Backslash Commands, Query Dispatch, and the describe.c Catalog Layer
Contents:
- Theoretical Background
- Common DBMS Design
- PostgreSQL’s Approach
- Source Walkthrough
- Source Verification
- Beyond PostgreSQL — Comparative Designs
- Sources
Theoretical Background
Section titled “Theoretical Background”A database interactive terminal is a thin client that sits between a human (or a script) and a database server. Its essential job is to accept input in two categories — plain SQL and client-side meta-commands — and to dispatch them to the correct handler. The design space is governed by three tensions:
-
Lexer boundary. Where does the terminal’s own parser end and the server’s parser begin? A terminal that sends every line immediately loses multi-line statement accumulation; one that tries to fully parse SQL client-side duplicates a large fraction of the server grammar. The practical middle ground is a statement-boundary detector: scan for semicolons and
\gdirectives, accumulate text until a boundary is found, then send the accumulated buffer to the server as an opaque string. -
Re-entrancy. A user may want to execute a script from within an interactive session (
\i), a script may itself include\icommands, and the editor invoked by\emay return a modified query buffer that needs re-scanning. This calls for a re-entrant main loop that saves and restores its source pointer on each recursive call rather than a single top-level read loop. -
Meta-command vs. SQL dispatch. The terminal must decide per-input whether a token is a meta-command (starts with
\) or SQL text to accumulate. This decision must cooperate with the lexer’s string literal and comment state so that a backslash inside a string literal is not misread as a meta-command.
Interactive terminals also carry output-formatting concerns that have no analogue in a wire-protocol layer: column alignment, pager invocation, output-file redirection, timing display, and variable interpolation into query text. In PostgreSQL’s psql these responsibilities are distributed across several cooperating files rather than concentrated in a single class.
Common DBMS Design
Section titled “Common DBMS Design”Statement-boundary scanning with a flex lexer
Section titled “Statement-boundary scanning with a flex lexer”Most database CLI tools use a lexer (generated or hand-written) to scan
input for statement boundaries. The lexer must track string-literal context
(single quotes), dollar-quoting, block comments, and identifier quoting so
that a semicolon inside a string does not terminate the statement. psql uses
a flex-generated scanner (psqlscan.l / psqlscanslash.l) that produces
scan tokens (PSCAN_SEMICOLON, PSCAN_BACKSLASH, PSCAN_EOL,
PSCAN_INCOMPLETE) rather than a full parse tree. The accumulated text is
passed verbatim to the server.
Backslash-command dispatch table
Section titled “Backslash-command dispatch table”CLI tools with meta-command sets universally use a dispatch table keyed on the command name. Because meta-commands are processed client-side, they can manipulate connection parameters, alter output formatting, or read local files — things the server cannot do. The dispatch table typically returns a result code (skip line / send query / error / terminate) that the calling loop uses to decide the next action.
Two-tier query execution path
Section titled “Two-tier query execution path”The execution path for a SQL statement typically has two tiers. The first
tier handles transaction management: wrap the query in BEGIN if autocommit
is off, optionally set a savepoint for error isolation. The second tier does
the actual send-and-receive: call PQexec or PQexecParams, wait for
results, iterate over result rows, format and print them. This separation
allows the terminal to retry a failed query with rollback semantics without
duplicating the send-and-receive logic.
Catalog-query abstraction for \d commands
Section titled “Catalog-query abstraction for \d commands”Describing database objects requires issuing SQL queries against the system
catalog. Terminals factor this into a dedicated module: a thin SQL generator
that assembles a SELECT against pg_class, pg_attribute, pg_constraint
and friends, executes it via the backdoor query path (PSQLexec), and formats
the result as a human-readable table. The SQL generator is version-aware: it
adjusts the query for the server version reported by PQserverVersion.
PostgreSQL’s Approach
Section titled “PostgreSQL’s Approach”psql is a standalone binary (src/bin/psql/). It connects to a server via
libpq (PQconnectdbParams) and drives the connection entirely through the
libpq API — it never speaks the wire protocol directly. The architecture
rests on five coordinated components:
startup.c main() — parse options, connect, invoke MainLoopmainloop.c MainLoop() — re-entrant read/scan/dispatch loopcommand.c HandleSlashCmds() / exec_command() — backslash dispatchcommon.c SendQuery() / ExecQueryAndProcessResults() — SQL dispatchdescribe.c \d command catalog-query generatorsThe global PsqlSettings pset (defined in startup.c, declared in
settings.h) is the single shared-state record. It holds the active
PGconn *db, output file handles, print format options, session variables,
and every boolean control flag. MainLoop and its callees read and write
pset directly; there is no hidden state elsewhere.
Startup sequence (startup.c)
Section titled “Startup sequence (startup.c)”main in startup.c performs five steps before handing off to MainLoop:
- Initialize
psetdefaults: print formatPRINT_ALIGNED, border1, pager enabled, Unicode linestyles,AUTOCOMMITon. - Call
EstablishVariableSpace— register substitute and assign hooks for every special variable (AUTOCOMMIT,ECHO,ON_ERROR_STOP,HISTSIZE,VERBOSITY,WATCH_INTERVAL, and ~20 others). The hooks translate string variable values intopsetfields so that\set AUTOCOMMIT offhas an immediate effect onpset.autocommit. - Parse command-line options into a
SimpleActionList. Each-cargument becomes anACT_SINGLE_QUERYorACT_SINGLE_SLASHcell; each-fbecomesACT_FILE. - Connect: call
PQconnectdbParamsin a loop until authentication succeeds or the user declines to supply a password. Store the connection inpset.db. - Dispatch: if any
-c/-factions exist, execute them in order (with optional-1single-transaction wrapping). Otherwise enterMainLoop(stdin).
// main — src/bin/psql/startup.c (condensed)pset.db = PQconnectdbParams(keywords, values, true);// ...if (options.actions.head != NULL){ for (cell = options.actions.head; cell; cell = cell->next) { if (cell->action == ACT_SINGLE_QUERY) successResult = SendQuery(cell->val) ? EXIT_SUCCESS : EXIT_FAILURE; else if (cell->action == ACT_SINGLE_SLASH) successResult = HandleSlashCmds(...) != PSQL_CMD_ERROR ? EXIT_SUCCESS : EXIT_FAILURE; else if (cell->action == ACT_FILE) successResult = process_file(cell->val, false); }}else successResult = MainLoop(stdin);The re-entrant MainLoop (mainloop.c)
Section titled “The re-entrant MainLoop (mainloop.c)”MainLoop(FILE *source) is the heart of psql. It is re-entrant: \i and
\e call MainLoop (or process_file which calls MainLoop) recursively.
Each invocation saves and restores pset.cur_cmd_source,
pset.cur_cmd_interactive, and pset.lineno.
The loop body for each line:
// MainLoop — src/bin/psql/mainloop.c (condensed)while (successResult == EXIT_SUCCESS){ // Ctrl-C cleanup via sigsetjmp(sigint_interrupt_jmp) // Fetch a line: gets_interactive() or gets_fromFile() line = pset.cur_cmd_interactive ? gets_interactive(get_prompt(prompt_status, cond_stack), query_buf) : gets_fromFile(source);
// Scan the line into query_buf, looking for statement boundaries psql_scan_setup(scan_state, line, strlen(line), pset.encoding, standard_strings());
while (success || !die_on_error) { scan_result = psql_scan(scan_state, query_buf, &prompt_tmp);
if (scan_result == PSCAN_SEMICOLON || (scan_result == PSCAN_EOL && pset.singleline)) { if (conditional_active(cond_stack)) success = SendQuery(query_buf->data); resetPQExpBuffer(query_buf); } else if (scan_result == PSCAN_BACKSLASH) { slashCmdStatus = HandleSlashCmds(scan_state, cond_stack, query_buf, previous_buf); if (slashCmdStatus == PSQL_CMD_SEND) success = SendQuery(query_buf->data); }
if (scan_result == PSCAN_INCOMPLETE || scan_result == PSCAN_EOL) break; }}Key design points:
- Three buffers:
query_bufaccumulates the current statement;previous_bufholds the last-executed statement (for\eto re-edit);history_bufaccumulates multi-line input for a single readline history entry. ConditionalStack cond_stack: tracks\if/\elif/\else/\endifnesting.conditional_active(cond_stack)returns false inside inactive branches; the scanner still runs (to count structure), butSendQueryandHandleSlashCmdsare skipped.- Ctrl-C recovery:
sigsetjmp(sigint_interrupt_jmp, 1)is re-established at the top of each iteration. A Ctrl-C during input or query execution jumps back here, resets the scan state and buffers, and re-prompts. - Buffer swap on send: after
SendQuery,previous_bufandquery_bufare swapped by pointer, not copied.query_bufis then reset. This meansprevious_bufalways holds the last-sent statement without a string copy. - PGDMP guard: the first line of a non-interactive source is checked for
the
PGDMPprefix (PostgreSQL custom-format dump marker) and rejected with a clear error message.
flowchart TD
A["MainLoop entry<br/>save prev source/lineno"] --> B["sigsetjmp<br/>Ctrl-C anchor"]
B --> C{"interactive?"}
C -- "yes" --> D["gets_interactive<br/>readline prompt"]
C -- "no" --> E["gets_fromFile"]
D --> F["psql_scan_setup"]
E --> F
F --> G["psql_scan loop"]
G --> H{"scan_result"}
H -- "PSCAN_SEMICOLON<br/>or singleline EOL" --> I{"conditional_active?"}
I -- "yes" --> J["SendQuery<br/>swap query/prev bufs"]
I -- "no" --> K["skip — warn if interactive"]
J --> G
K --> G
H -- "PSCAN_BACKSLASH" --> L["HandleSlashCmds"]
L --> M{"slashCmdStatus"}
M -- "PSQL_CMD_SEND" --> J
M -- "PSQL_CMD_NEWEDIT" --> F
M -- "PSQL_CMD_TERMINATE" --> N["break outer loop"]
H -- "PSCAN_EOL<br/>PSCAN_INCOMPLETE" --> B
N --> O["restore prev source/lineno<br/>return successResult"]
Figure 1 — MainLoop control flow. The re-entrant loop fetches one line,
scans it for statement boundaries and backslash commands, and dispatches
accordingly. Ctrl-C jumps back to the sigsetjmp anchor at the top of the
outer loop.
Backslash command dispatch (command.c)
Section titled “Backslash command dispatch (command.c)”HandleSlashCmds is the entry point for every \command token that the
scanner reports as PSCAN_BACKSLASH:
// HandleSlashCmds — src/bin/psql/command.c (condensed)backslashResultHandleSlashCmds(PsqlScanState scan_state, ConditionalStack cstack, PQExpBuffer query_buf, PQExpBuffer previous_buf){ cmd = psql_scan_slash_command(scan_state);
if (restricted && strcmp(cmd, "unrestrict") != 0) status = PSQL_CMD_ERROR; /* restricted mode: only \unrestrict allowed */ else status = exec_command(cmd, scan_state, cstack, query_buf, previous_buf);
if (status == PSQL_CMD_UNKNOWN) { pg_log_error("invalid command \\%s", cmd); status = PSQL_CMD_ERROR; } // Drain any remaining arguments after a valid command fflush(pset.queryFout); return status;}exec_command is a flat if/else if chain keyed on cmd. Each branch
calls a dedicated exec_command_* function. The chain covers ~50 commands:
// exec_command dispatch (excerpt) — src/bin/psql/command.cif (strcmp(cmd, "a") == 0) status = exec_command_a(scan_state, active_branch);else if (strcmp(cmd, "bind") == 0) status = exec_command_bind(scan_state, active_branch);else if (strcmp(cmd, "c") == 0 || strcmp(cmd, "connect") == 0) status = exec_command_connect(scan_state, active_branch);else if (cmd[0] == 'd') status = exec_command_d(scan_state, active_branch, cmd);else if (strcmp(cmd, "endpipeline") == 0) status = exec_command_endpipeline(scan_state, active_branch);else if (strcmp(cmd, "g") == 0 || strcmp(cmd, "gx") == 0) status = exec_command_g(scan_state, active_branch, cmd);// ... ~45 more branchesNotable groups:
- Output modifiers:
\a(toggle alignment),\C(title),\f(field separator),\H(HTML),\t(tuples only),\x(expanded),\T(table attributes). These calldo_psetwhich writes intopset.popt.topt. - Execution modifiers:
\g/\gx(execute with optional file output or expanded),\gdesc(describe result columns),\gexec(execute each result cell as SQL),\gset(store results in variables),\crosstabview. These set one-shot flags inpset(gdesc_flag,gexec_flag,gset_prefix) and returnPSQL_CMD_SENDsoMainLoopcallsSendQuery. - Connection:
\c/\connectcallsdo_connectwhich callsPQconnectdbParamsand replacespset.db. - Include/edit:
\i/\ircallprocess_filewhich callsMainLooprecursively;\e/\ef/\evinvoke an external editor and returnPSQL_CMD_NEWEDITsoMainLoopre-scans the edited buffer. - Conditional flow:
\if/\elif/\else/\endifmanipulatecond_stack. These are the only commands that run even in inactive branches (they must count nesting correctly). - Pipeline (PG17+):
\startpipeline/\endpipeline/\flush/\flushrequest/\syncpipeline/\getresultsmap toPSQL_SEND_MODEvalues (PSQL_SEND_START_PIPELINE_MODE,PSQL_SEND_PIPELINE_SYNC, etc.) stored inpset.send_modeforSendQueryto act on.
The return codes from exec_command_* functions are:
| Code | Meaning |
|---|---|
PSQL_CMD_SKIP_LINE | command executed; do not send query buffer |
PSQL_CMD_SEND | send query_buf to server after returning |
PSQL_CMD_NEWEDIT | query_buf was replaced; re-scan it |
PSQL_CMD_TERMINATE | exit MainLoop |
PSQL_CMD_ERROR | command failed |
PSQL_CMD_UNKNOWN | unrecognised command |
Query dispatch (common.c)
Section titled “Query dispatch (common.c)”SendQuery(const char *query) is the single entry point for all SQL
submitted to the server — from PSCAN_SEMICOLON detection in MainLoop,
from \g commands, and from non-interactive -c actions:
// SendQuery — src/bin/psql/common.c (condensed)boolSendQuery(const char *query){ // 1. Guard: connection must be live // 2. Single-step prompt if pset.singlestep // 3. Echo if pset.echo == PSQL_ECHO_QUERIES // 4. Log to pset.logfile if set
SetCancelConn(pset.db); transaction_status = PQtransactionStatus(pset.db);
// 5. Implicit BEGIN if autocommit=off and transaction is idle if (transaction_status == PQTRANS_IDLE && !pset.autocommit && !command_no_begin(query)) PQexec(pset.db, "BEGIN");
// 6. SAVEPOINT for ON_ERROR_ROLLBACK in interactive mode if (transaction_status == PQTRANS_INTRANS && pset.on_error_rollback != PSQL_ERROR_ROLLBACK_OFF && ...) PQexec(pset.db, "SAVEPOINT pg_psql_temporary_savepoint");
// 7. Execute: describe-only or normal if (pset.gdesc_flag) OK = DescribeQuery(query, &elapsed_msec); else OK = (ExecQueryAndProcessResults(query, &elapsed_msec, &svpt_gone, false, 0, NULL, NULL) > 0);
// 8. Handle savepoint: RELEASE or ROLLBACK TO based on txn status // 9. Post-send: gexec_flag loop, gset_prefix store // 10. Timing output if pset.timing}ExecQueryAndProcessResults does the actual libpq send and result loop:
// ExecQueryAndProcessResults — src/bin/psql/common.c (condensed)static intExecQueryAndProcessResults(const char *query, double *elapsed_msec, bool *svpt_gone_p, bool is_watch, int min_rows, const printQueryOpt *opt, FILE *printQueryFout){ // Send via pset.send_mode: // PSQL_SEND_QUERY → PQsendQuery (simple protocol) // PSQL_SEND_EXTENDED_* → PQsendQueryParams / PQsendPrepare / etc. // PSQL_SEND_PIPELINE_SYNC → PQpipelineSync
// Result loop: while ((result = PQgetResult(pset.db)) != NULL) { OK = AcceptResult(result, true); SetResultVariables(result, OK); // set $ERROR, $SQLSTATE, $ROW_COUNT // PrintQueryResult formats and outputs the result rows PrintQueryResult(result, last, opt, ...); PQclear(result); } return ntuples;}AcceptResult maps PQresultStatus to a boolean:
// AcceptResult — src/bin/psql/common.c (condensed)boolAcceptResult(const PGresult *result, bool show_error){ switch (PQresultStatus(result)) { case PGRES_COMMAND_OK: case PGRES_TUPLES_OK: case PGRES_TUPLES_CHUNK: case PGRES_EMPTY_QUERY: case PGRES_COPY_IN: case PGRES_COPY_OUT: case PGRES_PIPELINE_SYNC: return true; case PGRES_PIPELINE_ABORTED: case PGRES_BAD_RESPONSE: case PGRES_NONFATAL_ERROR: case PGRES_FATAL_ERROR: return false; /* also calls CheckConnection() */ }}SetResultVariables writes $ERROR, $SQLSTATE, $ROW_COUNT,
$LAST_ERROR_SQLSTATE, and $LAST_ERROR_MESSAGE into pset.vars after
every query.
PSQLexec is a parallel “back-door” path used by describe.c and other
internal callers. It bypasses the transaction-wrapping and result-variable
logic of SendQuery, and respects ECHO_HIDDEN for debugging:
// PSQLexec — src/bin/psql/common.c (condensed)PGresult *PSQLexec(const char *query){ if (pset.echo_hidden != PSQL_ECHO_HIDDEN_OFF) printf("/******** QUERY *********\n%s\n/************************\n\n", query); if (pset.echo_hidden == PSQL_ECHO_HIDDEN_NOEXEC) return NULL; return PQexec(pset.db, query);}PSQLexecWatch is a variant of ExecQueryAndProcessResults used by
\watch. It accepts a min_rows threshold to detect when a periodic query
returns fewer rows than expected, and uses a timer-based loop in
exec_command_watch to re-execute at the configured WATCH_INTERVAL.
The describe.c catalog-query layer
Section titled “The describe.c catalog-query layer”describe.c backs all \d variants. It has two layers:
Layer 1 — dispatch (exec_command_d in command.c): map the \d
variant string to the correct describe.c function. For example, \dt
calls listTables("t", pattern, verbose, showSystem), \df calls
listFunctions, \d <name> calls describeTableDetails.
Layer 2 — SQL generation (describe.c): assemble a PQExpBuffer
containing a SELECT against the system catalog, call PSQLexec, and
print the result via printQuery. The queries reference pg_catalog
tables (pg_class, pg_attribute, pg_constraint, pg_index,
pg_trigger, pg_policy, pg_inherits, and ~20 others).
The version-awareness pattern is pervasive: every function checks
pset.sversion before adding a column or JOIN that only exists on a newer
server:
// describeOneTableDetails — src/bin/psql/describe.c (condensed)if (pset.sversion >= 120000) appendPQExpBufferStr(&buf, ",\n pg_get_expr(d.adbin, d.adrelid) AS default_value");describeTableDetails calls describeOneTableDetails for each matching
relation OID. describeOneTableDetails issues multiple PSQLexec calls —
one for the main column list, one for indexes, one for constraints, one for
triggers, one for policies, one for row-level security, one for parent tables
— and assembles a printTableContent structure before calling
printTable(cont, pset.queryFout, ...).
flowchart TD
A["exec_command_d<br/>command.c"] --> B{"\\d variant"}
B -- "\\dt / \\dv / \\di / \\ds" --> C["listTables<br/>describe.c"]
B -- "\\df" --> D["listFunctions<br/>describe.c"]
B -- "\\d name" --> E["describeTableDetails<br/>describe.c"]
B -- "\\l" --> F["listAllDbs<br/>describe.c"]
C --> G["PSQLexec<br/>SELECT pg_class..."]
D --> G
E --> H["describeOneTableDetails<br/>describe.c"]
H --> I["PSQLexec ×N<br/>columns / indexes / triggers<br/>constraints / policies / RLS"]
I --> J["printTable<br/>fe_utils/print.c"]
G --> K["printQuery<br/>fe_utils/print.c"]
F --> G
Figure 2 — \d dispatch and the describe.c catalog-query layer.
Each \d variant routes to a describe.c function that assembles one or
more SQL queries against pg_catalog, runs them via PSQLexec, and renders
the results through the fe_utils/print.c formatter.
PsqlSettings (settings.h)
Section titled “PsqlSettings (settings.h)”The PsqlSettings struct (typedef struct _psqlSettings) holds all shared
state. Key fields:
// PsqlSettings (excerpt) — src/bin/psql/settings.htypedef struct _psqlSettings{ PGconn *db; /* active connection */ int encoding; /* client_encoding */ FILE *queryFout; /* output file / pipe */ printQueryOpt popt; /* print format options (border, format, ...) */ char *gfname; /* one-shot \g output file */ bool gdesc_flag; /* one-shot \gdesc: describe only */ bool gexec_flag; /* one-shot \gexec: execute result cells */ PSQL_SEND_MODE send_mode; /* normal / extended / pipeline-sync / ... */ int bind_nparams; /* \bind param count */ char **bind_params; /* \bind param values */ char *stmtName; /* \bind_named stmt name */ int piped_commands; /* pipeline: commands in flight */ int piped_syncs; /* pipeline: syncs in flight */ int available_results; /* pipeline: results ready */ bool autocommit; /* AUTOCOMMIT variable */ bool on_error_stop; /* ON_ERROR_STOP variable */ bool singleline; /* SINGLELINE variable */ bool singlestep; /* SINGLESTEP: prompt before each query */ PSQL_ECHO echo; /* ECHO: none/queries/errors/all */ PSQL_ECHO_HIDDEN echo_hidden; /* ECHO_HIDDEN: off/on/noexec */ PSQL_ERROR_ROLLBACK on_error_rollback; // ... timing, logfile, vars, prompt1/2/3, verbosity, etc.} PsqlSettings;PSQL_SEND_MODE is an enum (PG17+) that controls how ExecQueryAndProcessResults
sends the query — simple protocol (PSQL_SEND_QUERY), extended protocol
variants (PSQL_SEND_EXTENDED_QUERY_PARAMS, PSQL_SEND_EXTENDED_QUERY_PREPARED),
or pipeline control (PSQL_SEND_PIPELINE_SYNC, PSQL_SEND_START_PIPELINE_MODE,
PSQL_SEND_END_PIPELINE_MODE, PSQL_SEND_FLUSH, PSQL_SEND_FLUSH_REQUEST,
PSQL_SEND_GET_RESULTS).
Source Walkthrough
Section titled “Source Walkthrough”Anchor on symbol names, not line numbers. Use
git grep -n '<symbol>' src/bin/psql/to relocate; line numbers below are hints scoped to commit273fe94.
Startup (src/bin/psql/startup.c)
Section titled “Startup (src/bin/psql/startup.c)”main— parse options, connect, dispatch actions or enterMainLoop.EstablishVariableSpace— registerSetVariableHooksfor ~25 special variables; hooks translate string values intopsetfields.parse_psql_options—getopt_longloop; populatesadhoc_optsandSimpleActionList.process_psqlrc/process_psqlrc_file— load system-wide then user-level rc file; checks.psqlrc-<version>before.psqlrc.PsqlSettings pset— the single global settings instance.
Main loop (src/bin/psql/mainloop.c)
Section titled “Main loop (src/bin/psql/mainloop.c)”MainLoop— re-entrant loop; saves/restores source/lineno; drivespsql_scan→SendQuery/HandleSlashCmds.psqlscan_callbacks—PsqlScanCallbacksstruct wiringpsql_get_variableinto the flex scanner for:'var'interpolation.sigint_interrupt_jmp/sigint_interrupt_enabled—sigsetjmptarget for Ctrl-C recovery; re-established each outer loop iteration.
Backslash commands (src/bin/psql/command.c)
Section titled “Backslash commands (src/bin/psql/command.c)”HandleSlashCmds— entry point; callsexec_command; handles restricted mode and unknown-command error.exec_command— flatif/else ifdispatch on command name.exec_command_d— dispatches\d*variants todescribe.cfunctions.exec_command_connect— callsdo_connect; replacespset.db.exec_command_include— callsprocess_file(re-entrantMainLoop).exec_command_edit/exec_command_ef_ev— invoke$EDITOR; returnPSQL_CMD_NEWEDIT.exec_command_g/process_command_g_options— setpset.gfname,pset.gsavepopt, returnPSQL_CMD_SEND.exec_command_if/exec_command_elif/exec_command_else/exec_command_endif— manipulateConditionalStack.exec_command_startpipeline/exec_command_endpipeline/exec_command_flush/exec_command_flushrequest/exec_command_getresults— pipeline control; setpset.send_mode.
Query execution (src/bin/psql/common.c)
Section titled “Query execution (src/bin/psql/common.c)”SendQuery— top-level SQL dispatcher; transaction wrapping, savepoint,ExecQueryAndProcessResults, post-send actions.ExecQueryAndProcessResults— libpq send + result loop; handles simple and extended protocol, pipeline mode,\watchmode.PSQLexec— back-door single-query path; used bydescribe.c.PSQLexecWatch—\watchvariant; callsExecQueryAndProcessResultswithis_watch=trueandmin_rows.AcceptResult— classifyPQresultStatusas success/failure.SetResultVariables— write$ERROR,$SQLSTATE,$ROW_COUNTetc.SetShellResultVariables— write$SHELL_ERROR,$SHELL_EXIT_CODE.PrintQueryResult— format and print onePGresultviaprintQuery.ClearOrSaveResult— stash error results inpset.last_error_resultfor\errverbose;PQcleareverything else.pipelineReset— zeropiped_syncs,piped_commands,available_results,requested_results.
Catalog queries (src/bin/psql/describe.c)
Section titled “Catalog queries (src/bin/psql/describe.c)”describeTableDetails— OID lookup + loop overdescribeOneTableDetails.describeOneTableDetails— multi-query assembly; usesprintTableContentto collect columns, indexes, constraints, triggers, policies, RLS, parents.listTables— list tables/views/sequences/indexes matching a pattern.listAllDbs— list databases; used by\landpsql -l.listFunctions— list functions/procedures matching a pattern.map_typename_pattern— normalize type name patterns for\dT.printACLColumn— format ACL columns for\dp/\z.
Settings (src/bin/psql/settings.h)
Section titled “Settings (src/bin/psql/settings.h)”PsqlSettings(_psqlSettings) — global state struct.PSQL_SEND_MODE— enum governingExecQueryAndProcessResultssend path.PSQL_ECHO,PSQL_ECHO_HIDDEN,PSQL_ERROR_ROLLBACK,PSQL_COMP_CASE,HistControl— control enums for variable hooks.
Position hints (as of 2026-06-05, REL_18 273fe94)
Section titled “Position hints (as of 2026-06-05, REL_18 273fe94)”| Symbol | File | Line |
|---|---|---|
main | src/bin/psql/startup.c | 126 |
EstablishVariableSpace | src/bin/psql/startup.c | 1218 |
PsqlSettings (struct close) | src/bin/psql/settings.h | 187 |
PSQL_SEND_MODE | src/bin/psql/settings.h | 73 |
MainLoop | src/bin/psql/mainloop.c | 33 |
psqlscan_callbacks | src/bin/psql/mainloop.c | 20 |
HandleSlashCmds | src/bin/psql/command.c | 231 |
exec_command | src/bin/psql/command.c | 315 |
SendQuery | src/bin/psql/common.c | 1118 |
AcceptResult | src/bin/psql/common.c | 416 |
SetResultVariables | src/bin/psql/common.c | 476 |
PSQLexec | src/bin/psql/common.c | 655 |
PSQLexecWatch | src/bin/psql/common.c | 710 |
PrintQueryResult | src/bin/psql/common.c | 1039 |
ExecQueryAndProcessResults | src/bin/psql/common.c | 1569 |
listAllDbs | src/bin/psql/describe.c | 946 |
describeTableDetails | src/bin/psql/describe.c | 1488 |
describeOneTableDetails | src/bin/psql/describe.c | 1571 |
listTables | src/bin/psql/describe.c | 4007 |
Source Verification
Section titled “Source Verification”Facts verified against commit
273fe94(REL_18_STABLE).
Verified facts
Section titled “Verified facts”-
MainLoopis genuinely re-entrant. It saves and restorespset.cur_cmd_source,pset.cur_cmd_interactive, andpset.linenoat entry and exit.\iandprocess_filecallMainLoopon aFILE *, and recursive invocations get their ownscan_state,cond_stack, andquery_buf. Verified inmainloop.c: the save/restore block at lines 58–66 and 656–659. -
The
previous_buf/query_bufswap is a pointer swap, not a copy. AfterSendQuery,MainLoopswaps the twoPQExpBuffer *pointers using a temporary variable.previous_buftherefore always holds the last-sent statement with zero allocation. Verified inmainloop.cat the swap block followingSendQuery. -
sigint_interrupt_jmpis re-established every outer loop iteration. Thesigsetjmpcall is inside thewhile (successResult == EXIT_SUCCESS)loop, not before it. This is documented in the source comment: “We must re-do this each time through the loop for safety, since the jmpbuf might get changed during command execution.” Verified inmainloop.clines 107–142. -
PSQLexecbypasses transaction wrapping and result-variable setting. It callsPQexecdirectly after theECHO_HIDDENcheck. It does not callcommand_no_begin, does not issueBEGIN, does not callSetResultVariables, and does not updatepset.last_error_result. Verified incommon.clines 654–700. -
SetResultVariablesis called after everySendQueryresult, not just errors. On success it writesERROR=false,SQLSTATE=00000, andROW_COUNTfromPQcmdTuples. On failure it writesERROR=true, the SQLSTATE code, and updatesLAST_ERROR_MESSAGE. Verified incommon.clines 476–504. -
PSQL_SEND_MODEenum was added for pipeline support. The enum containsPSQL_SEND_START_PIPELINE_MODEandPSQL_SEND_END_PIPELINE_MODE, confirming that pipeline mode is a REL_18-present feature in psql, not PG19-only. Verified insettings.hlines 73–84. -
process_psqlrc_filetries a minor-version-suffixed rc file first. It constructsfilename-PG_VERSION(e.g.,.psqlrc-18.1), thenfilename-PG_MAJORVERSION(e.g.,.psqlrc-18), then the plain file. This allows version-specific customization. Verified instartup.clines 813–835.
Open questions
Section titled “Open questions”-
\watchtimer mechanism.exec_command_watchis implemented incommand.cand usessetitimeron Unix, but the interaction between the interval timer,SIGALRM, andsigsetjmpinMainLoopis not fully traced here. Theempty_signal_handlerinstalled inmainforSIGALRMsuggests that the signal is used for wakeup but not for action directly. Investigation path:exec_command_watchincommand.cand thedo_watchpath incommon.c. -
\bindand extended query protocol in psql.exec_command_bindstores parameters inpset.bind_paramsand setspset.send_mode = PSQL_SEND_EXTENDED_QUERY_PARAMS. HowExecQueryAndProcessResultsdispatches toPQsendQueryParamsvs. the simplePQsendQuerypath, and how error recovery in pipeline mode interacts with the savepoint mechanism inSendQuery, is partially traced. Investigation path:ExecQueryAndProcessResultsincommon.clines 1569–2747. -
\copyimplementation.exec_command_copyincommand.ccallsdo_copyincopy.c. The\copycommand implements a client-side COPY that routes data through the libpq COPY protocol rather than a server-side file path. The interaction withPGRES_COPY_IN/PGRES_COPY_OUTresult statuses visible inAcceptResultis not detailed in this document.
Beyond PostgreSQL — Comparative Designs
Section titled “Beyond PostgreSQL — Comparative Designs”Pointers, not analysis. Each bullet is a starting handle for a follow-up document.
-
MySQL
mysqlCLI. Uses a similar readline-based loop but has a simpler meta-command set. Status variables ($mysql_affected_rows,$mysql_insert_id) are exposed differently; there is no equivalent to psql’s\gsetstoring arbitrary result columns into named shell variables. -
SQLite
sqlite3CLI. Implements a similar two-tier architecture. The.schema,.tables, and.dumpmeta-commands are analogous to psql’s\d*set but query SQLite’ssqlite_masterrather thanpg_catalog. The describe layer is substantially simpler because SQLite has no roles, ACLs, triggers on views, row-level security, or partitioning. -
pgcli(Python). An alternative psql replacement that usespgspecialfor\dcommands andprompt_toolkitfor the interactive frontend. It demonstrates that the psql architecture (separate meta-command dispatch, catalog query layer, result formatter) is language-independent and can be re-implemented cleanly outside the PostgreSQL source tree. -
psqlpipeline mode (PG14+). The\startpipeline/\endpipeline/\bind/\syncpipelinecommands introduced in PG14–17 make psql a first-class test tool for the libpq pipeline API. ThePSQL_SEND_MODEenum insettings.hand thepipelineReset/SetPipelineVariablesfunctions incommon.cform the client-side counterpart to the server-sidePostgresMainpipeline handling documented inpostgres-wire-protocol.md. -
pg_dump/pg_restore. These utilities sharefe_utils/helpers with psql (notablyfe_utils/print.cfor table formatting) but do not useMainLoopor the meta-command dispatch. They are closer to thePSQLexec-only pattern: each operation is aPQexeccall feeding a structured output formatter. Seepostgres-pg-dump-restore.md.
Sources
Section titled “Sources”PostgreSQL source (under /data/hgryoo/references/postgres, REL_18 273fe94)
Section titled “PostgreSQL source (under /data/hgryoo/references/postgres, REL_18 273fe94)”src/bin/psql/mainloop.c—MainLoop, re-entrant loop, Ctrl-C recovery.src/bin/psql/command.c—HandleSlashCmds,exec_command, allexec_command_*functions.src/bin/psql/common.c—SendQuery,ExecQueryAndProcessResults,PSQLexec,AcceptResult,SetResultVariables,PrintQueryResult.src/bin/psql/describe.c—describeTableDetails,describeOneTableDetails,listTables,listAllDbs,listFunctions.src/bin/psql/startup.c—main,EstablishVariableSpace,parse_psql_options,process_psqlrc.src/bin/psql/settings.h—PsqlSettings,PSQL_SEND_MODEenum, control enums.
Cross-references (sibling module docs)
Section titled “Cross-references (sibling module docs)”postgres-wire-protocol.md— the server-side FE/BE protocol that psql exercises through libpq;PostgresMainand the message dispatch loop.postgres-portals-prepared.md— the server-sideCachedPlanSourceandPortalobjects that psql’s\bind/ extended-query path creates.postgres-pg-dump-restore.md—pg_dumpandpg_restore, which sharefe_utils/but do not useMainLoop.postgres-overview-client-protocol.md— section router for the client-protocol subcategory.