Skip to content

Add Parquet support to view, less, and a new parquet2tab - #2

Merged
mbreese merged 1 commit into
mainfrom
parquet-support
Jul 28, 2026
Merged

Add Parquet support to view, less, and a new parquet2tab#2
mbreese merged 1 commit into
mainfrom
parquet-support

Conversation

@mbreese

@mbreese mbreese commented Jul 28, 2026

Copy link
Copy Markdown
Member

Adds a parquet2tab subcommand (mirroring csv2tab) and teaches view and less to read Parquet directly.

Parquet is detected from its PAR1 magic bytes, in the same spirit as the existing gzip sniffing, so no flag is needed. An explicit --parquet flag is also available, mirroring --csv.

$ tabl view nested.parquet
id    | name    | price    | day        | tags      | addr.city    | addr.zip    | attrs
======+=========+==========+============+===========+==============+=============+===========
1     | alpha   | 123.45   | 2024-01-01 | ["x","y"] | Boston       | 02115       | {"a":"1"}
2     |         | -0.50    | 1970-01-01 |           |              |             |
3     | gamma   | 1.00     | 2024-10-04 | ["solo"]  | St. Louis    | 63110       | {"b":"2"}

Approach

Readers now sit behind a RecordReader interface. The viewer, pager, and CSV exporter only ever needed ReadLine/Close/header access, so they consume the interface instead of *DelimitedTextFile. exporter.go and sorter.go reach further into the text internals and stay on the concrete type — DelimitedTextFile keeps its exported Header field, so both are untouched.

Converting CSVExporter is what lets parquet2tab reuse it wholesale, including the existing tab-escaping, rather than duplicating a second writer.

Type mapping

This is the substance of the change. parquet.Value.String() renders the physical type only — it would print a DATE as a day count and a DECIMAL(10,2) as its raw unscaled integer. ParquetFile builds a per-column formatter from the logical type instead:

Logical type Rendering
Decimal(p,s) scale applied via big.Int (handles int32/int64/FLBA two's complement)
Date days since epoch → 2024-01-01
Time / Timestamp unit-aware; RFC3339, unzoned when not adjusted to UTC
Integer{BitWidth,IsSigned} unsigned values don't wrap negative
UUID, INT96 decoded to canonical / RFC3339 form

Schema handling: nested structs flatten to dotted names (addr.city); lists and maps render as JSON in one cell; NULLs are empty by default, configurable with --na=STRING.

Pipe limitation

Parquet keeps its schema in a footer at the end of the file, so reading one requires random access. It cannot be read from a pipe — - is rejected with an explicit message rather than failing obscurely. Flags meaningless for Parquet (--no-header, --header-comment, --show-comments, --csv) are rejected when actually set.

Verification

  • make test green; six new tests lock down the formatting that's easy to get wrong (123.45 not 12345, 2024-01-01 not 19723, 4000000000 not negative)
  • 150-row round-trip against examples/iris.txt is identical
  • All error paths checked: pipe rejection, missing file, flag conflicts
  • CGO_ENABLED=0 cross-compiles for linux/macos/windows

Dependency note: parquet-go bumps mattn/go-runewidth v0.0.2 → v0.0.15, which underpins termbox's width calculations. Pager output was diffed before and after, including CJK text, and is byte-identical — no pin needed.

Binary size: 6.5 MB → 15.2 MB (brotli, klauspost/compress, lz4, protobuf). Worth a look if that matters for releases; -ldflags="-s -w" would claw some back.

Known limitation: an empty list and a NULL list both render as the NA string. Distinguishing them needs definition-level inspection, left out of scope.

🤖 Generated with Claude Code

Adds a parquet2tab subcommand (mirroring csv2tab) and teaches view and
less to read Parquet files directly.

Parquet is detected from its PAR1 magic bytes, in the same spirit as the
existing gzip sniffing, so no flag is needed for a .parquet file. An
explicit --parquet flag is also available, mirroring --csv.

To make this work without duplicating the formatting stack, the readers
now sit behind a RecordReader interface. The viewer, pager, and CSV
exporter only ever needed ReadLine/Close/header access, so they consume
the interface instead of *DelimitedTextFile. The exporter and sorter
reach further into the text internals and were left on the concrete
type. DelimitedTextFile keeps its exported Header field, so those two
are untouched.

Type mapping is the substance of the change. parquet.Value.String()
renders the physical type only -- it would print a DATE as a day count
and a DECIMAL(10,2) as its raw unscaled integer -- so ParquetFile builds
a per-column formatter from the logical type instead: decimals get their
scale applied, dates/times/timestamps are decoded from their integer
representations, unsigned ints don't wrap negative, and UUID and the
legacy INT96 timestamps are decoded properly.

Schema handling:
  - nested structs flatten to dotted column names (addr.city)
  - lists and maps render as JSON in a single cell
  - NULL cells are empty by default, configurable with --na=STRING

Parquet keeps its schema in a footer at the end of the file, so reading
one requires random access. It cannot be read from a pipe; "-" is
rejected with an explicit message rather than failing obscurely. Flags
that are meaningless for Parquet (--no-header, --header-comment,
--show-comments, --csv) are rejected when the user actually sets them.

Note that parquet-go pulls mattn/go-runewidth from v0.0.2 to v0.0.15,
which underpins termbox's width calculations. Pager output was diffed
before and after, including CJK text, and is byte-identical.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@mbreese
mbreese merged commit 4f07f51 into main Jul 28, 2026
1 check passed
@mbreese
mbreese deleted the parquet-support branch July 28, 2026 04:19
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant