Skip to content

perf(parser): currentToken as a pointer into p.tokens with a per-parser EOF sentinel [W3-02] - #922

Open
ajitpratap0 wants to merge 5 commits into
mainfrom
perf/w3-02-currenttoken-pointer
Open

ajitpratap0 wants to merge 5 commits into
mainfrom
perf/w3-02-currenttoken-pointer

Conversation

@ajitpratap0

@ajitpratap0 ajitpratap0 commented Nov 1, 2026 •

Copy link
Copy Markdown
Owner

Summary

PLAN W3-02 (PC-3 = ADV-1 = MEM-5), decision D4 option (b).

  • Parser.currentToken is now *models.TokenWithSpan pointing at p.tokens[currentPos], instead of a 72-byte pointerful copy made on every advance/seek (each copy paid a GC write barrier per pointer word plus a memmove).
  • The end-of-input sentinel is per parser: an embedded zero eof models.TokenWithSpan, never a package variable. A stray write can therefore neither race across goroutines nor leak between parsers.
  • peekToken returns a pointer (into p.tokens, or &p.eof past the end).
  • Zero-value &Parser{} stays usable: every parse entry point (parseTokens, parseContextTokens, parseWithRecovery, including the empty-stream case), advance, seek, Reset, Release, NewParser and the pool New point currentToken at &p.eof when no real token applies.
  • nextAdjacent compares End/Start values instead of copying tokens; writeRawLeaf passes the pointer.
  • Mechanical sweep: 9 files, no unrelated edits. Saved/restored cursors (savedTok := p.currentToken) now save a pointer, which is equivalent because p.tokens is never written during a parse.
  • No exported API change. CHANGELOG [Unreleased] entry added.

Behaviour: no golden diff. Beyond the W0-02 goldens, a throwaway differential (not committed) dumped, for every corpus statement x every dialect x truncated prefixes (about 618 MB), the output of ParseFromModelTokens (SQL(), error, all positions), ParseWithRecoveryFromModelTokens (statements and errors) and ParseContextFromModelTokens on origin/main and on this branch: byte-identical.

Test Plan

