Hrana value codec.
Hrana represents SQLite values as tagged JSON maps:
%{"type" => "null"}
%{"type" => "integer", "value" => "42"} # i64 carried as a string, for precision
%{"type" => "float", "value" => 3.14}
%{"type" => "text", "value" => "..."}
%{"type" => "blob", "base64" => "..."} # standard base64, no paddingdecode/1 turns a Hrana value into a native Elixir term suitable for binding
into SQLite; encode/1 turns a native term back into a Hrana value. A text
value decodes to a binary and a blob to {:blob, binary}, so the two stay
distinguishable when re-encoding.
Summary
Functions
Decodes a Hrana value map into a native Elixir term.
Encodes a native Elixir term into a Hrana value map.
Encodes a native term straight to Hrana-value JSON iodata — the JSON transports' fast path.
Types
Functions
Decodes a Hrana value map into a native Elixir term.
Encodes a native Elixir term into a Hrana value map.
Encodes a native term straight to Hrana-value JSON iodata — the JSON transports' fast path.
encode/1's tagged map exists only to be consumed by a JSON encoder on those
transports, costing a map + key/tag binaries per cell and a second full traversal by
Jason; row encoding is the dominant per-request CPU of a SQL proxy, and the
intermediate layer roughly doubled it. This emits the final wire bytes directly —
five value shapes, all trivially concatenable. Floats and text delegate to
Jason.encode_to_iodata!/1 so number formatting and string escaping stay
byte-identical to the map path (pinned by test); an invalid-UTF-8 binary falls back
to blob exactly as encode/1 does.
Why the String.valid?/1 pre-scan is back
This branch used to skip the pre-scan and let Jason's own rejection classify the value: try
to encode as text, rescue Jason.EncodeError, fall back to blob. It saved a walk on every
text cell (the map path validates here and escapes in Jason, walking twice), and on
text-only workloads it measured slightly faster.
It is catastrophic on blobs. A blob arrives here as a bare binary — a SQLite driver hands back a plain binary for both TEXT and BLOB cells, so the storage class is already gone — and random bytes are essentially never valid UTF-8. So every blob cell raised and rescued an exception, and building an exception in the BEAM builds a stacktrace. Measured at 64 bytes:
raise/rescue 32.84 µs/value <- blob
pre-scan 0.225 µs/value <- blob, 146x faster
raise/rescue 0.162 µs/value <- ASCII text
pre-scan 0.232 µs/value <- ASCII text, +0.07 µsSo the pre-scan costs ~0.07 µs on a text cell and saves ~32.6 µs on a blob cell: break-even is around one blob per 460 text cells, and a blob-bearing result set is hundreds of times faster. It went unnoticed because every benchmark that drives this path — TPC-B, TPC-C, the wire benches — is INTEGER, REAL and TEXT only, and never put a blob on the wire.
Jason.encode_to_iodata/2 (non-bang) was measured too and is not a fix: it still builds the
error struct, and came in at 22.5 µs/value.
The protobuf transports keep consuming encode/1's maps — this is JSON-only.