Experiment 081: Binary row result storage under select()

Date: 2026-04-20

Status: Rejected

Problem

select() returns row-shaped results as a shared flat List<Object?>

backed by ResultSet / Row. That shape is already lean compared with

materialized maps, but we suspected there might still be headroom in the

result object itself:

The question was whether a more compact internal row format could improve

performance without changing the public List<Map<String, Object?>>

contract.

Hypothesis

Replace the flat row-major List<Object?> backing store with a row-major

binary representation:

Expected outcome:

Approach

Prototype branch:

The decoder opportunistically chose a binary-row builder for result sets

with fixed-width numeric columns, falling back to the ordinary flat path

if later rows violated the expected shape. The Row facade remained

unchanged from the consumer’s point of view.

To avoid fooling ourselves with a transfer-only win, the profile harness

was extended with:

Those workloads separately measured:

  1. db.select() return time
  2. full row consumption on the main isolate

Results

Matched control artifact:

Candidate artifact:

Key deltas versus control:

Numeric-heavy scans

WorkloadControlCandidateDelta
numeric_scan select p503382μs2977μs-12%
numeric_scan_consume select p503394μs2918μs-14%
numeric_scan_consume consume p50234μs1374μs+487%

Mixed-schema scans

WorkloadControlCandidateDelta
mixed_scan select p503127μs3354μs+7%
mixed_scan_consume select p503052μs3888μs+27%
mixed_scan_consume consume p50203μs997μs+391%

Spillover on small results

WorkloadControlCandidateDelta
noop reader floor7μs10μs+43%
point query p507μs10μs+43%

Decision

Rejected.

The prototype improved transfer/return time for some numeric-heavy scans,

but it did so by making row access substantially more expensive on the

main isolate. That is the wrong trade-off for resqlite’s contract, where:

The important learning is that a result-object experiment must be judged

on transfer + consume, not on db.select() return time alone. Once

consumption was measured explicitly, the binary-row direction lost

decisively.

This does not mean result transport is fully solved; it means a

generic binary-row replacement for select() is not the right path under

the current API contract.