Tests written first (pkg/sql/parser/currenttoken_pointer_test.go):

  • TestCurrentToken_TracksCursor: currentToken == &tokens[currentPos] (or &p.eof) across advance, seek (including negative), and save/restore, for a corpus of 20 inputs incl. error and empty inputs.
  • TestCurrentToken_PeekToken, TestCurrentToken_NextAdjacent (pins the rewritten helper).
  • TestCurrentToken_SentinelUnmodifiedAfterParsing (the plan's sentinel-unmodified test): all three entry points, error and recovery paths, pooled parser reused across rounds and Reset; p.eof must stay the zero token.
  • Review fixes (5e64df2): Reset and Release must re-point currentToken at &p.eof (TestCurrentToken_ReleaseRepointsSentinel, TestCurrentToken_ResetDropsOversizedBuffer, oversized tokenBuf and caller-owned slice); pool New init and peekToken sentinel identity asserted; empty ParseContextFromModelTokens stream covered. TestCurrentTokenSentinelUnmodifiedOverCorpus (external test) runs all three entry points over the full W0-02 corpus for every dialect, as PLAN W3-02 asks. Fix (a59315c): the dialect is re-applied after each Reset (Reset clears it), so every statement really runs under its dialect, and each stream is also fed with the trailing EOF token stripped so currentToken/peekToken actually reach &p.eof. Overlay mutation check: an unconditional and a clickhouse-gated write to p.eof both fail this test (previously the gated one passed). Mutation check: deleting the re-point at parser.go lines 70, 163, 672 or 734 now fails the suite. Merged origin/main (perf(tokenizer): single fused-hash keyword table [W2-01] #920); no golden diffs.
  • TestCurrentToken_SentinelIsPerParser (D4), TestCurrentToken_ZeroValueParser, TestCurrentToken_EmptyStreamClearsStaleCursor, TestCurrentToken_ConcurrentParsers (-race).
  • Existing tests adjusted for the type: dialect_migration_test.go (two tests used to write through currentToken, they now allocate their own token so they cannot touch a sentinel), paren_query_test.go, type_helpers_test.go, utility_cursor_test.go.

Gates (AC-STD), all run on this branch:

  • task check (fmt, vet, lint, test:race including the W0-02 goldens): pass
  • task test:cbinding: pass
  • go test -count=1 ./pkg/compatibility/...: pass
  • go run honnef.co/go/tools/cmd/staticcheck@latest ./pkg/sql/parser/: clean

Local A/B (Apple M4, shared machine, indicative only)

scripts/perf/ab.sh -g -a -r 10 -t 300ms against origin/main (c05c5fd), 10 interleaved rounds, -cpu 1. A/A control: 0 of 30 benchmarks moved more than 3%. allocs/op and B/op identical on every benchmark.

metric ParseE2E ParseStage/parseonly ParseRW/*/medium
-cpu 1, GOGC=off (gated primary) -0.2% .. -1.1% (suite geomean over 30 benchmarks -1.29%, p<0.001) -0.6% .. -2.8% (geomean -1.9%) -0.8% .. -1.9% (geomean -1.2%)
-cpu 1, default GC (secondary) -2.2% .. -4.2% (all p<0.03) -2.8% .. -9.7% (all p<0.02) -3.2% .. -4.7% (all p<0.001); suite geomean -4.43%
-cpu 4, default GC, 10 ABBA rounds geomean of medians -0.4%, of minima -2.9% (not significant, per-sample noise +-50..150% under load) geomean of medians -2.0%, of minima -2.0% not run

Reading: the gain is the GC write-barrier and copy cost, so it is small with GC off, 3-4% end to end and 6% (geomean) in parse-only with the GC running at -cpu 1, and not distinguishable from noise at -cpu 4. This matches the PLAN evidence (CONFIRMED at -cpu 1, not significant at -cpu 4). Plan gates: parseonly <= -5% at -cpu 1 is met with default GC (geomean about -6%) but not with GOGC=off (-1.9%); E2E <= -2% at -cpu 1 is met with default GC (about -3.3%) but not with GOGC=off (-0.7%). The CI gate's primary metric is GOGC=off, so see the CI verdict below; the miss against the PLAN threshold in that regime is reported, not chased.

Raw outputs: scratchpad perf/wave1/w3-02/ab1/report.md, cpu4-benchstat.txt (not committed).

CI perf gate (amd64, shadow)

Run 41331968001, "A/B perf gate (shadow)": PASS, "4 benchmark(s) faster by 3% or more", no regression, allocs/op and B/op unchanged on every benchmark, A/A control 0 of 26 benchmarks over 3%. amd64 (pinned CPU 3), GOGC=off, -cpu 1, 10 interleaved rounds. Full sticky comment above; summary:

group (primary, GOGC=off, -cpu 1) time vs base
ParseE2E/* (11) -0.55% .. -2.53%; significant (p<0.01) on join, cte, window, complex, update
ParseRW/*/medium (8) -1.42% .. -3.14%, all p<0.01
ParseFresh/{simple_select,join,complex} -27.8%, -44.6%, -38.8% (p<0.001)
PoolGetPut/serial, Tokenize/scratch/* within +-0.25% (unchanged)
suite geomean -6.54% (p<0.001; A/A -0.10%)
instructions/op (perfdriver, e2e) -0.06% .. -0.59%
secondary: default GC, GOMAXPROCS=1, ParseE2E -2.8% .. -5.3%, all p<0.001; suite geomean -8.29%

Against the PLAN gates: E2E <= -2% at -cpu 1 is met with default GC (-2.8% .. -5.3%) and by half of the queries in the GOGC=off primary metric (geomean about -1.5%); parseonly is not in the CI gate suite (local numbers above). -cpu 4 is not measured by the CI gate; the local -cpu 4 result is within noise, as the PLAN predicts.

Note: ParseFresh (-28% to -45%) is far larger than the local steady-state gain. Hypothesis, not verified: the smaller frames from dropping the 72 B copies (the audit measured parseExpression 384 -> 288 B, parseStatement 288 -> 256 B) mean fewer morestack stack growths on a fresh goroutine. It is consistent with the PLAN's note that frame-size effects show on fresh goroutines, but I did not profile it.

Breaking Changes

None. currentToken, eof and peekToken are unexported. Parse results, error codes, locations and AST positions are unchanged (goldens pass, differential identical).

🤖 Generated with Claude Code

https://claude.ai/code/session_01DnbRnTFErhoMJdAjVEeCg2

…er EOF sentinel [W3-02]

advance copied a 72-byte pointerful TokenWithSpan into the Parser on every
token, paying a GC write barrier per pointer word and a memmove. currentToken
now points at p.tokens[currentPos], or at the parser's own zero EOF sentinel
past the end of the stream (decision D4 option b), and peekToken returns a
pointer. Every parse entry point, advance, seek, Reset and Release set the
pointer, so a zero-value &Parser{} stays usable.

Parse results, errors and positions are unchanged: the behaviour goldens pass
and a recovery-path differential over the corpus, every dialect and truncated
variants was byte-identical. New tests pin the cursor invariant, that the
sentinel is per parser and never written, and the zero-value parser.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DnbRnTFErhoMJdAjVEeCg2
@vercel

vercel Bot commented Nov 1, 2026 •

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated
gosqlx-website Ready Ready Preview Nov 1, 2026 7:00pm UTC

@github-actions github-actions Bot added documentation Improvements or additions to documentation parser labels Nov 1, 2026
@github-actions

github-actions Bot commented Nov 1, 2026 •

Copy link
Copy Markdown

Claude finished @ajitpratap0's task in 27s —— View job


Review of PR #922: currentToken as a pointer into p.tokens

Verdict: looks good. I have no blocking issues. This was a read-only review. I didn't run the tests; the results below are from the PR description.

  • Read the diff and the surrounding parser code
  • Checked pointer-validity edge cases
  • Posted feedback

Correctness

  • Pointer validity: currentToken points into p.tokens, which is either the parser-owned tokenBuf or the caller's slice. p.tokens is never written during a parse, so the pointer stays valid.
  • alter_table.go truncation: parseAlterMutation temporarily sets p.tokens = full[:stop]. The slice shares the same backing array, so pointers into it stay valid. After restoring p.tokens, p.seek(p.currentPos) re-points currentToken. This path is safe.
  • Stale pointers: Reset and Release point currentToken at &p.eof. They don't leave it aimed at a scrubbed or dropped tokenBuf.
  • Empty streams: every entry point (parseTokens, parseContextTokens, parseWithRecovery) now clears the stale cursor when len(tokens) == 0. Before this change the old value was kept. The EmptyStreamClearsStaleCursor test covers it.
  • Saved cursors: savedTok := p.currentToken now saves a pointer. This is equivalent to saving a copy because the tokens are read-only during a parse.
  • Per-parser eof: this avoids a package-level sentinel that a stray write could race on or leak between parsers.
  • nextAdjacent: comparing End/Start values is equivalent to the old token copies and avoids them.

Risks and suggestions (non-blocking)

  1. The read-only rule isn't enforced. Nothing in the type system stops p.currentToken.X = .... A write would now mutate the shared token stream. If that stream is the caller's slice, the write would also reach the caller's tokens. The sentinel test only guards p.eof. A cheap guard would be a go vet-style grep or lint rule, or a CI step that fails on currentToken\.[A-Za-z.]+\s*=[^=]. I found no such writes in non-test code, and the PR moved the two tests that did write through it.
  2. Aliasing the caller's tokens: parseTokens takes &tokens[0] from whatever slice it is given. This is fine because the public paths pass tokenBuf. If any other internal caller passes a caller-owned slice, currentToken aliases it and a concurrent caller mutation would be visible. A one-line comment on parseTokens would document the assumption.
  3. Lifetime of peekToken() results: the returned pointer now aliases p.tokens. A caller that holds it across Reset or a new parse would see the new contents. All current uses are short-lived, so this is fine. The doc comment already says read-only.

Tests

The new tests cover the main risks well:

  • the cursor invariant across advance, seek (including negative) and save/restore;
  • the sentinel staying zero across all entry points, error and recovery paths, and pool reuse;
  • per-parser sentinel isolation;
  • the zero-value parser;
  • a -race concurrent test.

The byte-identical differential run adds good confidence.

Performance

The reported win is modest. It is about 1% with GC off and 3–4% end to end with GC on at -cpu 1. allocs/op and B/op are unchanged. The PR is upfront that the PLAN thresholds are missed under GOGC=off, and I agree with that framing. The CI perf gate section is still marked pending in the description.

Minor

  • The struct field alignment in Parser changed: currentPos and currentToken are now separated from depth by a comment block, so gofmt aligns them differently. This is cosmetic only.
  • The CHANGELOG entry is present.
    · branch perf/w3-02-currenttoken-pointer

@github-actions

github-actions Bot commented Nov 1, 2026 •

Copy link
Copy Markdown

Perf gate: PASS (shadow mode: report only, this check never fails)

Base d29c64466939, head 49fabd625dee. 10 interleaved rounds, benchtime 200ms, -cpu 1, pinned to CPU 3 (SMT siblings 2-3) with taskset.

  • 6 benchmark(s) faster by 3% or more

Rules: time regresses at >= +3% with p < 0.01 (Mann-Whitney U), or a suite geomean >= +1.5% (per-round geomeans, same test); allocs/op must not increase; B/op must not rise more than 2%; instructions/op must not rise more than 1.5%. An A/A control with more than 10% of benchmarks moving over 3% is noisy: re-run once, then inconclusive (neutral).

Gated suite (GOGC=off, GOMAXPROCS=1)

benchmark time p A/A B/op allocs/op status
ParseE2E/simple_select -3.14% <0.001 -0.09% +0.00% 5 → 5 faster
ParseE2E/select_where -2.16% <0.001 -0.08% +0.00% 18 → 18 ok
ParseE2E/join -1.38% 0.001 -0.32% +0.00% 17 → 17 ok
ParseE2E/aggregate -2.61% <0.001 -0.26% +0.00% 18 → 18 ok
ParseE2E/subquery -3.33% <0.001 -0.17% +0.00% 14 → 14 faster
ParseE2E/cte -1.87% <0.001 +0.14% +0.00% 21 → 21 ok
ParseE2E/window -2.68% <0.001 -0.14% +0.00% 11 → 11 ok
ParseE2E/complex -1.63% 0.004 -0.03% +0.00% 50 → 50 ok
ParseE2E/insert -2.04% <0.001 +0.06% +0.00% 17 → 17 ok
ParseE2E/update -2.34% <0.001 +0.05% +0.00% 13 → 13 ok
ParseE2E/delete -1.54% <0.001 +0.23% +0.00% 5 → 5 ok
ParseRW/postgresql/medium -0.76% <0.001 +0.11% -0.00% 646 → 646 ok
ParseRW/mysql/medium -2.41% <0.001 -0.41% -0.00% 365 → 365 ok
ParseRW/mariadb/medium -1.49% <0.001 +0.21% -0.00% 523 → 523 ok
ParseRW/sqlserver/medium -2.35% <0.001 -0.14% -0.00% 936 → 936 ok
ParseRW/oracle/medium -1.73% <0.001 +0.10% -0.00% 1093 → 1093 ok
ParseRW/sqlite/medium -1.91% <0.001 +0.09% +0.00% 346 → 346 ok
ParseRW/snowflake/medium -0.74% 0.003 +0.23% +0.00% 777 → 777 ok
ParseRW/clickhouse/medium -1.26% 0.075 +0.28% +0.00% 384 → 384 ok
ParseFresh/simple_select -30.96% <0.001 +0.33% +0.00% 6 → 6 faster
ParseFresh/join -50.06% <0.001 +0.26% +0.00% 18 → 18 faster
ParseFresh/complex -35.22% <0.001 +0.22% +0.00% 51 → 51 faster
PoolGetPut/serial +0.82% <0.001 -0.05% +0.00% 0 → 0 ok
Tokenize/scratch/join +0.43% 0.058 -0.09% +0.00% 0 → 0 ok
Tokenize/scratch/complex +0.10% 0.161 +0.48% +0.00% 0 → 0 ok
Tokenize/scratch/insert -0.05% 0.264 -0.09% +0.00% 0 → 0 ok
Normalize/simple_select -1.78% <0.001 +0.00% +0.00% 10 → 10 ok
Normalize/join +0.56% 0.002 +0.09% +0.00% 40 → 40 ok
Normalize/complex +0.39% 0.123 +0.49% +0.00% 110 → 110 ok
TransformParse/simple_select -5.04% <0.001 +0.34% +0.00% 5 → 5 faster
TransformParse/join -1.97% <0.001 +0.04% +0.00% 17 → 17 ok
TransformParse/complex -1.94% <0.001 -0.09% +0.00% 50 → 50 ok
geomean -5.95% <0.001 +0.06%

A/A control: 0 of 32 benchmarks moved more than 3%.

Instructions per operation (cmd/perfdriver)

case instructions/op status
e2e/aggregate -0.39% ok
e2e/complex -0.45% ok
e2e/cte -0.32% ok
e2e/delete -0.21% ok
e2e/insert -0.67% ok
e2e/join -0.45% ok
e2e/select_where -0.23% ok
e2e/simple_select -0.21% ok
e2e/subquery -0.19% ok
e2e/update -0.35% ok
e2e/window -0.30% ok
rw/postgresql/medium -0.54% ok
Secondary metric: default GC, GOMAXPROCS=1 (informational)
benchmark time p
ParseE2E/simple_select -5.07% <0.001
ParseE2E/select_where -4.19% <0.001
ParseE2E/join -5.24% <0.001
ParseE2E/aggregate -5.16% <0.001
ParseE2E/subquery -4.44% <0.001
ParseE2E/cte -4.39% <0.001
ParseE2E/window -3.91% <0.001
ParseE2E/complex -5.10% <0.001
ParseE2E/insert -2.83% <0.001
ParseE2E/update -3.99% <0.001
ParseE2E/delete -5.57% <0.001
ParseRW/postgresql/medium -3.13% <0.001
ParseRW/mysql/medium -6.10% <0.001
ParseRW/mariadb/medium -5.54% <0.001
ParseRW/sqlserver/medium -3.75% <0.001
ParseRW/oracle/medium -3.25% <0.001
ParseRW/sqlite/medium -4.20% <0.001
ParseRW/snowflake/medium -5.24% <0.001
ParseRW/clickhouse/medium -4.74% <0.001
ParseFresh/simple_select -31.26% <0.001
ParseFresh/join -48.80% <0.001
ParseFresh/complex -34.68% <0.001
PoolGetPut/serial +0.92% <0.001
Tokenize/scratch/join -0.07% 0.732
Tokenize/scratch/complex -0.51% 0.173
Tokenize/scratch/insert -0.05% 0.939
Normalize/simple_select -1.83% <0.001
Normalize/join -2.07% 0.004
Normalize/complex -2.22% <0.001
TransformParse/simple_select -7.29% <0.001
TransformParse/join -5.53% <0.001
TransformParse/complex -4.70% <0.001
geomean -7.77%
Environment
date:        2026-11-01T19:01:17Z
base:        d29c64466939295f6c00b5283411faec33fb6175
head:        working tree of 49fabd625dee53d1beb8cea68e12de42115bf213
go:          go version go1.26.1 linux/amd64
kernel:      Linux 7.0.0-1017-azure x86_64
pinning:     pinned to CPU 3 (SMT siblings 2-3) with taskset
clocksource: tsc
load:        1.37, 0.57, 0.23
instr count: cachegrind
perf probe:  Error: No supported events found. The instructions:u event is not supported. 

Architecture:                            x86_64
CPU(s):                                  4
Model name:                              Intel(R) Xeon(R) Platinum 8370C CPU @ 2.80GHz
Thread(s) per core:                      2
Core(s) per socket:                      2
Socket(s):                               1
CPU(s) scaling MHz:                      125%
Hypervisor vendor:                       Microsoft
L1d cache:                               96 KiB (2 instances)
L1i cache:                               64 KiB (2 instances)
L2 cache:                                2.5 MiB (2 instances)
L3 cache:                                48 MiB (1 instance)
benchstat (primary metric)
goos: linux
goarch: amd64
pkg: github.com/ajitpratap0/GoSQLX/pkg/fingerprint
cpu: Intel(R) Xeon(R) Platinum 8370C CPU @ 2.80GHz
                        │    base     │                head                │
                        │   sec/op    │   sec/op     vs base               │
Normalize/simple_select   2.634µ ± 0%   2.587µ ± 0%  -1.78% (p=0.000 n=10)
Normalize/join            9.182µ ± 0%   9.234µ ± 0%  +0.56% (p=0.002 n=10)
Normalize/complex         23.01µ ± 0%   23.10µ ± 1%       ~ (p=0.123 n=10)
geomean                   8.225µ        8.202µ       -0.28%

                        │     base     │                 head                  │
                        │     B/op     │     B/op      vs base                 │
Normalize/simple_select   1.016Ki ± 0%   1.016Ki ± 0%       ~ (p=1.000 n=10) ¹
Normalize/join            3.234Ki ± 0%   3.234Ki ± 0%       ~ (p=1.000 n=10) ¹
Normalize/complex         8.945Ki ± 0%   8.945Ki ± 0%       ~ (p=1.000 n=10) ¹
geomean                   3.086Ki        3.086Ki       +0.00%
¹ all samples are equal

                        │    base    │                head                 │
                        │ allocs/op  │ allocs/op   vs base                 │
Normalize/simple_select   10.00 ± 0%   10.00 ± 0%       ~ (p=1.000 n=10) ¹
Normalize/join            40.00 ± 0%   40.00 ± 0%       ~ (p=1.000 n=10) ¹
Normalize/complex         110.0 ± 0%   110.0 ± 0%       ~ (p=1.000 n=10) ¹
geomean                   35.30        35.30       +0.00%
¹ all samples are equal

pkg: github.com/ajitpratap0/GoSQLX/pkg/gosqlx
                          │    base     │                head                 │
                          │   sec/op    │   sec/op     vs base                │
ParseE2E/simple_select      1.609µ ± 1%   1.558µ ± 0%   -3.14% (p=0.000 n=10)
ParseE2E/select_where       3.631µ ± 0%   3.552µ ± 1%   -2.16% (p=0.000 n=10)
ParseE2E/join               5.279µ ± 1%   5.206µ ± 0%   -1.38% (p=0.000 n=10)
ParseE2E/aggregate          4.159µ ± 2%   4.051µ ± 1%   -2.61% (p=0.000 n=10)
ParseE2E/subquery           3.480µ ± 1%   3.364µ ± 0%   -3.33% (p=0.000 n=10)
ParseE2E/cte                4.777µ ± 2%   4.688µ ± 1%   -1.87% (p=0.000 n=10)
ParseE2E/window             3.151µ ± 2%   3.067µ ± 0%   -2.68% (p=0.000 n=10)
ParseE2E/complex            13.88µ ± 1%   13.66µ ± 2%   -1.63% (p=0.004 n=10)
ParseE2E/insert             2.495µ ± 1%   2.444µ ± 0%   -2.04% (p=0.000 n=10)
ParseE2E/update             2.221µ ± 0%   2.169µ ± 0%   -2.34% (p=0.000 n=10)
ParseE2E/delete             1.332µ ± 0%   1.311µ ± 0%   -1.54% (p=0.000 n=10)
ParseRW/postgresql/medium   166.4µ ± 0%   165.1µ ± 0%   -0.76% (p=0.000 n=10)
ParseRW/mysql/medium        93.90µ ± 0%   91.63µ ± 0%   -2.41% (p=0.000 n=10)
ParseRW/mariadb/medium      137.1µ ± 0%   135.0µ ± 1%   -1.49% (p=0.000 n=10)
ParseRW/sqlserver/medium    298.6µ ± 0%   291.6µ ± 1%   -2.35% (p=0.000 n=10)
ParseRW/oracle/medium       360.1µ ± 0%   353.9µ ± 1%   -1.73% (p=0.000 n=10)
ParseRW/sqlite/medium       78.64µ ± 1%   77.14µ ± 1%   -1.91% (p=0.000 n=10)
ParseRW/snowflake/medium    204.5µ ± 0%   203.0µ ± 1%   -0.74% (p=0.003 n=10)
ParseRW/clickhouse/medium   91.16µ ± 1%   90.01µ ± 3%        ~ (p=0.075 n=10)
ParseFresh/simple_select    9.774µ ± 0%   6.748µ ± 0%  -30.96% (p=0.000 n=10)
ParseFresh/join             21.90µ ± 0%   10.94µ ± 0%  -50.06% (p=0.000 n=10)
ParseFresh/complex          32.39µ ± 1%   20.98µ ± 0%  -35.22% (p=0.000 n=10)
PoolGetPut/serial           87.51n ± 0%   88.23n ± 0%   +0.82% (p=0.000 n=10)
geomean                     13.71µ        12.64µ        -7.80%

                          │     base     │                head                 │
                          │     B/s      │     B/s       vs base               │
ParseE2E/simple_select      15.42Mi ± 1%   15.92Mi ± 0%  +3.25% (p=0.000 n=10)
ParseE2E/select_where       27.06Mi ± 0%   27.65Mi ± 1%  +2.20% (p=0.000 n=10)
ParseE2E/join               22.76Mi ± 1%   23.08Mi ± 0%  +1.40% (p=0.000 n=10)
ParseE2E/aggregate          19.26Mi ± 2%   19.77Mi ± 1%  +2.67% (p=0.000 n=10)
ParseE2E/subquery           22.47Mi ± 1%   23.25Mi ± 0%  +3.44% (p=0.000 n=10)
ParseE2E/cte                24.76Mi ± 2%   25.23Mi ± 1%  +1.91% (p=0.000 n=10)
ParseE2E/window             28.45Mi ± 2%   29.23Mi ± 0%  +2.75% (p=0.000 n=10)
ParseE2E/complex            25.96Mi ± 1%   26.39Mi ± 2%  +1.65% (p=0.003 n=10)
ParseE2E/insert             29.05Mi ± 1%   29.66Mi ± 0%  +2.10% (p=0.000 n=10)
ParseE2E/update             23.62Mi ± 0%   24.18Mi ± 0%  +2.38% (p=0.000 n=10)
ParseE2E/delete             37.24Mi ± 0%   37.83Mi ± 1%  +1.59% (p=0.000 n=10)
ParseRW/postgresql/medium   21.56Mi ± 0%   21.72Mi ± 0%  +0.75% (p=0.000 n=10)
ParseRW/mysql/medium        28.82Mi ± 0%   29.54Mi ± 0%  +2.46% (p=0.000 n=10)
ParseRW/mariadb/medium      25.87Mi ± 0%   26.26Mi ± 1%  +1.51% (p=0.000 n=10)
ParseRW/sqlserver/medium    22.28Mi ± 0%   22.82Mi ± 1%  +2.42% (p=0.000 n=10)
ParseRW/oracle/medium       23.28Mi ± 0%   23.68Mi ± 1%  +1.74% (p=0.000 n=10)
ParseRW/sqlite/medium       26.27Mi ± 1%   26.77Mi ± 1%  +1.92% (p=0.000 n=10)
ParseRW/snowflake/medium    24.86Mi ± 0%   25.04Mi ± 1%  +0.73% (p=0.002 n=10)
ParseRW/clickhouse/medium   24.72Mi ± 1%   25.03Mi ± 3%       ~ (p=0.075 n=10)
geomean                     24.55Mi        25.05Mi       +2.01%

                          │      base      │                 head                  │
                          │      B/op      │     B/op      vs base                 │
ParseE2E/simple_select        816.0 ± 0%       816.0 ± 0%       ~ (p=1.000 n=10) ¹
ParseE2E/select_where       1.523Ki ± 0%     1.523Ki ± 0%       ~ (p=1.000 n=10) ¹
ParseE2E/join               2.305Ki ± 0%     2.305Ki ± 0%       ~ (p=1.000 n=10) ¹
ParseE2E/aggregate          1.812Ki ± 0%     1.812Ki ± 0%       ~ (p=1.000 n=10) ¹
ParseE2E/subquery           1.828Ki ± 0%     1.828Ki ± 0%       ~ (p=1.000 n=10) ¹
ParseE2E/cte                2.336Ki ± 0%     2.336Ki ± 0%       ~ (p=1.000 n=10) ¹
ParseE2E/window             1.281Ki ± 0%     1.281Ki ± 0%       ~ (p=1.000 n=10) ¹
ParseE2E/complex            6.399Ki ± 0%     6.399Ki ± 0%       ~ (p=1.000 n=10) ¹
ParseE2E/insert               824.0 ± 0%       824.0 ± 0%       ~ (p=1.000 n=10) ¹
ParseE2E/update               736.0 ± 0%       736.0 ± 0%       ~ (p=1.000 n=10) ¹
ParseE2E/delete               400.0 ± 0%       400.0 ± 0%       ~ (p=1.000 n=10) ¹
ParseRW/postgresql/medium   70.52Ki ± 0%     70.52Ki ± 0%       ~ (p=0.132 n=10)
ParseRW/mysql/medium        43.73Ki ± 0%     43.73Ki ± 0%       ~ (p=0.011 n=10)
ParseRW/mariadb/medium      64.84Ki ± 0%     64.84Ki ± 0%  -0.00% (p=0.005 n=10)
ParseRW/sqlserver/medium    90.60Ki ± 0%     90.60Ki ± 0%  -0.00% (p=0.001 n=10)
ParseRW/oracle/medium       113.8Ki ± 0%     113.8Ki ± 0%  -0.00% (p=0.000 n=10)
ParseRW/sqlite/medium       39.89Ki ± 0%     39.89Ki ± 0%       ~ (p=1.000 n=10) ¹
ParseRW/snowflake/medium    83.38Ki ± 0%     83.38Ki ± 0%       ~ (p=1.000 n=10) ¹
ParseRW/clickhouse/medium   37.13Ki ± 0%     37.13Ki ± 0%       ~ (p=1.000 n=10) ¹
ParseFresh/simple_select      896.0 ± 0%       896.0 ± 0%       ~ (p=1.000 n=10) ¹
ParseFresh/join             2.383Ki ± 0%     2.383Ki ± 0%       ~ (p=1.000 n=10) ¹
ParseFresh/complex          6.477Ki ± 0%     6.477Ki ± 0%       ~ (p=1.000 n=10) ¹
PoolGetPut/serial             0.000 ± 0%       0.000 ± 0%       ~ (p=1.000 n=10) ¹
geomean                                  ²                 -0.00%                ²
¹ all samples are equal
² summaries must be >0 to compute geomean

                          │     base      │                 head                 │
                          │   allocs/op   │  allocs/op   vs base                 │
ParseE2E/simple_select       5.000 ± 0%      5.000 ± 0%       ~ (p=1.000 n=10) ¹
ParseE2E/select_where        18.00 ± 0%      18.00 ± 0%       ~ (p=1.000 n=10) ¹
ParseE2E/join                17.00 ± 0%      17.00 ± 0%       ~ (p=1.000 n=10) ¹
ParseE2E/aggregate           18.00 ± 0%      18.00 ± 0%       ~ (p=1.000 n=10) ¹
ParseE2E/subquery            14.00 ± 0%      14.00 ± 0%       ~ (p=1.000 n=10) ¹
ParseE2E/cte                 21.00 ± 0%      21.00 ± 0%       ~ (p=1.000 n=10) ¹
ParseE2E/window              11.00 ± 0%      11.00 ± 0%       ~ (p=1.000 n=10) ¹
ParseE2E/complex             50.00 ± 0%      50.00 ± 0%       ~ (p=1.000 n=10) ¹
ParseE2E/insert              17.00 ± 0%      17.00 ± 0%       ~ (p=1.000 n=10) ¹
ParseE2E/update              13.00 ± 0%      13.00 ± 0%       ~ (p=1.000 n=10) ¹
ParseE2E/delete              5.000 ± 0%      5.000 ± 0%       ~ (p=1.000 n=10) ¹
ParseRW/postgresql/medium    646.0 ± 0%      646.0 ± 0%       ~ (p=1.000 n=10) ¹
ParseRW/mysql/medium         365.0 ± 0%      365.0 ± 0%       ~ (p=1.000 n=10) ¹
ParseRW/mariadb/medium       523.0 ± 0%      523.0 ± 0%       ~ (p=1.000 n=10) ¹
ParseRW/sqlserver/medium     936.0 ± 0%      936.0 ± 0%       ~ (p=1.000 n=10) ¹
ParseRW/oracle/medium       1.093k ± 0%     1.093k ± 0%       ~ (p=1.000 n=10) ¹
ParseRW/sqlite/medium        346.0 ± 0%      346.0 ± 0%       ~ (p=1.000 n=10) ¹
ParseRW/snowflake/medium     777.0 ± 0%      777.0 ± 0%       ~ (p=1.000 n=10) ¹
ParseRW/clickhouse/medium    384.0 ± 0%      384.0 ± 0%       ~ (p=1.000 n=10) ¹
ParseFresh/simple_select     6.000 ± 0%      6.000 ± 0%       ~ (p=1.000 n=10) ¹
ParseFresh/join              18.00 ± 0%      18.00 ± 0%       ~ (p=1.000 n=10) ¹
ParseFresh/complex           51.00 ± 0%      51.00 ± 0%       ~ (p=1.000 n=10) ¹
PoolGetPut/serial            0.000 ± 0%      0.000 ± 0%       ~ (p=1.000 n=10) ¹
geomean                                 ²                +0.00%                ²
¹ all samples are equal
² summaries must be >0 to compute geomean

                          │    base     │                head                │
                          │  sec/stmt   │  sec/stmt    vs base               │
ParseRW/postgresql/medium   13.87µ ± 0%   13.76µ ± 0%  -0.75% (p=0.000 n=10)
ParseRW/mysql/medium        11.74µ ± 0%   11.45µ ± 0%  -2.41% (p=0.000 n=10)
ParseRW/mariadb/medium      15.23µ ± 0%   15.00µ ± 1%  -1.49% (p=0.000 n=10)
ParseRW/sqlserver/medium    14.22µ ± 0%   13.89µ ± 1%  -2.35% (p=0.000 n=10)
ParseRW/oracle/medium       15.01µ ± 0%   14.75µ ± 1%  -1.73% (p=0.000 n=10)
ParseRW/sqlite/medium       13.11µ ± 1%   12.86µ ± 1%  -1.91% (p=0.000 n=10)
ParseRW/snowflake/medium    15.73µ ± 0%   15.61µ ± 1%  -0.74% (p=0.003 n=10)
ParseRW/clickhouse/medium   13.02µ ± 1%   12.86µ ± 3%       ~ (p=0.072 n=10)
geomean                     13.93µ        13.71µ       -1.58%

pkg: github.com/ajitpratap0/GoSQLX/pkg/sql/tokenizer
                         │    base     │                head                │
                         │   sec/op    │   sec/op     vs base               │
Tokenize/scratch/join      2.187µ ± 0%   2.197µ ± 4%       ~ (p=0.054 n=10)
Tokenize/scratch/complex   5.901µ ± 1%   5.907µ ± 1%       ~ (p=0.160 n=10)
Tokenize/scratch/insert    1.062µ ± 1%   1.062µ ± 0%       ~ (p=0.262 n=10)
geomean                    2.393µ        2.397µ       +0.16%

                         │     base     │                head                 │
                         │     B/s      │     B/s       vs base               │
Tokenize/scratch/join      54.95Mi ± 0%   54.71Mi ± 4%       ~ (p=0.060 n=10)
Tokenize/scratch/complex   61.09Mi ± 1%   61.03Mi ± 1%       ~ (p=0.136 n=10)
Tokenize/scratch/insert    68.23Mi ± 1%   68.29Mi ± 0%       ~ (p=0.224 n=10)
geomean                    61.18Mi        61.09Mi       -0.15%

                         │     base     │                head                 │
                         │     B/op     │    B/op     vs base                 │
Tokenize/scratch/join      0.000 ± 0%     0.000 ± 0%       ~ (p=1.000 n=10) ¹
Tokenize/scratch/complex   0.000 ± 0%     0.000 ± 0%       ~ (p=1.000 n=10) ¹
Tokenize/scratch/insert    0.000 ± 0%     0.000 ± 0%       ~ (p=1.000 n=10) ¹
geomean                               ²               +0.00%                ²
¹ all samples are equal
² summaries must be >0 to compute geomean

                         │     base     │                head                 │
                         │  allocs/op   │ allocs/op   vs base                 │
Tokenize/scratch/join      0.000 ± 0%     0.000 ± 0%       ~ (p=1.000 n=10) ¹
Tokenize/scratch/complex   0.000 ± 0%     0.000 ± 0%       ~ (p=1.000 n=10) ¹
Tokenize/scratch/insert    0.000 ± 0%     0.000 ± 0%       ~ (p=1.000 n=10) ¹
geomean                               ²               +0.00%                ²
¹ all samples are equal
² summaries must be >0 to compute geomean

pkg: github.com/ajitpratap0/GoSQLX/pkg/transform
                             │    base     │                head                │
                             │   sec/op    │   sec/op     vs base               │
TransformParse/simple_select   1.616µ ± 2%   1.535µ ± 0%  -5.04% (p=0.000 n=10)
TransformParse/join            5.700µ ± 1%   5.588µ ± 1%  -1.97% (p=0.000 n=10)
TransformParse/complex         14.81µ ± 1%   14.53µ ± 0%  -1.94% (p=0.000 n=10)
geomean                        5.148µ        4.994µ       -3.00%

                             │     base     │                 head                  │
                             │     B/op     │     B/op      vs base                 │
TransformParse/simple_select     816.0 ± 0%     816.0 ± 0%       ~ (p=1.000 n=10) ¹
TransformParse/join            2.305Ki ± 0%   2.305Ki ± 0%       ~ (p=1.000 n=10) ¹
TransformParse/complex         6.398Ki ± 0%   6.398Ki ± 0%       ~ (p=1.000 n=10) ¹
geomean                        2.273Ki        2.273Ki       +0.00%
¹ all samples are equal

                             │    base    │                head                 │
                             │ allocs/op  │ allocs/op   vs base                 │
TransformParse/simple_select   5.000 ± 0%   5.000 ± 0%       ~ (p=1.000 n=10) ¹
TransformParse/join            17.00 ± 0%   17.00 ± 0%       ~ (p=1.000 n=10) ¹
TransformParse/complex         50.00 ± 0%   50.00 ± 0%       ~ (p=1.000 n=10) ¹
geomean                        16.20        16.20       +0.00%
¹ all samples are equal

Run, raw data and benchstat

ajitpratap0 and others added 2 commits November 1, 2026 23:25
… over the full corpus [W3-02]

Require currentToken == &p.eof after Reset and Release, cover an oversized
tokenBuf, the pool New init, peekToken sentinel identity and the empty
ParseContextFromModelTokens stream, and drive the sentinel-unmodified check
over the W0-02 corpus for every dialect through all three entry points.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DnbRnTFErhoMJdAjVEeCg2
…ams in sentinel corpus test [W3-02]

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DnbRnTFErhoMJdAjVEeCg2
@github-actions

github-actions Bot commented Nov 1, 2026 •

Copy link
Copy Markdown

Claude finished @ajitpratap0's task in 28s —— View job


Review of PR #922: currentToken as a pointer into p.tokens

Verdict: looks good, with no blocking issues. This was a read-only review. I read the diff for parser.go, recovery.go, expressions_ops.go, utility_stmt.go and ddl_columns.go. I did not run the tests, so the test and benchmark results below come from the PR description and CI.

Correctness

  • Entry points: parseTokens, parseContextTokens and parseWithRecovery now point currentToken at &p.eof for an empty stream. Before, they kept whatever stale value was there.
  • Cursor movement: advance and seek re-point currentToken into p.tokens, or at &p.eof past the end. Reset, Release, NewParser and the pool New all start from &p.eof, so a zero-value &Parser{} still works.
  • Saved cursors: savedTok := p.currentToken now saves a pointer. This is equivalent to saving a copy, as long as nothing writes to p.tokens while parsing.
  • nextAdjacent: it compares End and Start values and gives the same result as the old copies.
  • writeRawLeaf: it passes the pointer, which fits the new type.
  • Per-parser eof: this avoids a shared package-level sentinel that could race or leak between parsers.

Non-blocking suggestions

  1. The read-only rule isn't enforced by the compiler. A write such as p.currentToken.X = ... would now change the shared token stream. If that stream is the caller's slice, the write would also change the caller's tokens. TestCurrentToken_SentinelUnmodifiedAfterParsing only guards p.eof. A cheap guard would be a CI grep for currentToken\.[A-Za-z.]+\s*=[^=] in non-test code.
  2. Aliasing the caller's slice: parseTokens takes &tokens[0] from the slice it is given. That is fine while the public paths pass the parser-owned tokenBuf. A one-line comment on parseTokens would record that assumption.
  3. Lifetime of peekToken() results: the returned pointer aliases p.tokens. A caller that held it across Reset or a new parse would see different contents. Current uses are short-lived, so this is fine. The doc comment already says the result is read-only.
  4. alter_table.go truncation: parseAlterMutation temporarily reslices p.tokens. I did not re-read that file in this pass. The earlier review judged it safe, because the slice shares the same backing array and p.seek re-points currentToken after the restore.

Tests
Coverage looks solid:

  • the cursor invariant across advance, seek (including negative) and save/restore;
  • the sentinel staying zero across all three entry points, error and recovery paths, and pool reuse;
  • per-parser sentinel isolation;
  • the zero-value parser;
  • a -race concurrent test.

The byte-identical differential run in the description adds further confidence.

Performance
The CI perf gate passed:

  • The suite geomean is -6.54%.
  • ParseE2E is down 0.55% to 2.53%, and ParseRW/*/medium is down 1.42% to 3.14%.
  • allocs/op and B/op are unchanged.
  • The ParseFresh improvement of 28–45% is much larger than the steady-state gain. The PR attributes it to smaller stack frames, but marks that as unverified.

Minor
The CHANGELOG entry is present. The Parser struct field alignment in parser.go now differs from before: currentPos and currentToken are separated from depth by a comment block. This is cosmetic only.
· branch perf/w3-02-currenttoken-pointer

@github-actions

github-actions Bot commented Nov 1, 2026 •

Copy link
Copy Markdown

Claude finished @ajitpratap0's task in 21s —— View job


Review of PR #922: currentToken as a pointer into p.tokens

Verdict: looks good to merge. I found no blocking issues. I read the core diff (parser.go, recovery.go, expressions_ops.go). I did not run the tests, so test and benchmark results come from the PR description and the CI perf comment.

Correctness

  • Every place that sets currentToken is covered: parseTokens, parseContextTokens, parseWithRecovery (including the empty-stream case), advance, seek, Reset, Release, NewParser and the pool New. Each one falls back to &p.eof when no real token applies. That keeps a zero-value &Parser{} usable and clears any stale pointer into a scrubbed or dropped tokenBuf.
  • Making the sentinel a per-parser field is the right choice over a package variable. A stray write can't race across goroutines or leak into other parsers.
  • Saving a pointer in savedTok := p.currentToken is equivalent to the old copy, because p.tokens is not written during a parse. The alter_table.go truncation reslices the same backing array, so its pointers stay valid.
  • The nextAdjacent rewrite compares End and Start values and gives the same result as before.

Non-blocking suggestions

  1. Nothing enforces the read-only rule. A write through p.currentToken or peekToken() would mutate the shared token stream, and possibly the caller's slice. The sentinel tests only guard p.eof. A cheap CI grep for currentToken\.\w+(\.\w+)*\s*=[^=] in non-test files would catch regressions. I found no such writes in non-test code.
  2. parseTokens and parseContextTokens alias their input. They take &tokens[0] from whatever slice they get, so a caller who mutates that slice mid-parse would change what the parser sees. A one-line doc comment saying so would help.
  3. peekToken() results alias p.tokens. A pointer held across Reset or a new parse would see the new contents. Current uses are short-lived, so this is fine. The doc comment already says the result is read-only.

Tests
Coverage is strong:

  • the cursor invariant across advance, negative seek and save/restore;
  • the sentinel staying zero over the full corpus on all three entry points;
  • re-pointing on Reset and Release, with mutation checks;
  • per-parser isolation and the zero-value parser;
  • a -race concurrent test.

The byte-identical differential run adds good confidence that behaviour is unchanged.

Performance

  • The CI shadow gate passed, and allocs/op and B/op are unchanged on every benchmark.
  • On the GOGC=off primary metric, ParseE2E and ParseRW improve by about 0.5–3%.
  • The default-GC secondary metric improves by 3–5%.
  • The ParseFresh gain of 28–45% is much larger than the steady-state gain. The PR's frame-size and stack-growth explanation is plausible but not profiled. A quick profile or a note in the CHANGELOG would be worth adding if you want to claim it.
  • The PR is upfront about missing the PLAN thresholds under GOGC=off.

Minor

  • The Parser struct field alignment changed because of the new comment block. It's cosmetic only.
  • The CHANGELOG entry is present.
    · branch perf/w3-02-currenttoken-pointer

@github-actions

github-actions Bot commented Nov 1, 2026 •

Copy link
Copy Markdown

Claude finished @ajitpratap0's task in 18s —— View job


Review of PR #922: currentToken as a pointer into p.tokens

Verdict: looks good to merge. I found no blocking issues. This was a read-only review of the diff. I did not run the tests. Test and benchmark results come from the PR description and the CI perf comment.

Correctness

  • Every place that sets currentToken falls back to &p.eof when no real token applies: parseTokens, parseContextTokens, parseWithRecovery (including the empty-stream case), advance, seek, Reset, Release, NewParser and the pool New. A zero-value &Parser{} stays usable. Nothing is left pointing at a scrubbed or dropped tokenBuf.
  • The sentinel is a per-parser field, not a package variable. A stray write can't race across goroutines or leak between parsers.
  • Saved cursors (savedTok := p.currentToken) now hold a pointer. That equals the old copy as long as p.tokens is not written during a parse.
  • nextAdjacent compares End and Start values and gives the same result as before.
  • A grep for writes through currentToken (currentToken\.[A-Za-z.]+\s*=[^=]) finds none in pkg/.

Non-blocking suggestions

  1. The read-only rule isn't enforced by the compiler. A write through currentToken or peekToken() would change the shared token stream, and possibly the caller's slice. The sentinel tests only guard p.eof. The grep above could be a cheap CI or lint check.
  2. parseTokens and parseContextTokens alias their input. They take &tokens[0] from whatever slice they receive. A one-line doc comment saying the slice must not be mutated during the parse would record that.
  3. peekToken() results alias p.tokens. A pointer held across Reset or a new parse would see the new contents. Current uses are short-lived, so this is fine. The doc comment already says the result is read-only.
  4. alter_table.go truncation: parseAlterMutation reslices p.tokens. The reslice shares the same backing array, so pointers stay valid, and seek re-points currentToken after the restore. I did not re-read that file in this pass.

Tests
Coverage is strong:

  • the cursor invariant across advance, negative seek and save/restore;
  • the sentinel staying zero over the full corpus on all three entry points;
  • re-pointing on Reset and Release, with the mutation checks the description reports;
  • per-parser sentinel isolation and the zero-value parser;
  • a -race concurrent test.

The byte-identical differential run in the description adds confidence that behaviour is unchanged.

Performance

  • The CI shadow gate passed. Suite geomean is about -7%. allocs/op and B/op are unchanged on every benchmark.
  • On the GOGC=off primary metric, ParseE2E and ParseRW move by about 0–4%, mostly within noise. The default-GC secondary metric improves by about 3–6%.
  • The ParseFresh gain of 34–48% is much larger than the steady-state gain. The stack-frame explanation in the description is plausible but unprofiled. Say so if you cite the number in the CHANGELOG.
  • The description is upfront about missing the PLAN thresholds under GOGC=off.

Minor

  • The Parser struct field alignment changed because of the new comment block. It's cosmetic only.
  • The CHANGELOG entry is present.
    · branch perf/w3-02-currenttoken-pointer

This branch was successfully deployed

1 active deployment
Preview — 8a154e92 Deployed Nov 1, 2026 by vercel[bot]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation parser

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant