This repo is a reverse-engineered, pure-Python reader for Tableau .hyper
files (hyperparse.py). There is no public spec for the on-disk format —
everything here was derived by reading Tableau's own engine binary and
cross-checking against the real Hyper API as an oracle. If you are an agent
picking this up cold, read this file before touching hyperparse.py.
-
Use the symbol-rich binary, not the Linux one. The Linux
hyperdis fully stripped (objdump -treturns an empty symbol table). The macOS arm64hyperd(bundled in thetableauhyperapiwheel) ships tens of thousands of demangledhyper::symbols, including the whole storage engine. Get it with:pip download tableauhyperapi --no-deps --only-binary=:all: \ --platform macosx_13_0_arm64 -d ./wheelsDisassemble a named function directly, no Ghidra project needed:
xcrun llvm-objdump -d --disassemble-symbols=<mangled-name> hyperd
-
You have a ground-truth oracle.
pip install tableauhyperapigives you the real engine.hyperparse.py --verify FILEcross-checks a parse against it; theprobe*.pyscripts build synthetic.hyperfiles and check the results against the same API. -
Differential testing beats staring at hexdumps — but reading the function beats both. When a byte-level guess resists a brute-force sweep, that's a signal to go disassemble the relevant function instead of trying more guesses.
-
Correlation is not mechanism. Don't infer a field's meaning or a format rule from a size/byte correlation observed in a single file. Build a second file that varies only the suspected cause before trusting it. Every retracted claim in this project's history came from skipping that step — see the corrections log in the full notes (next section).
The full session-by-session derivation — every field offset, every
compression scheme, the complete corrections log of retracted claims, open
questions, and the tooling reference — lives in REVERSE_ENGINEERING.md in
this directory. That file is gitignored on purpose (it's a working
research log, not user-facing docs), so it will only be present in a
checkout that already has it locally; a fresh clone won't have it. If it's
missing and you need the detailed evidence behind a field or encoding,
regenerate the understanding using the same method: read the engine, use
the API as an oracle, write a probe.
These are the highest-cost lessons from that log, condensed:
- Nullability is a property of the block, not the column. A nullable column with no NULLs in it gets a non-null scheme. Every Tableau-written column is declared nullable, so this bites immediately.
- A DELETE does not rewrite Data Blocks. Deleted rows stay physically
present; a separate
Relation_Metadatatombstone B-tree records which ones are gone. A reader that ignores it returns the right values with too many rows. - Relations over 131,072 rows split into multiple blocks per column. Reading only the first block silently truncates.
- String/Bytea/JSON columns move to the "native" (wide) dictionary entry once any value hits 256+ bytes — the "narrow" family's length is a single byte. Assuming a type family from the column's declared type alone is wrong.
- The domain-size (distinct count) offset depends on the value's byte width, not on a fixed distance from the dictionary.
- Integers are signed. An unsigned read passes every test until a negative value shows up.
- Coverage claims are proven with a probe per claim, not a bigger corpus. Real-world corpora tend to under-exercise edge cases (no long strings, no negative numbers, no relation past the block cap) — a corpus that "cannot fail" isn't evidence.
- No third-party dependencies are required to run
hyperparse.pyitself — LZ4 block decompression and CRC-32C are implemented inline. Only theprobe*.pyscripts andcheckrows.pyneedtableauhyperapi(used purely as a test oracle, never as a runtime dependency of the reader). - When you change decoding logic, re-run
--verifyagainst real files and, iftableauhyperapiis available, the relevantprobe*.pyscript before calling it done. A change that only passes onsample_superstore.hyperis not verified — see the gotchas above for why. tableau_hyper.ksyis a Kaitai Struct definition kept in sync withhyperparse.py;kscheck.pydiffs the two over the corpus and should pass after any format change that touches both.