rows: cache the parseTime format index per result column

Background

(*conn).parseTime ran on every TEXT-stored DATE / DATETIME / TIMESTAMP column read in Next(). The function tried (*conn).parseTimeString first and then walked parseTimeFormats[0..6] sequentially until time.Parse matched the row's value. For the canonical SQLite TEXT datetime format ("2006-01-02 15:04:05.999999999", index 2) every row paid two failed time.Parse attempts before the matching one. Each failed Parse allocates a time.ParseError, so the per-row cost from the format search alone was ~5 allocs (warmup + ParseError structs + the matching value).

What this MR does

Add a sticky per-column hint cache:

  • rows.parseFmtIdx []int8, sized at newRows() to the column count and initialised to -1 (no match recorded yet).
  • (*conn).parseTime now takes hintIdx int and returns (value, ok, matchedIdx). It tries hintIdx first if in range, then walks the list skipping the index it just tried. Old contract is preserved: on failure the returned value is the original string and ok is false.
  • Next() records the first successful index per column and reuses it on subsequent rows. The cache is sticky: it is set once and not overwritten, so a mixed-format column still pays the original fall-through cost on non-matching rows but a steady column wins on every row after the first.

No API change. The fall-through chain is preserved bit-for-bit so any row the old code would have parsed still parses to the same value.

Benchmark

darwin/arm64 (Apple M3), 1000-row SELECT of a DATETIME TEXT column in the canonical SQLite format, -benchtime=2s, before -> after:

allocs/op B/op ns/op
Baseline 10013 392417 633531
With cache 5019 168672 397843
Diff -50% -57% -37%

The 5 allocs saved per row break down as the two failed time.Parse attempts that the cache skips (each producing a *time.ParseError) plus an internal allocation in the time package's format machinery. Bytes saved (~223 per row) are dominated by those ParseError structures with their captured layout/value strings. ns/op gain is the wall-time cost of running those format-fail paths.

Tests

TestParseTimeFormatCache walks five rows whose TEXT values span three different parseTimeFormats entries (three canonical-format rows that prime the cache at index 2, then one ISO-T row at index 3, then one date-only row at index 6). Each row is scanned as time.Time and asserted against the expected wall-clock value, so any stale-hint regression or fall-through skip bug would surface as a scan failure or a wrong time.

Full go test -count=1 ./... stays green (467s on darwin/arm64).

Backward compatibility

Pure perf fix; no exported API change, no behaviour change visible to callers. The returned driver.Value for every parsed row is identical to before.

Merge request reports

Loading