Skip to content

PostgreSQL psql — Interactive Terminal: MainLoop, Backslash Commands, Query Dispatch, and the describe.c Catalog Layer

Contents:

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:

  1. 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 \g directives, accumulate text until a boundary is found, then send the accumulated buffer to the server as an opaque string.

  2. Re-entrancy. A user may want to execute a script from within an interactive session (\i), a script may itself include \i commands, and the editor invoked by \e may 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.

  3. 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.

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.

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.

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.

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.

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 MainLoop
mainloop.c MainLoop() — re-entrant read/scan/dispatch loop
command.c HandleSlashCmds() / exec_command() — backslash dispatch
common.c SendQuery() / ExecQueryAndProcessResults() — SQL dispatch
describe.c \d command catalog-query generators

The 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.

main in startup.c performs five steps before handing off to MainLoop:

  1. Initialize pset defaults: print format PRINT_ALIGNED, border 1, pager enabled, Unicode linestyles, AUTOCOMMIT on.
  2. 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 into pset fields so that \set AUTOCOMMIT off has an immediate effect on pset.autocommit.
  3. Parse command-line options into a SimpleActionList. Each -c argument becomes an ACT_SINGLE_QUERY or ACT_SINGLE_SLASH cell; each -f becomes ACT_FILE.
  4. Connect: call PQconnectdbParams in a loop until authentication succeeds or the user declines to supply a password. Store the connection in pset.db.
  5. Dispatch: if any -c/-f actions exist, execute them in order (with optional -1 single-transaction wrapping). Otherwise enter MainLoop(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);

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_buf accumulates the current statement; previous_buf holds the last-executed statement (for \e to re-edit); history_buf accumulates multi-line input for a single readline history entry.
  • ConditionalStack cond_stack: tracks \if/\elif/\else/\endif nesting. conditional_active(cond_stack) returns false inside inactive branches; the scanner still runs (to count structure), but SendQuery and HandleSlashCmds are 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_buf and query_buf are swapped by pointer, not copied. query_buf is then reset. This means previous_buf always holds the last-sent statement without a string copy.
  • PGDMP guard: the first line of a non-interactive source is checked for the PGDMP prefix (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.

HandleSlashCmds is the entry point for every \command token that the scanner reports as PSCAN_BACKSLASH:

// HandleSlashCmds — src/bin/psql/command.c (condensed)
backslashResult
HandleSlashCmds(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.c
if (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 branches

Notable groups:

  • Output modifiers: \a (toggle alignment), \C (title), \f (field separator), \H (HTML), \t (tuples only), \x (expanded), \T (table attributes). These call do_pset which writes into pset.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 in pset (gdesc_flag, gexec_flag, gset_prefix) and return PSQL_CMD_SEND so MainLoop calls SendQuery.
  • Connection: \c/\connect calls do_connect which calls PQconnectdbParams and replaces pset.db.
  • Include/edit: \i/\ir call process_file which calls MainLoop recursively; \e/\ef/\ev invoke an external editor and return PSQL_CMD_NEWEDIT so MainLoop re-scans the edited buffer.
  • Conditional flow: \if/\elif/\else/\endif manipulate cond_stack. These are the only commands that run even in inactive branches (they must count nesting correctly).
  • Pipeline (PG17+): \startpipeline/\endpipeline/\flush/ \flushrequest/\syncpipeline/\getresults map to PSQL_SEND_MODE values (PSQL_SEND_START_PIPELINE_MODE, PSQL_SEND_PIPELINE_SYNC, etc.) stored in pset.send_mode for SendQuery to act on.

The return codes from exec_command_* functions are:

CodeMeaning
PSQL_CMD_SKIP_LINEcommand executed; do not send query buffer
PSQL_CMD_SENDsend query_buf to server after returning
PSQL_CMD_NEWEDITquery_buf was replaced; re-scan it
PSQL_CMD_TERMINATEexit MainLoop
PSQL_CMD_ERRORcommand failed
PSQL_CMD_UNKNOWNunrecognised command

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)
bool
SendQuery(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 int
ExecQueryAndProcessResults(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)
bool
AcceptResult(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.

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.

The PsqlSettings struct (typedef struct _psqlSettings) holds all shared state. Key fields:

// PsqlSettings (excerpt) — src/bin/psql/settings.h
typedef 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).

Anchor on symbol names, not line numbers. Use git grep -n '<symbol>' src/bin/psql/ to relocate; line numbers below are hints scoped to commit 273fe94.

  • main — parse options, connect, dispatch actions or enter MainLoop.
  • EstablishVariableSpace — register SetVariableHooks for ~25 special variables; hooks translate string values into pset fields.
  • parse_psql_optionsgetopt_long loop; populates adhoc_opts and SimpleActionList.
  • 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.
  • MainLoop — re-entrant loop; saves/restores source/lineno; drives psql_scanSendQuery / HandleSlashCmds.
  • psqlscan_callbacksPsqlScanCallbacks struct wiring psql_get_variable into the flex scanner for :'var' interpolation.
  • sigint_interrupt_jmp / sigint_interrupt_enabledsigsetjmp target 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; calls exec_command; handles restricted mode and unknown-command error.
  • exec_command — flat if/else if dispatch on command name.
  • exec_command_d — dispatches \d* variants to describe.c functions.
  • exec_command_connect — calls do_connect; replaces pset.db.
  • exec_command_include — calls process_file (re-entrant MainLoop).
  • exec_command_edit / exec_command_ef_ev — invoke $EDITOR; return PSQL_CMD_NEWEDIT.
  • exec_command_g / process_command_g_options — set pset.gfname, pset.gsavepopt, return PSQL_CMD_SEND.
  • exec_command_if / exec_command_elif / exec_command_else / exec_command_endif — manipulate ConditionalStack.
  • exec_command_startpipeline / exec_command_endpipeline / exec_command_flush / exec_command_flushrequest / exec_command_getresults — pipeline control; set pset.send_mode.
  • SendQuery — top-level SQL dispatcher; transaction wrapping, savepoint, ExecQueryAndProcessResults, post-send actions.
  • ExecQueryAndProcessResults — libpq send + result loop; handles simple and extended protocol, pipeline mode, \watch mode.
  • PSQLexec — back-door single-query path; used by describe.c.
  • PSQLexecWatch\watch variant; calls ExecQueryAndProcessResults with is_watch=true and min_rows.
  • AcceptResult — classify PQresultStatus as success/failure.
  • SetResultVariables — write $ERROR, $SQLSTATE, $ROW_COUNT etc.
  • SetShellResultVariables — write $SHELL_ERROR, $SHELL_EXIT_CODE.
  • PrintQueryResult — format and print one PGresult via printQuery.
  • ClearOrSaveResult — stash error results in pset.last_error_result for \errverbose; PQclear everything else.
  • pipelineReset — zero piped_syncs, piped_commands, available_results, requested_results.
  • describeTableDetails — OID lookup + loop over describeOneTableDetails.
  • describeOneTableDetails — multi-query assembly; uses printTableContent to collect columns, indexes, constraints, triggers, policies, RLS, parents.
  • listTables — list tables/views/sequences/indexes matching a pattern.
  • listAllDbs — list databases; used by \l and psql -l.
  • listFunctions — list functions/procedures matching a pattern.
  • map_typename_pattern — normalize type name patterns for \dT.
  • printACLColumn — format ACL columns for \dp/\z.
  • PsqlSettings (_psqlSettings) — global state struct.
  • PSQL_SEND_MODE — enum governing ExecQueryAndProcessResults send 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)”
SymbolFileLine
mainsrc/bin/psql/startup.c126
EstablishVariableSpacesrc/bin/psql/startup.c1218
PsqlSettings (struct close)src/bin/psql/settings.h187
PSQL_SEND_MODEsrc/bin/psql/settings.h73
MainLoopsrc/bin/psql/mainloop.c33
psqlscan_callbackssrc/bin/psql/mainloop.c20
HandleSlashCmdssrc/bin/psql/command.c231
exec_commandsrc/bin/psql/command.c315
SendQuerysrc/bin/psql/common.c1118
AcceptResultsrc/bin/psql/common.c416
SetResultVariablessrc/bin/psql/common.c476
PSQLexecsrc/bin/psql/common.c655
PSQLexecWatchsrc/bin/psql/common.c710
PrintQueryResultsrc/bin/psql/common.c1039
ExecQueryAndProcessResultssrc/bin/psql/common.c1569
listAllDbssrc/bin/psql/describe.c946
describeTableDetailssrc/bin/psql/describe.c1488
describeOneTableDetailssrc/bin/psql/describe.c1571
listTablessrc/bin/psql/describe.c4007

Facts verified against commit 273fe94 (REL_18_STABLE).

  • MainLoop is genuinely re-entrant. It saves and restores pset.cur_cmd_source, pset.cur_cmd_interactive, and pset.lineno at entry and exit. \i and process_file call MainLoop on a FILE *, and recursive invocations get their own scan_state, cond_stack, and query_buf. Verified in mainloop.c: the save/restore block at lines 58–66 and 656–659.

  • The previous_buf / query_buf swap is a pointer swap, not a copy. After SendQuery, MainLoop swaps the two PQExpBuffer * pointers using a temporary variable. previous_buf therefore always holds the last-sent statement with zero allocation. Verified in mainloop.c at the swap block following SendQuery.

  • sigint_interrupt_jmp is re-established every outer loop iteration. The sigsetjmp call is inside the while (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 in mainloop.c lines 107–142.

  • PSQLexec bypasses transaction wrapping and result-variable setting. It calls PQexec directly after the ECHO_HIDDEN check. It does not call command_no_begin, does not issue BEGIN, does not call SetResultVariables, and does not update pset.last_error_result. Verified in common.c lines 654–700.

  • SetResultVariables is called after every SendQuery result, not just errors. On success it writes ERROR=false, SQLSTATE=00000, and ROW_COUNT from PQcmdTuples. On failure it writes ERROR=true, the SQLSTATE code, and updates LAST_ERROR_MESSAGE. Verified in common.c lines 476–504.

  • PSQL_SEND_MODE enum was added for pipeline support. The enum contains PSQL_SEND_START_PIPELINE_MODE and PSQL_SEND_END_PIPELINE_MODE, confirming that pipeline mode is a REL_18-present feature in psql, not PG19-only. Verified in settings.h lines 73–84.

  • process_psqlrc_file tries a minor-version-suffixed rc file first. It constructs filename-PG_VERSION (e.g., .psqlrc-18.1), then filename-PG_MAJORVERSION (e.g., .psqlrc-18), then the plain file. This allows version-specific customization. Verified in startup.c lines 813–835.

  1. \watch timer mechanism. exec_command_watch is implemented in command.c and uses setitimer on Unix, but the interaction between the interval timer, SIGALRM, and sigsetjmp in MainLoop is not fully traced here. The empty_signal_handler installed in main for SIGALRM suggests that the signal is used for wakeup but not for action directly. Investigation path: exec_command_watch in command.c and the do_watch path in common.c.

  2. \bind and extended query protocol in psql. exec_command_bind stores parameters in pset.bind_params and sets pset.send_mode = PSQL_SEND_EXTENDED_QUERY_PARAMS. How ExecQueryAndProcessResults dispatches to PQsendQueryParams vs. the simple PQsendQuery path, and how error recovery in pipeline mode interacts with the savepoint mechanism in SendQuery, is partially traced. Investigation path: ExecQueryAndProcessResults in common.c lines 1569–2747.

  3. \copy implementation. exec_command_copy in command.c calls do_copy in copy.c. The \copy command implements a client-side COPY that routes data through the libpq COPY protocol rather than a server-side file path. The interaction with PGRES_COPY_IN / PGRES_COPY_OUT result statuses visible in AcceptResult is not detailed in this document.

Pointers, not analysis. Each bullet is a starting handle for a follow-up document.

  • MySQL mysql CLI. 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 \gset storing arbitrary result columns into named shell variables.

  • SQLite sqlite3 CLI. Implements a similar two-tier architecture. The .schema, .tables, and .dump meta-commands are analogous to psql’s \d* set but query SQLite’s sqlite_master rather than pg_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 uses pgspecial for \d commands and prompt_toolkit for 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.

  • psql pipeline mode (PG14+). The \startpipeline / \endpipeline / \bind / \syncpipeline commands introduced in PG14–17 make psql a first-class test tool for the libpq pipeline API. The PSQL_SEND_MODE enum in settings.h and the pipelineReset / SetPipelineVariables functions in common.c form the client-side counterpart to the server-side PostgresMain pipeline handling documented in postgres-wire-protocol.md.

  • pg_dump / pg_restore. These utilities share fe_utils/ helpers with psql (notably fe_utils/print.c for table formatting) but do not use MainLoop or the meta-command dispatch. They are closer to the PSQLexec-only pattern: each operation is a PQexec call feeding a structured output formatter. See postgres-pg-dump-restore.md.

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.cMainLoop, re-entrant loop, Ctrl-C recovery.
  • src/bin/psql/command.cHandleSlashCmds, exec_command, all exec_command_* functions.
  • src/bin/psql/common.cSendQuery, ExecQueryAndProcessResults, PSQLexec, AcceptResult, SetResultVariables, PrintQueryResult.
  • src/bin/psql/describe.cdescribeTableDetails, describeOneTableDetails, listTables, listAllDbs, listFunctions.
  • src/bin/psql/startup.cmain, EstablishVariableSpace, parse_psql_options, process_psqlrc.
  • src/bin/psql/settings.hPsqlSettings, PSQL_SEND_MODE enum, control enums.
  • postgres-wire-protocol.md — the server-side FE/BE protocol that psql exercises through libpq; PostgresMain and the message dispatch loop.
  • postgres-portals-prepared.md — the server-side CachedPlanSource and Portal objects that psql’s \bind / extended-query path creates.
  • postgres-pg-dump-restore.mdpg_dump and pg_restore, which share fe_utils/ but do not use MainLoop.
  • postgres-overview-client-protocol.md — section router for the client-protocol subcategory.