Skip to content

pgman v0.2.0

Choose a tag to compare

@github-actions github-actions released this 24 Oct 10:34
· 334 commits to main since this release
c719886

Highlights

  • Edit a cell in place. u on a result cell writes a one-row
    UPDATE by primary key, shown in the same safety confirm as any
    statement you run.
  • Page past 1000 rows. M fetches the next 1000 rows of a
    SELECT, through a server-side cursor that pgman closes after 60 s
    idle, 10 min open, or before any other work; a standby holds none.
  • A config advisor in the schema wizard (W). Read-only findings
    on cache hit ratio, temp-file spills, dead tuples, lock waits and
    checkpoints, each with its evidence, a conservative setting and the
    RDS / Aurora parameter to change.
  • The whole JDBC tap event. Enter on a tap row shows the full
    SQL, every bound parameter, the error's cause chain and the caller
    stack; the list shows the focused statement's parameters beneath it.
  • Help in sections. One section at a time instead of one long page,
    with a summary for each and a filter across them all.
  • More logs read. Logs → SQL now reads Postgres's csvlog and
    jsonlog, p6spy and datasource-proxy logs, and statements that
    failed with an ERROR:.
  • sslrootcert in a connection string pins the trusted CAs, as in
    libpq.

Added

  • Cell editing: u on a cell in the grid (or a field in row detail)
    edits it in place. Enter writes a one-row UPDATE … WHERE <primary key> and sends it through the safety guard like any editor statement,
    the statement shown in the confirm; Ctrl-N sets NULL (and NULL ↔ the
    empty string). It works on a SELECT from one table with its primary
    key in the result, and otherwise says why not (a join, a view, no
    primary key, the key not selected, a computed column, read-only). The
    UPDATE changes exactly one row or is rolled back, and the cell then
    shows the value as the server stored it. Before it commits, pgman
    checks the server still calls the table, the column and the key what
    pgman's schema cache does; a column renamed since pgman read the
    schema (and another added under its old name) is rolled back with
    the table changed since pgman read it, never written to the wrong
    column.
  • M in the results fetches past the first 1000 rows. A plain SELECT
    from the editor now reads through a server-side cursor; while it has
    more, the title says partial · M fetches more, and each M appends
    1000 rows (up to 100 000 held), keeping the sort and filter. While
    the cursor is open the session is idle in transaction, holding an
    ACCESS SHARE lock on the tables the query reads: DDL on them (a
    migration, ALTER, TRUNCATE, VACUUM FULL, REINDEX) waits behind
    it, and later statements on those tables queue behind that DDL. pgman
    closes the cursor after 60 s without a fetch or 10 min open (sooner if
    the server's own timeouts are), before any other database work, and
    at the end of the result; M then re-runs the query. On a standby
    no cursor is held at all (a recovery conflict would end the session):
    the first page commits at once and M re-runs the query for more.
    EXPLAIN, scripts, \watch, locking SELECTs, a SELECT that calls
    a function other than a short list of read-only built-ins (count,
    coalesce, lower, now, …) — or one of those names that a schema
    has its own function for — and a SELECT inside an open
    transaction stay capped at 1000 as before.
  • Config advisor in the schema wizard (W): read-only findings from the
    server's statistics — buffer cache hit ratio, temp-file spills (with the
    spilling statements from pg_stat_statements), dead tuples and the
    autovacuum setting behind them, lock-wait chains, and requested
    checkpoints (pg_stat_checkpointer from PG17), blamed on
    max_wal_size only when pg_stat_wal shows the WAL to have forced
    them. Each shows its
    evidence, how long the counters have accumulated, a conservative
    setting, and its RDS / Aurora parameter-group name, static or dynamic.
    e yanks a finding's evidence query.
  • Enter on a JDBC tap List row opens the whole event: the full SQL,
    pool / conn / txn, every bound parameter, the error's cause chain
    down to the root cause, and the caller stack. A failed row used to be
    only coloured red, and only the innermost caller frame reached the
    Callers view.
  • The JDBC tap's List view shows the focused statement's bound
    parameters beneath the list, labelled params (some redacted) when
    the producer masked a value and params (withheld by source) for
    OpenTelemetry spans, which carry none.
  • sslrootcert=<file> in a connection string pins the trusted CAs, as
    in libpq (and require with a CA file checks the chain). It used to
    be ignored, with the default trust store used instead. sslcrl and
    sslcrldir are refused rather than ignored.
  • A Postgres log's ERROR: + STATEMENT: pair is reconstructed as a
    query marked failed with the error message — what the default
    log_min_error_statement logs with nothing else on. A statement
    log_statement already logged is marked failed rather than listed
    twice. The log picker marks a failed pick ✗ and shows its error.
  • Logs → SQL reads Postgres's csvlog and jsonlog (PG 15+) formats as
    well as stderr: same pairing of parameters and errors, by pid.
  • Logs → SQL reads p6spy (default single-line format) and
    datasource-proxy query logs, tagged [p6spy] / [ds-proxy] in the
    picker.
  • Tab after :help completes the help section names, as it completes
    command names.
  • docs/tap.md: the JDBC tap for pgman users — the OpenTelemetry, TCP
    and UDP ways in, the v1 event fields, the limits and drop counts,
    --tap-record / --tap-replay, redaction and security.

