Add \pset options for boolean value display

First seen: 2025-03-21 03:24:46+00:00 · Messages: 22 · Participants: 9

Latest Update

2026-05-14 · opus 4.7

Overview

This thread proposes adding two new psql \pset options — display_true and display_false — which allow the user to override the default t/f rendering of boolean column values in psql's aligned/wrapped output. The patch is structurally analogous to the long-standing \pset null option. It eventually lands in master as commit 645cb44c5490f70da4dca57b8ecca6562fb883a7, but only after substantive philosophical debate about whether the feature should exist at all, and a follow-up bug (a double-free in do_pset/savePsetInfo/restorePsetInfo) is uncovered by AddressSanitizer once the option is combined with the per-\g option-override syntax.

The Core Problem

Boolean values in PostgreSQL, when rendered through psql's print.c formatting machinery, come back as the canonical SQL/text-output forms t and f. For interactive consumption — particularly for the catalog-inspection \d* family, which renders many boolean attributes (relhasindex, attnotnull, etc.) — single-character t/f columns are visually noisy and easy to misread, especially in wide tables.

Johnston's motivation is twofold:

  1. Improve interactive readability for end users.
  2. Allow PostgreSQL's own documentation — which is generated from psql output — to render booleans as true/false (or any chosen string) rather than t/f. This is a non-trivial point: changing how examples are rendered in the docs requires the option to exist in psql first, and Johnston wanted it in v18 so that in-flight documentation work (including his concurrent \pset null documentation effort) could rely on it.

Architecturally, this is purely a client-side, presentation-layer concern. It does not affect the wire protocol, the server's output functions, or COPY (which bypasses psql's printQuery/print.c path entirely). The substitution happens in the same place \pset null substitution happens — when psql walks the PGresult and decides what string to hand to the formatter for a NULL cell, except the predicate is "column type is bool and value is t/f" rather than "value is NULL".

The Philosophical Disagreement

Tom Lane (committer, with strong weight on questions of compatibility and tooling impact) raised the principal objection: psql output is consumed by a large body of scripts, and making boolean rendering user-configurable risks silently breaking them. His framing is the classic "the cost is paid by people who didn't ask for it" argument, and he explicitly notes that \pset null set a precedent he is not enthusiastic about extending.

The counter-arguments, from Johnston, Vik Fearing, and (later) Álvaro Herrera:

Vik's stance ("scripts that don't use --no-psqlrc are already bug ridden") is the strongest articulation of the established convention: machine consumers should opt out of the user's psql environment, full stop. That convention pre-dates this patch and is the load-bearing reason the objection ultimately did not block commit.

A separate, older thread referenced by Daniel Vérité (56308F56.8060908@joh.to) shows essentially the same proposal had been rejected previously; Johnston's argument is that opinions and the surrounding ecosystem have shifted in the intervening decade.

Secondary Design Questions

Naming

Johnston initially preferred symmetry with \pset null:

\pset null   '(null)'
\pset true   'YES'
\pset false  'no'

Álvaro's v2 went with display_true / display_false because the bare true/false names are extremely generic identifiers in a \pset namespace that already contains things like format, border, pager. Evan Li pointed out the resulting inconsistency in the test cases (mixing null with display_true/display_false), but Álvaro's view — that retroactively renaming null is not on the table, and that prefixing the new names is the lesser evil — prevailed at commit time. The committer explicitly notes "I got zero votes and lots of digression," which is itself a meta-comment on bikeshedding fatigue.

Sanity-checking the values

Evan Li proposed rejecting obviously-deceptive settings, e.g. \pset display_true 'f', on the grounds that a user could leave such a setting in their .psqlrc after testing and then misread results. Johnston was initially sympathetic, then settled on "preventing literal t/f swap is reasonable" — but Tom shut this down decisively: any rule that rejects 'f' is trivially defeated by 'f ', 'f\t', NBSP+f, Unicode lookalikes, etc. A partial guard is worse than none because it suggests a safety property that doesn't actually hold. This is a clean application of the general principle that input validation must be either total or absent; half-measures create false confidence. Tom's view carried — no validation was added.

Localization / scope

Pavel Stěhule observed that there are essentially three sensible default modes — short (t/f), long (true/false), and localized — and that localized-short (single-letter native-language equivalents) is a non-starter because of collisions and lack of convention. He also observed that NULL has no analogous "natural" alternative representation, which is why \pset null doesn't have the same naming pressure. The committed patch deliberately does not localize anything; it is purely a user-configurable string substitution.

Coverage limits

Daniel Vérité raised an important architectural caveat: this approach intercepts boolean values only at the top level of result columns. Booleans inside arrays, composite types, ranges, JSON, and COPY output are not substituted, because psql sees those as opaque text from the server's output functions. Johnston accepted this — arrays might be feasible later, composites are low-volume, and COPY is explicitly out of scope (and shouldn't be affected by \pset anyway). The committed feature is therefore intentionally incomplete: it improves the common case (top-level boolean columns in \d* and ad-hoc queries) without trying to be a deep type-aware rewriter.

The Post-Commit Bug

Roughly five months after commit, Alexander Kozhemyakin reported an ASan double-free reproducible with:

\pset display_false 'f'
SELECT 1 as one, 2 as two \g (display_false=csv csv_fieldsep='\t')
\pset display_false 'f'

The crash is at command.c:5310 inside do_pset, with the prior allocation traced to pg_strdup and the prior free also inside do_pset. The mechanism is the per-\g options-override path: when \g (option=value …) is used, psql constructs a temporary printQueryOpt (popt) struct, applies the overrides, executes the query, then restores the original. The save/restore path is implemented by savePsetInfo/restorePsetInfo, which must know about every heap-allocated string field in the popt struct so it can dup-on-save and free-on-restore correctly.

The original commit added display_true/display_false to the popt struct and to do_pset (which pg_frees the old value before assigning the new one), but did not teach savePsetInfo/restorePsetInfo about the new fields. Consequently after a \g (display_false=…) invocation, restore copies a pointer that was already freed (or fails to dup the saved value), and the next \pset display_false … triggers pg_free on a stale pointer — a textbook double-free.

Johnston's fix (committed by Bruce Momjian) extends savePsetInfo/restorePsetInfo to handle the two new string fields the same way they handle nullPrint, fieldSep.separator, etc. Johnston correctly suspects the same class of bug could exist for \pset null combined with \g (null=…) — which suggests this is a recurring trap whenever a new heap-owned popt field is added: the save/restore symmetry is easy to forget because the two functions are physically distant from do_pset in command.c, and there is no compile-time enforcement that they stay in sync. This is the most interesting engineering lesson from the thread: the popt struct's memory ownership invariants are maintained by convention across at least three functions, and a checklist or struct-driven approach would prevent recurrence.

Significance

The feature itself is small, but the thread is a good case study in: