tsvsheet logo tsvsheet

PostgreSQL

PostgreSQL's COPY text format is tab-separated — a rendered sheet is already a valid COPY stream, and a COPY out is already a sheet. The two formats were made for each other; they just didn't know it.

Everything here runs against grades.tsvt from tsvsheet.examples (fetch it with curl -fsSO https://raw.githubusercontent.com/tsvsheet/tsvsheet.examples/main/grades.tsvt). Add your usual connection flags to each psql; the exact commands below were verified against postgres:17 — see the container section to reproduce them verbatim.

Load a sheet

\copy ... FROM in text format expects tab-separated rows — exactly what tsv render writes. HEADER (PostgreSQL 15+) skips the column row; on older servers, strip it with tail -n +2 instead:

$ psql -c 'CREATE TABLE grades (student text, math numeric, reading numeric, science numeric, average numeric, result text);'
CREATE TABLE
$ tsv render grades.tsvt | psql -c '\copy grades FROM pstdin WITH (FORMAT text, HEADER)' \
    -c 'SELECT result, count(*) AS n, round(avg(average),1) AS avg FROM grades GROUP BY result;'
COPY 4
 result | n | avg
--------+---+------
 Fail   | 1 | 61.7
 Pass   | 3 | 83.0
(2 rows)

The Average and Result columns were =formulas in the source sheet; the database receives their computed values.

Query back out as a sheet

The symmetry runs both ways: \copy ... TO in text format emits a tab-separated grid, header included. Append a =formula row and the query result is a living sheet:

$ { psql -c '\copy (SELECT student AS "Student", average AS "Average" FROM grades ORDER BY average DESC) TO pstdout WITH (FORMAT text, HEADER)'
    printf 'Class average\t=round(avg(B2:B5), 1)\n'; } | tsv render -
Student	Average
Carol	91.3
Alice	84.3
Bob	73.3
Dave	61.7
Class average	77.6

For ad-hoc queries outside \copy, psql -A -F $'\t' -P footer=off produces the same header-plus-rows grid.

No postgres handy?

A throwaway container makes the whole recipe reproducible from nothing — this is verbatim what verified this page:

$ docker run --rm -d --name tsvt-pg -e POSTGRES_PASSWORD=pg postgres:17
$ docker exec tsvt-pg pg_isready -U postgres
$ tsv render grades.tsvt | docker exec -i tsvt-pg psql -U postgres \
    -c 'CREATE TABLE grades (student text, math numeric, reading numeric, science numeric, average numeric, result text);' \
    -c '\copy grades FROM pstdin WITH (FORMAT text, HEADER)' \
    -c 'SELECT result, count(*) AS n, round(avg(average),1) AS avg FROM grades GROUP BY result;'
$ docker stop tsvt-pg    # the --rm container and its data vanish