Changed

  • Ctrl-C in the results cancels a running query, as it does in the
    editor, instead of arming quit; with nothing running it still asks
    for a second press.
  • Y and :fixture say fetched rows only when the result has more
    rows than pgman holds.
  • The result diff (D) matches rows on the table's real primary key
    when A and B are the same columns from one table and the key is in
    both (key: id (primary key) in the title), instead of the leftmost
    column that happens to be unique; otherwise it infers as before.
  • Help is a list of sections and one section at a time instead of one
    ~275-row page: from 110 columns the list sits beside the section, and
    on a narrower terminal the list comes first, each section with a
    one-line summary, and Enter opens one. Tab / ] / [ move between
    sections, / filters every section live (matching rows under their
    headings), and :help all keeps the single page. :help <topic>
    takes any section name or the start of one, and an unknown topic lists
    every section.
  • One size format everywhere: database sizes on the start card and in
    the environments overview, and table sizes in the schema browser, read
    9.0 MB / 212 MB / 1.2 GB. The start card used pg_size_pretty,
    which stays in kB up to 10 MB (10199 kB beside 10 MB), and the
    schema browser used MiB with two decimals.
  • In the editor and the prompts the error footer points at F2 / :error detail, not F2 alone: F2 takes fn on a Mac.
  • The start card's recent list says how to bring an entry back: in the editor, ctrl-p recalls · ctrl-r searches.
  • Loading a saved query (after its :param values, if any) ends the
    status with the key that runs it: ⏎ runs it, or alt-⏎ runs it when
    the statement has no ; and Enter would be a newline.
  • The tab bar's hint reads [ ] prev/next tab instead of ] [ switch,
    which looked like a stray pair of brackets.
  • One TLS crypto provider (ring) instead of two: no cmake / C build of
    aws-lc, and a smaller binary (x86_64 macOS 11.9 MB to 9.1 MB). The
    Postgres connection no longer offers the post-quantum X25519MLKEM768
    key exchange, which only the aws-lc provider had.
  • Logs → SQL: a Postgres FATAL: or PANIC: followed by its
    STATEMENT: (a read replica's terminating connection due to conflict with recovery) is reconstructed as a failed query, like an
    ERROR:'s.

Fixed

  • :readonly on under a writable profile could be undone from the
    editor: SET default_transaction_read_only = off got the other
    guard's confirm, and after y the session wrote while the header still
    said read-only. A session read-only by :readonly on or a standby's
    pin now blocks that SET (and BEGIN READ WRITE, set_config(…)) as
    a read-only profile does, and says :readonly off is the way out.
  • A connection that dropped under a running pgman — a session the server
    ended (pg_terminate_backend, a restart, an idle timeout) or a network
    that went — left the header saying connected, and every later command
    failed with connection closed; the log said only postgres connection error: db error. pgman now shows the connection-failed screen, with
    connection lost and the server's own message (terminating connection due to administrator command), and r reconnects; the log keeps the
    message and its SQLSTATE.
  • A long pattern typed into a footer prompt (the schema browser's and
    help's / filter, the grid's / filter and f find, ctrl-r
    history search) no longer loses its filter: / / find: head or the
    cursor: the head stays, the pattern shows its end after a …, and
    the cursor sits after it. The key hints go first.
  • The JDBC tap monitor read "no events yet", and the footer showed no
    TAP badge, while only commits / rollbacks had arrived; the status
    now counts them (2 commits/rollbacks).
  • The schema wizard, slow queries, sessions and notifications lists,
    and the JDBC tap's List, Hotspots, Callers, Transactions, N+1 and
    Baseline views, scrolled as if their header row were a list row, so
    moving the cursor past the bottom left the focused row hidden one
    line below the edge.
  • After SELECT … FROM ONLY t, I yanked an INSERT INTO public.only:
    ONLY was read as the table's name. It is now skipped; a quoted
    "only" or a schema only.t is still a name.
  • Dependencies: ctutils 0.4.5 (RUSTSEC-2026-0385, non-constant-time
    comparisons in the SCRAM path's digest crates) and clipboard-win
    5.4.3 (5.4.1 was yanked).
  • lo_from_bytea, lo_put, lo_unlink, lo_creat, lowrite and
    lo_truncate classified as plain reads, so a writable connection ran
    them with no confirmation. They now need one, like other writes.
  • If the TLS connector couldn't be built, a require / verify-*
    connection fell back to plaintext. It now fails instead.
  • Help opened from a mode (F1 / ?, or :help editor) now shows the
    section's heading; it was hidden under the ↑ N more above marker,
    so the section opened on its first key.
  • The help overlay's footer shows its own keys; a status left from before
    help opened used to take the footer instead.
  • The digits 1–9 no longer switch editor tabs while help is open:
    the tab changed unseen, behind the page being read.
  • I (row as INSERT) named a quoted mixed-case table bare: a row
    yanked from "OrderLine" was inserted into a lowercase twin
    orderline if there was one, and failed otherwise. The FROM parse
    now folds unquoted names to lower case and keeps quoted ones (spaces
    and "" included), and the INSERT quotes the table. :fixture
    refuses such a table instead of capturing it under the twin's name:
    DBUnit names tables case-insensitively, so no fixture can name it.
    F (FK follow) prefers the exact table over a case-insensitive match.
  • A fixture's apply script quotes a reserved table or column name
    (user), which was a syntax error, and still reads every other
    name as DBUnit does, case-insensitively (<UserAccount Id="1"> is
    useraccount (id)). :fixture refuses, naming it, a column that
    can't round-trip — one with upper-case letters (createdAt) or one
    XML can't carry (a"b, with space, ?column?) — instead of
    renaming it with _ into a column the table doesn't have.
  • \fixture, \report and \i (and their : spellings) take the
    rest of the line as the path, so a path with spaces works; a
    double-quoted path has its quotes removed. They took the first word,
    so :fixture my dir/f.xml wrote ./my.
  • I (row as INSERT), F (FK follow) and the fixture apply script
    write a value holding a backslash as an E'…' string with the
    backslash doubled, so it inserts the same value whatever
    standard_conforming_strings is. With it off, a bytea's \x0102
    was read as the escape \x01.
  • I's footer counted the INSERT's bytes and called them characters;
    it counts characters now. Every yank reports a clipboard failure the
    same way (yank failed: …), and the test suite no longer writes to
    the system clipboard.
  • --batch with a script of several statements, one of them a
    SELECT, says on stderr that a script prints only its status, not
    rows: --format json printed [{"status":"batch executed"}] and the
    rows seemed lost. Run the SELECT on its own to get them.
  • :fixture with a grid filter active captures the rows the filter
    shows, as Y exports them, and the status says N of M row(s) (filtered). It captured every row, hidden ones included.
  • Sorting a column (s) decides once per column whether to compare
    numbers or text: a column that is all numbers sorts by exact value
    (int8 past 2^53 no longer ties, NaN sorts above Infinity as in
    Postgres); any other column sorts as text, byte by byte. Deciding per
    pair was not an order (2 < 10 < 1a < 2), so ascending and descending
    disagreed, and text such as inf or nan sorted as a number.
  • Connected to a standby by :connect or --dsn, nothing said it was
    one; only an environment naming a replica did. Every connect now asks
    pg_is_in_recovery(), and the header wears a replica badge on a
    standby whichever way it was reached. The session is pinned read-only
    there too, as an environment's replica is, whatever safety.toml
    says: the header and start card say read-only, and u and
    :readonly off say it is a replica instead of sending a write the
    server refuses.
  • A result stopped at 1000 rows said only capped at 1000, which read as
    though that was the whole table. It now says more rows not fetched,
    and the status line adds that pgman query '…' --json returns every
    row.
  • The completion popup's title was cut to Tab to cyc: its width was
    measured without the title's decoration.
  • Counts read as English: 1 match · Tab to cycle, 4 saved queries,
    2 environments · 6 databases (the bare first number now says what it
    counts), and the row-detail title row 1 of 50 · field 1 of 24 like
    the cell detail's. Row detail's ↓ N more below counts fields, as the
    title does, rather than wrapped lines of text (↓ 3 more fields below); the cell detail says more lines.
  • A footer tip too wide for the terminal cut a key hint down to its
    label (… expand-all/collapse-all, y… suggestion). Key hints are now
    dropped whole, from the end; prose goes before them, and a prose
    segment that would shrink to a stub (r…) is dropped whole too.
  • pgman --dsn X query 'SQL' failed with clap's "unexpected argument":
    a sub-command (query, env, mcp) now works after global options.
    --help's Usage lists the three sub-commands, and query's no-DSN
    error mentions Spring application*.yml alongside the other configs
    it reads.
  • The no-connection start card wraps at 80 columns instead of cutting
    its lines off mid-word.
  • --redact blocked out pgman's own name — the title read ▓▓▓▓▓ 0.1.0
    — when a database name had pgman in it (pgman_test). The product
    name is now never redacted; the rest of such a name still is.
  • A typo like selec 1; no longer reads as a guarded write. It is still
    guarded (pgman never assumes an unrecognised statement is safe), but the
    confirm card now says "selec isn't an SQL statement pgman recognises —
    send it to Postgres anyway? (it will likely be a syntax error)" rather
    than that a read-only session will refuse it, and pgman query says it
    isn't a statement pgman recognises (typo?) — re-run with --yes to send
    it anyway.
  • After loading a query from the log picker, Ctrl-L / F8 said "no
    queries found", because the loaded query had replaced the log in the
    buffer. With no log in the buffer it now reopens the last log's picker
    (reopened the last log · N queries); a new log replaces it.
  • Two pgman windows no longer lose each other's editor draft. A window
    that never changed the draft writes nothing when it quits, so it can't
    overwrite what another window saved meanwhile; and a window with edits
    of its own, finding draft.sql changed by another window, saves to a
    sibling draft-YYYYMMDD-HHMMSS.sql and says so, instead of clobbering it.
  • The tap monitor's status line kept reading "no events yet" after
    events had arrived; it now follows them.
  • OpenTelemetry spans were stamped with their end time rather than
    their start, which inflated the Pools view's peak concurrency.
  • A burst of tap events no longer piles up in memory behind the
    screen. pgman redrew once per event, so 4 000 large events took over
    30 s to land and held more than 800 MiB on the way; they now land in
    a few frames (under a second, 100–200 MiB at peak). The hop into the
    app is bounded, so events pgman cannot keep up with are dropped and
    counted on the DROP badge instead of queueing without limit.
  • A tap listener that cannot bind (port in use) is now reported on the
    status line and on stderr — --tap-listen, --tap-udp, --tap-otlp
    and the listener a Java project turns on by itself. The status line
    used to announce the port as listening while the failure went only
    to the log.
  • --tap-otlp accepts gzip-compressed bodies, which the OpenTelemetry
    Collector's otlphttp exporter sends by default; they failed with
    400 not utf-8. The inflated body has the same 4 MiB cap as the raw
    one. Any other Content-Encoding gets a 415 saying to send gzip or
    none.
  • A pgman-tap event claiming an absurd duration is clamped to one hour,
    as OpenTelemetry spans already were; u64::MAX showed as
    18446744073709.55s, broke the column alignment and topped every
    sort by time.
  • The tap List view's app column is cut by display width, so a CJK app
    name no longer pushes its row's SQL out of line. A name too long for
    the column ends in ….
  • The tap monitor's status line says 1 query · 1 heartbeat, not
    1 queries · 1 heartbeats, as its title already did.
  • --tap-listen, --tap-udp and --tap-otlp take a host name
    (localhost:7432), which failed as invalid socket address syntax.
    A name that does not resolve says to use an IP address instead.
  • The tap monitor's empty state and status line say where pgman is
    listening (listening on TCP 127.0.0.1:7432 · OTLP/HTTP …). They
    told the operator to start pgman with --tap-otlp :4318 even with a
    listener — that one included — already bound; the hint now points
    the JVM at the bound OTLP port.
  • The tap monitor's title is no longer cut mid-word by its border: the
    room it was fitted to left out the decoration round its name.
  • A producer's huge drop counter no longer pushes the tap monitor's key
    hints off its title: counts from 10 000 up read 12.3k, 4.5M, …,
    1.0e18, and the drop count now comes after the hints, so a narrow
    terminal gives it up first (the status line still shows it).
  • The tap monitor's OpenTelemetry hint gives
    OTEL_EXPORTER_OTLP_TRACES_ENDPOINT=http://localhost:4318/v1/traces.
    Without the path, which the exporter does not add to that variable,
    the OpenTelemetry Java agent's exports got a 404.
  • A write refused by a read-only connection no longer puts the full
    safety.toml path in the footer, where at 80 columns it took three
    rows and was cut anyway. The footer names safety.toml; the F2
    overlay and --batch stderr give the path. And it names
    safety.toml only when the profile made the session read-only: a
    standby, :readonly on, or a SET / BEGIN READ ONLY / role default
    under a writable profile each says so instead of sending the operator
    to a file that already says read_only = false. A database the
    project's .pgman/pgman.toml keeps read-only is described as that
    file's block, by the refusal and by :readonly off, instead of as a
    [databases.<db>] section of safety.toml that the operator's file
    doesn't have.
  • A completion popup the editor opened on its own (after . or FROM )
    that narrows to nothing as you type on now closes without leaving
    completion: no matches for … in the footer. A popup Tab opened still
    says why it closed.
  • NULL and the empty string no longer look the same. The grid
    draws a NULL as a dimmed NULL (a new theme colour, null_cell) and
    an empty string as nothing; row and cell detail say NULL and
    (empty string). The I INSERT yank wrote NULL for an empty
    string, changing the row it copied; it now writes ''.
  • The I yank writes back the row it copied. Every value is a
    quoted literal (a text 007 came back as 7, TRUE as true),
    column names are quoted when they need it (createdAt), and reserved
    words are quoted everywhere pgman builds SQL (a column named order).
  • --format csv and Y write a NULL as an empty field and an empty
    string as "", as COPY … CSV does; --format tsv writes a NULL as
    \N, as COPY's text format does. --format json already wrote
    null, and now does for a multi-statement script too.
  • Sort puts NULLs last ascending and first descending (Postgres's
    defaults) and an empty string first ascending, not with the NULLs.
    /null and f null find NULL cells.
  • F on a NULL foreign key says so instead of opening WHERE id = NULL;
    y on a NULL field leaves the clipboard alone.
  • The result diff counts a NULL → '' change and shows it; \fixture
    leaves NULL cells out (flat XML's NULL) instead of writing col="".
  • Logs → SQL: a Postgres log's bound value with a newline in it no
    longer corrupts the reconstruction. The rest of the value, which
    Postgres writes on the next line, went into the statement's SQL and
    the value was cut short; it now stays in the value.
  • Logs → SQL: a multi-line statement from a Postgres log no longer
    keeps the tab Postgres adds to each continuation line, which changed
    a string literal that spans lines.
  • Logs → SQL: Hibernate logs in logback's default pattern
    (%logger{36} - %msg) are read whole. pgman cut the statement at
    its first : (like 'ref:%', a 10:00:00 timestamp), or dropped a
    statement with no : in it.
  • Logs → SQL: under Spring Boot 3.2's default log pattern, which puts
    [appName] before the thread, a Hibernate bind paired with whichever
    thread logged a statement last. The thread is now the bracket group
    nearest the logger.
  • Logs → SQL: BEGIN / COMMIT / ROLLBACK, SET / SHOW / RESET /
    DISCARD and select 1 pool pings are no longer reported as N+1
    clusters, where they outranked the real one. They stay in the list.
  • Logs → SQL: ANSI colour codes in a pasted log (Hibernate's
    highlight_sql=true, a coloured console or pager) are ignored. A
    highlighted formatted statement was dropped, and a one-line one kept
    the codes in its SQL.
  • Logs → SQL: a Postgres duration: … execute record whose SQL
    contains statement: (like 'statement:%') was cut there, leaving
    the tail of the statement as the query.
  • Logs → SQL: a Hibernate 6 ARRAY bind ([a, b c]) is substituted as
    a Postgres array literal ('{"a","b c"}') instead of a string the
    server rejects as a malformed array.