Repository navigation
fix(tokenizer,errors): release oversized lineStarts and bound error context on long lines (#614, #615) - #626
Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
|
Claude finished @ajitpratap0's task in 26s —— View job Review of PR #626 (#614 lineStarts retention, #615 bounded error context)I read the diff for Verdict: looks good. I found no blocking issues. #614
|
|
Claude finished @ajitpratap0's task in 20s —— View job Review of PR #626 (#614 lineStarts retention, #615 bounded error context)I read Verdict: looks good. I found no blocking issues. #614:
|
…rs do not retain large inputs (#614) Tokenizer.Reset (called by PutTokenizer and at the start of every Tokenize) kept any lineStarts capacity, so one input with many lines left a pooled tokenizer holding a large backing array for later callers. The issue's repro, "SELECT" + 8M newlines, left cap(lineStarts) = 9038848 (about 72 MB) after PutTokenizer. Reset now keeps the slice (truncated to [0]) only while 0 < cap <= maxRetainedLineStarts (8192 entries, 64 KiB on 64-bit); a larger one is replaced by the default 16-entry slice, matching the existing litBuf and Comments retention caps. load() already re-grows lineStarts for large inputs, so token output and Line/Column spans are unchanged. Doc comments updated. Tests (pkg/sql/tokenizer/linestarts_retention_test.go) failed before the fix (cap 37888 / 8193 / 32768 / 9038848 retained) and pass after: Reset drop, threshold table, positions equal to a fresh tokenizer after a drop, and the 8M-newline repro (skipped under -short). Benchmarks (count=6, benchstat, machine under heavy concurrent load): B/op and allocs/op unchanged for all tokenizer benchmarks except VeryLargeSQL_1MB (+2.4% B/op, +4 allocs/op of ~6125: its lineStarts now reallocates per call, about 8 bytes per line). ns/op deltas were within load noise (a re-run of TokenizerPool/GetPut gave a 21 ns median, equal to baseline; its path is unchanged for small caps). Fixes #614 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UZSk1292jPc8ipDboNxeve
#615) Error(), FormatMultiLineContext and FormatContextWindow printed every context line verbatim, padded the caret with Column-1 spaces and ran strings.Split over the whole SQL on each call, so the size of an error string grew with the input: a 100 KB single-line query produced a ~205 KB E1002 and a ~103 KB E2002 message, and a 1 MiB query a ~1-2 MiB one. All three renderers now share one unexported helper (pkg/errors/context_window.go). Lines of 120 bytes or less render byte-identically to before (locked by golden tests captured from the old code). Longer lines, including the neighbour lines, are clipped to ~60 bytes either side of the error column on UTF-8 boundaries with "..." markers; caret padding is capped by the displayed text and the highlight run by 120. Lines are located by a bounded '\n' scan that stops after the last needed line instead of splitting the whole input. Repro (issue inputs through gosqlx.Parse): 205,129 / 102,714 / 1,048,891 bytes before; 405 / 413 / 414 bytes after. Benchmarks (benchstat): 100 KB-line Error() 937 KiB/op -> 1.3 KiB/op and ~50us -> ~1.6us; FormatMultiLineContext and FormatContextWindow allocate 23-28% less with no slowdown. Fixes #615 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UZSk1292jPc8ipDboNxeve
Bound clipLine's rune-boundary scans to utf8.UTFMax-1 steps so a long run of UTF-8 continuation bytes can no longer push start past end, which made FormatMultiLineContext, FormatContextWindow and (*Error).Error() panic (strings.Builder.Grow: negative count). Valid UTF-8 never needs more than three steps, so output for valid input is unchanged. Adds TestContext_InvalidUTF8LongLine covering clipLine and all three exported entry points on a 300-byte all-0x80 line. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UZSk1292jPc8ipDboNxeve
…st (#614) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UZSk1292jPc8ipDboNxeve
…or byte in long-line context (#615) Two issues in the bounded long-line context window found in adversarial review of #626: 1. Tab-expanded columns used as byte offsets. tokenizer.toSQLPosition counts each '\t' as 4 columns (every other byte as 1), but clipLine and caretPad treated Column-1 as a byte offset. On a line longer than 120 bytes with many tabs before the error, the clipped window was centred past the error and did not show it at all. renderContext now maps the display column to a byte offset per line with the same rule (columnToByte), clips around that byte, and pads the caret by the tab-expanded width of the shown text before it. Verbatim lines keep the historical Column-1 padding, and tab-free lines render byte-identically to before. 2. Rune-boundary snap could drop the error byte. The forward snap of the window start ran even when start was 0, so a line beginning with an invalid UTF-8 byte (e.g. "\x80SELECT ...", or "\xbf" + 121 zeros at Column 1) lost that byte, got a spurious "..." prefix and a caret pointing into the ellipsis. The snap now runs only when start > 0 and never moves start past the error byte. The UTFMax-1 scan bound is kept. Adds TestContext_TabExpandedColumnLongLine, TestContext_ClipNeverHidesErrorByte and FuzzClipLine (seed corpus only in normal runs). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UZSk1292jPc8ipDboNxeve
6aed561 to
2cbce4d
Compare
|
Merge train (batch 3): rebased onto main after #625 (2cbce4d). The adversarial + security pass (Opus) found no must-fix items. It raised two should-fix edge cases in the new long-line context window, both now fixed in 5830e12:
Tests that fail before the fix and pass after were added, plus a |
|
Claude finished @ajitpratap0's task in 21s —— View job Review of PR #626 (#614 lineStarts retention, #615 bounded error context)I read Verdict: looks good. I found no blocking issues. #614
|
Summary
#614 tokenizer: pooled Tokenizer keeps lineStarts capacity of the largest input it has seen
Tokenizer.Reset now keeps the lineStarts backing array only while 0 < cap <= maxRetainedLineStarts (8192 entries, 64 KiB). A larger one is replaced with a fresh 16-entry slice, matching the existing litBuf and Comments retention caps, so pooled tokenizers no longer hold about 72 MB after an input with millions of lines (#614). On the issue's 8M-newline input, lineStarts capacity after Reset drops from 9038848 to 16. Small slices are still reused, and token positions after a drop match a fresh tokenizer. The new tests fail on the parent commit and pass with the fix. The full race gate, go vet and golangci-lint are clean. An interleaved benchstat (n=8) shows no ns/op change. The only cost is +2.4% B/op (+4 allocs) on the 1 MB benchmark, because inputs above about 410 KB now reallocate lineStarts per call. Out of scope by design: changing load()'s len/50+1 pre-size heuristic, capping only in PutTokenizer, and caps for other buffers (already nil'd or capped). Issue #615 (error-string size) lands as a separate commit on this branch.
#615 errors: error string grows with the error column on long single-line input (full source line plus caret padding)
Fixes #615. The SQL context in error messages used to grow with the input: a 100 KB single-line query produced a 205 KB E1002 string, and a 1 MiB query about 1 MB. (*Error).Error(), FormatMultiLineContext and FormatContextWindow now share one unexported renderer (pkg/errors/context_window.go). It finds lines with a bounded scan instead of strings.Split over the whole SQL. Lines over 120 bytes, neighbour lines included, are clipped to about 60 bytes either side of the error column on UTF-8 boundaries, marked with '...'. Caret padding and highlight length are capped. A follow-up commit bounds the rune-boundary scans so invalid UTF-8 cannot panic.
Results:
Deferred, with sound reasons:
Commits (3), one implementation commit per issue plus test-only follow-ups:
7 files changed, 838 insertions(+), 167 deletions(-)Test Plan
CGO_ENABLED=1 go test -race -timeout 300s $(go list ./... | grep -v /cbinding)),go vet,golangci-lint, re-ran the reproduction cases on HEAD, and confirmed the new tests fail on the parent commit.benchstat -count=6before/after on the same machine.#614 evidence (validator)
#614 race gate: PASS. CGO_ENABLED=1 go test -race -timeout 300s $(go list ./... | grep -v /cbinding) exited 0 with 44 packages ok and no FAIL lines. Output is at /private/tmp/claude-501/-Users-ajitpratapsingh-dev-GoSQLX/b99cd3cf-cb92-4294-a0db-d6a7c9a86253/scratchpad/b3/614/val_race.txt. No re-run was needed.
#615 evidence (validator)
#615 race gate: PASS. I ran CGO_ENABLED=1 go test -race -timeout 300s $(go list ./... | grep -v /cbinding) on HEAD 80dbc72: exit 0, 44 packages ok, no FAIL lines, no flakes (log: scratchpad/b3/615/race.txt). go test -short on pkg/errors and pkg/gosqlx also passes.
Breaking Changes
Lines of 120 bytes or less render byte-identically. All new helpers and constants are unexported. The change is documented in the three functions' doc comments.
Notes
gosqlx formatsilently corrupts many expressions; switch the CLI to pkg/formatter #599-schema/postgres: loader misreports index columns and key flags (column order, INCLUDE, expression and partitioned indexes, read-only roles) #620); later PRs in the train are rebased onto this one after it merges.main.Fixes #614
Fixes #615
Follow-up to #566
🤖 Generated with Claude Code
https://claude.ai/code/session_01UZSk1292jPc8ipDboNxeve