Skip to content

perf(parser): size joins slice from a JOIN-count hint [W4-03] - #929

Merged
ajitpratap0 merged 7 commits into
mainfrom
perf/w4-03-joins-slice-hint
Nov 3, 2026
Merged

ajitpratap0 merged 7 commits into
mainfrom
perf/w4-03-joins-slice-hint

Conversation

@ajitpratap0

@ajitpratap0 ajitpratap0 commented Nov 2, 2026 •

Copy link
Copy Markdown
Owner

Summary

W4-03 (PLAN, C0 rank 4). parseJoinClauses used make([]ast.JoinClause, 0, 2). It now allocates with capacity 1 + (depth-0 JOIN tokens that follow), capped at 8, scanning at most 256 tokens and stopping at a depth-0 ;, UNION/EXCEPT/INTERSECT or the ) closing the enclosing level, via Parser.joinsCap(). The scan runs only when the first join of a FROM clause is appended, so join-free statements pay nothing. No API or AST change; only slice capacity differs.

Deviation from the PLAN text ("count in the preprocess loop"): the first version did count in preprocessTokensInto. The CI gate showed +1.2% to +1.7% instructions/op on every e2e case (including join-free ones) and +3% to +5% time on several join-free benchmarks, so the counter was moved to a lazy lookahead (commit b0eeb75). Review found that the first lazy version (256-token window only) scanned across statement and UNION boundaries, so scripts of short single-join statements allocated cap 8 (416 B/JoinClause) and B/op rose about +101% to +124% on such inputs (invisible to the gated suite, whose ParseRW medium statements are long). Fixed by making the scan depth-aware: it counts only depth-0 JOINs and stops at ;, UNION/EXCEPT/INTERSECT or the closing ), which also ignores JOINs inside nested subqueries.

Test Plan

  • task check, task test:cbinding, go test -count=1 ./pkg/compatibility/...: pass. staticcheck on ./pkg/sql/parser: clean.
  • Multi-statement trade-off (pre-tokenized parseTokens, B/op deterministic; base / old head / this head): 50 x single-join statements 93,957 / 210,344 / 69,929 B/op (-26% vs base); mixed 1-join/2-join script 99,963 / 221,448 / 87,948 (-12%); 20 x JOIN ... UNION ALL 39,254 / 78,755 / 29,646 (-24%). ParseE2E/join 2,138 and complex 5,405 B/op are unchanged from the previous head. New BenchmarkParseManyShortJoinStatements keeps this visible.
  • New pkg/sql/parser/join_hint_test.go: capacity and join content/order for 1,2,3,5,8,9,12 joins; window and cap behaviour in joinsCap; multi-statement script with a join-free statement in the middle; capacity assertions (cap 1 for each single-join statement in a script, for each UNION/EXCEPT/INTERSECT branch, for a subquery followed by outer joins, outer cap ignoring nested-subquery joins). Reverting joinsCap to the previous version fails 7 of these subtests.
  • Local task bench:ab (shared M4, indicative): ParseE2E/join B/op 2.305Ki to 1.836Ki (-20.34%, p=0.000), complex 6.398Ki to 5.023Ki (-21.49%), complex allocs 50 to 49; time unchanged within noise (geomean +0.59%, p=0.80).
  • CI perf gate (amd64, shadow; base afec16a, head 218d720, which includes the grow-by-hint change): PASS, A/A control 0 of 32 benchmarks moved more than 3%. ParseE2E/join B/op -20.34%, time -0.60% (p=0.028); complex B/op -21.49%, allocs 50 to 49, time -1.23%; ParseFresh/join B/op -19.67%; ParseRW/*/medium B/op -3.2% to -7.9% with fewer allocs on 6 of 7 dialects; Normalize/join -3.32% and TransformParse/join -4.11% time (both flagged faster); suite geomean -0.47% (p=0.029). allocs/op never rise and B/op never rises on any gated benchmark; instructions/op stays within +0.78% (worst e2e/join), under the 1.5% limit. Largest time movements are on join-free cases and sit above their A/A control (ParseE2E/aggregate +2.25%, select_where +1.09%, Normalize/simple_select +1.34%), all under the 3% threshold and with allocs/B/op and instructions/op unchanged. The later commits on this branch change only a test and a doc comment (plus a merge of main), not parser code.
  • Accepted trade-off: +1 alloc on two edge shapes the lookahead cannot see, a parenthesized join group followed by one outer join ((a JOIN b) JOIN c) and one join followed by a comma table (a JOIN b, c). Both are outside the gated suite.
  • The PLAN W4-03 gain (join B/op <= -18%, complex <= -10%, time neutral or better) is met. The C0 time expectation for join (-3% to -5%) is NOT met on ParseE2E/join. Reported, not chased.
  • Review fixes: TestJoinsCapSubqueryDepth now places the nested subquery after the first join (WHERE, and a derived table in a later join) and asserts exact outer caps 1 and 2; removing the depth-0 guard in joinsCap fails it (outer cap 3 and 4). parseJoinClauses now grows a full slice by the hint (slices.Grow) instead of only sizing an empty one. New TestJoinsCapParenGroupThenOuter and TestJoinsCommaAfterJoin. TestJoinsFullSliceRegrownFromHint pins the full-slice regrow: with the condition reverted to cap(joins) == 0 a parenthesized group followed by four outer joins ends at cap 8 (repeated doubling) and the test fails.
  • Known edge cost (not in the gated suite, testing.AllocsPerRun, pre-tokenized): SELECT * FROM (a JOIN b ON a.i=b.i) JOIN c ON c.i=a.i 13 to 14 allocs; the same with a further JOIN is unchanged at 18; SELECT * FROM a JOIN b ON a.i=b.i, c 10 to 11. The lookahead stops at the group's ) and cannot see a trailing comma table, so the cap-1 slice must grow once. Accepted; also noted in the CHANGELOG.
  • Oracle MINUS is tokenized as an identifier and is not a scan boundary; a MINUS chain can over-count later branches' JOINs, bounded by cap 8 and the 256-token window (capacity hint only). Noted in the joinsCap doc comment.

Breaking Changes

None.

🤖 Generated with Claude Code

https://claude.ai/code/session_01DnbRnTFErhoMJdAjVEeCg2

@vercel

vercel Bot commented Nov 2, 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 3, 2026 5:10am UTC

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

github-actions Bot commented Nov 2, 2026 •

Copy link
Copy Markdown

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


Review of PR #929: size joins slice from a JOIN-count hint

I read the diff and did not run the tests or benchmarks. The change is small and safe: the hint only affects slice capacity, so it can't change correctness.

What looks good

  • preprocessTokensInto now wraps preprocessTokensCount, so the existing callers are unchanged.
  • The count covers compound tokens such as INNER_JOIN, which expand to [INNER, JOIN]. It does this by scanning dst[before:].
  • The Oracle path in oracle_execute.go refreshes the hint, and Reset zeroes it.
  • joinsCap clamps to [1, 8] and falls back to 2 when there is no hint.
  • The test plan is thorough: the clamp table, hint equals the JOIN count, DeepEqual ASTs across hints from -1 to 50, and Reset behaviour.

Points to consider

  1. The hint is stream-wide, not per statement. The count covers the whole token stream, but parseJoinClauses allocates a slice for every SELECT, including subqueries and later statements in a script.
    • Example: a script with 8 statements that each have 1 JOIN gives a hint of 8, so each statement allocates cap 8 where it previously allocated cap 2.
    • That is a memory regression for batch or multi-statement input, and it is not covered by the single-statement ParseE2E benchmarks.
    • Cheap mitigations: use the hint only when the stream has a single statement, or cap lower (such as 4). A per-statement count would be more precise but costs more.
    • I'd at least add a benchmark or test with a many-statement script so the trade-off is visible.
  2. The hint can be stale. ParseFromModelTokens and parseTokens callers that skip preprocess and don't call Reset keep the previous parse's hint. This only affects capacity, so it is harmless, but the "0 = unknown" comment on joinHint is slightly overstated.
  3. Minor: counting cost. The extra loop over dst[before:] runs for every token. It is cheap, and you report time as neutral. Checking len(dst) == before+1 and testing the type once would avoid the inner loop in the common case, but this is optional.
  4. Comment formatting. The gofmt realignment of tokenBuf and joinHint is fine.

Verdict: Fine to merge once the perf gate verdict is in. I'd like a note, or a quick check, on the multi-statement over-allocation in point 1.

@github-actions

github-actions Bot commented Nov 2, 2026 •

Copy link
Copy Markdown

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

Base 01ee8242bccc, head 843dc98792a5. 10 interleaved rounds, benchtime 200ms, -cpu 1, pinned to CPU 3 (SMT siblings 2-3) with taskset.

  • 1 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 +0.59% <0.001 +0.12% +0.00% 5 → 5 ok
ParseE2E/select_where -0.60% 0.007 +0.28% +0.00% 18 → 18 ok
ParseE2E/join -0.36% 0.028 +0.13% -20.34% 17 → 17 ok
ParseE2E/aggregate +2.06% <0.001 -0.15% +0.00% 18 → 18 ok
ParseE2E/subquery +1.07% 0.002 +0.18% +0.00% 14 → 14 ok
ParseE2E/cte -0.12% 0.650 +0.14% +0.00% 21 → 21 ok
ParseE2E/window -0.74% 0.002 +0.20% +0.00% 11 → 11 ok
ParseE2E/complex -1.43% <0.001 +0.09% -21.49% 50 → 49 -allocs
ParseE2E/insert +0.74% <0.001 +0.08% +0.00% 17 → 17 ok
ParseE2E/update -0.24% 0.111 -0.03% +0.00% 13 → 13 ok
ParseE2E/delete +0.82% <0.001 +0.13% +0.00% 5 → 5 ok
ParseRW/postgresql/medium -1.96% <0.001 -0.23% -6.65% 646 → 644 -allocs
ParseRW/mysql/medium +0.58% 0.089 +0.01% -3.22% 365 → 365 ok
ParseRW/mariadb/medium -1.12% <0.001 -0.02% -6.80% 523 → 521 -allocs
ParseRW/sqlserver/medium -0.08% 0.143 -0.03% -7.56% 928 → 926 -allocs
ParseRW/oracle/medium -0.03% 0.739 -0.12% -7.91% 1093 → 1089 -allocs
ParseRW/sqlite/medium -0.09% 0.853 +0.05% -4.54% 346 → 345 -allocs
ParseRW/snowflake/medium -0.34% 0.009 -0.09% -6.93% 777 → 774 -allocs
ParseRW/clickhouse/medium -0.06% 0.393 +0.34% -6.23% 384 → 383 -allocs
ParseFresh/simple_select +0.05% 0.545 -0.08% +0.00% 6 → 6 ok
ParseFresh/join -1.13% <0.001 +0.14% -19.67% 18 → 18 ok
ParseFresh/complex -1.51% <0.001 -0.09% -21.23% 51 → 50 -allocs
PoolGetPut/serial -0.46% <0.001 +0.13% +0.00% 0 → 0 ok
Tokenize/scratch/join +0.00% 0.939 -0.15% +0.00% 0 → 0 ok
Tokenize/scratch/complex +0.04% 0.910 +0.10% +0.00% 0 → 0 ok
Tokenize/scratch/insert +0.08% 0.449 +0.00% +0.00% 0 → 0 ok
Normalize/simple_select +0.80% <0.001 -0.16% +0.00% 7 → 7 ok
Normalize/join -2.43% <0.001 -0.13% -17.49% 23 → 23 ok
Normalize/complex -1.34% <0.001 +0.20% -18.35% 69 → 68 -allocs
TransformParse/simple_select +0.55% <0.001 -0.04% +0.00% 5 → 5 ok
TransformParse/join -4.00% <0.001 -0.34% -20.34% 17 → 17 faster
TransformParse/complex -1.99% <0.001 +0.13% -21.49% 50 → 49 -allocs
LintRW/postgresql +0.35% 0.052 +0.08% -0.30% 36481 → 36479 -allocs
LintRW/mysql +0.76% 0.247 +0.33% -0.40% 44192 → 44188 -allocs
LintRW/mariadb +0.73% 0.009 +0.65% -0.50% 40735 → 40730 -allocs
LintRW/sqlserver +0.37% 0.143 +0.01% -0.40% 36897 → 36894 -allocs
LintRW/oracle +0.32% 0.075 +0.13% -0.34% 45293 → 45288 -allocs
LintRW/sqlite +0.37% 0.481 -0.27% -0.40% 40582 → 40580 -allocs
LintRW/snowflake -0.06% 1.000 -0.13% -0.38% 39843 → 39840 -allocs
LintRW/clickhouse +0.03% 1.000 +0.14% -0.29% 44998 → 44994 -allocs
LintRule/L007 +0.01% 0.912 -0.20% +0.00% 72565 → 72565 ok
LintRule/L010 +0.98% <0.001 +0.02% +0.00% 20758 → 20758 ok
ParseError/syntax_mid/parse -0.10% 0.790 -0.11% +0.00% 21 → 21 ok
ParseError/syntax_mid/validate -0.19% 0.256 -0.17% +0.00% 21 → 21 ok
ParseError/syntax_end/parse +0.00% 0.970 -0.24% +0.00% 25 → 25 ok
ParseError/syntax_end/validate +0.40% 0.034 +0.09% +0.00% 25 → 25 ok
ParseError/tokenize/parse -0.02% 0.595 -0.22% +0.00% 19 → 19 ok
ParseError/tokenize/validate +0.10% 0.733 +0.14% +0.00% 30 → 30 ok
ParseError/long_prefix/parse +0.36% 0.247 -0.07% +0.00% 229 → 229 ok
ParseError/long_prefix/validate +0.38% 0.052 -0.10% +0.00% 229 → 229 ok
geomean -0.16% 0.280 +0.02%

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

Instructions per operation (cmd/perfdriver)

case instructions/op status
e2e/aggregate +0.01% ok
e2e/complex +0.67% ok
e2e/cte -0.03% ok
e2e/delete +0.07% ok
e2e/insert +0.00% ok
e2e/join +0.86% ok
e2e/select_where +0.00% ok
e2e/simple_select -0.03% ok
e2e/subquery -0.06% ok
e2e/update +0.02% ok
e2e/window +0.08% ok
rw/postgresql/medium +0.05% ok
Secondary metric: default GC, GOMAXPROCS=1 (informational)
benchmark time p
ParseE2E/simple_select +0.58% 0.472
ParseE2E/select_where +0.01% 0.970
ParseE2E/join -2.90% <0.001
ParseE2E/aggregate -0.05% 0.880
ParseE2E/subquery +0.28% 0.427
ParseE2E/cte -0.12% 0.519
ParseE2E/window +0.05% 0.791
ParseE2E/complex -3.33% <0.001
ParseE2E/insert -0.38% 0.173
ParseE2E/update +0.48% 0.225
ParseE2E/delete +0.31% 0.734
ParseRW/postgresql/medium -1.61% <0.001
ParseRW/mysql/medium +0.02% 0.853
ParseRW/mariadb/medium -1.18% <0.001
ParseRW/sqlserver/medium -0.82% 0.004
ParseRW/oracle/medium -0.68% 0.015
ParseRW/sqlite/medium -1.88% <0.001
ParseRW/snowflake/medium -1.22% <0.001
ParseRW/clickhouse/medium -1.20% <0.001
ParseFresh/simple_select -0.07% 0.623
ParseFresh/join -0.74% <0.001
ParseFresh/complex -1.59% <0.001
PoolGetPut/serial -0.49% <0.001
Tokenize/scratch/join +0.26% 0.149
Tokenize/scratch/complex -0.17% 0.150
Tokenize/scratch/insert +0.02% 0.545
Normalize/simple_select +0.74% 0.002
Normalize/join -0.98% 0.009
Normalize/complex -1.67% 0.003
TransformParse/simple_select -0.03% 0.879
TransformParse/join -2.03% <0.001
TransformParse/complex -3.82% <0.001
LintRW/postgresql +0.22% 0.190
LintRW/mysql +0.62% 0.063
LintRW/mariadb +0.22% 0.190
LintRW/sqlserver +0.29% 0.089
LintRW/oracle +0.47% 0.015
LintRW/sqlite -0.10% 0.481
LintRW/snowflake +0.40% 0.218
LintRW/clickhouse +0.38% 0.015
LintRule/L007 -0.48% 0.529
LintRule/L010 +0.68% 0.315
ParseError/syntax_mid/parse -0.14% 0.705
ParseError/syntax_mid/validate +0.32% 0.622
ParseError/syntax_end/parse +0.09% 0.734
ParseError/syntax_end/validate +0.32% 0.151
ParseError/tokenize/parse -0.49% 0.151
ParseError/tokenize/validate -3.95% 0.307
ParseError/long_prefix/parse +0.18% 0.579
ParseError/long_prefix/validate +0.33% 0.579
geomean -0.50%
Environment
date:        2026-11-03T05:12:07Z
base:        01ee8242bccc69969735750789ef93fc195478be
head:        working tree of 843dc98792a558c86ac4287b86d89b5884ba1a6d
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.69, 0.52, 0.19
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 8573C
Thread(s) per core:                      2
Core(s) per socket:                      2
Socket(s):                               1
CPU(s) scaling MHz:                      135%
Hypervisor vendor:                       Microsoft
L1d cache:                               96 KiB (2 instances)
L1i cache:                               64 KiB (2 instances)
L2 cache:                                4 MiB (2 instances)
L3 cache:                                260 MiB (1 instance)
benchstat (primary metric)
goos: linux
goarch: amd64
pkg: github.com/ajitpratap0/GoSQLX/pkg/fingerprint
cpu: INTEL(R) XEON(R) PLATINUM 8573C
                        │    base     │                head                │
                        │   sec/op    │   sec/op     vs base               │
Normalize/simple_select   1.873µ ± 0%   1.888µ ± 0%  +0.80% (p=0.000 n=10)
Normalize/join            6.358µ ± 0%   6.204µ ± 0%  -2.43% (p=0.000 n=10)
Normalize/complex         16.10µ ± 0%   15.88µ ± 0%  -1.34% (p=0.000 n=10)
geomean                   5.765µ        5.708µ       -1.00%

                        │     base     │                  head                  │
                        │     B/op     │     B/op      vs base                  │
Normalize/simple_select     896.0 ± 0%     896.0 ± 0%        ~ (p=1.000 n=10) ¹
Normalize/join            2.680Ki ± 0%   2.211Ki ± 0%  -17.49% (p=0.000 n=10)
Normalize/complex         7.492Ki ± 0%   6.117Ki ± 0%  -18.35% (p=0.000 n=10)
geomean                   2.600Ki        2.279Ki       -12.34%
¹ all samples are equal

                        │    base    │                head                 │
                        │ allocs/op  │ allocs/op   vs base                 │
Normalize/simple_select   7.000 ± 0%   7.000 ± 0%       ~ (p=1.000 n=10) ¹
Normalize/join            23.00 ± 0%   23.00 ± 0%       ~ (p=1.000 n=10) ¹
Normalize/complex         69.00 ± 0%   68.00 ± 0%  -1.45% (p=0.000 n=10)
geomean                   22.31        22.20       -0.49%
¹ all samples are equal

pkg: github.com/ajitpratap0/GoSQLX/pkg/gosqlx
                                │     base     │                head                 │
                                │    sec/op    │    sec/op     vs base               │
ParseE2E/simple_select             1.263µ ± 0%    1.270µ ± 0%  +0.59% (p=0.000 n=10)
ParseE2E/select_where              2.825µ ± 0%    2.808µ ± 0%  -0.60% (p=0.005 n=10)
ParseE2E/join                      3.975µ ± 1%    3.960µ ± 0%       ~ (p=0.025 n=10)
ParseE2E/aggregate                 2.990µ ± 0%    3.052µ ± 2%  +2.06% (p=0.000 n=10)
ParseE2E/subquery                  2.528µ ± 0%    2.555µ ± 0%  +1.07% (p=0.001 n=10)
ParseE2E/cte                       3.609µ ± 1%    3.605µ ± 2%       ~ (p=0.643 n=10)
ParseE2E/window                    2.283µ ± 1%    2.266µ ± 0%  -0.74% (p=0.001 n=10)
ParseE2E/complex                   10.41µ ± 0%    10.26µ ± 1%  -1.43% (p=0.000 n=10)
ParseE2E/insert                    1.891µ ± 0%    1.905µ ± 0%  +0.74% (p=0.000 n=10)
ParseE2E/update                    1.676µ ± 0%    1.672µ ± 1%       ~ (p=0.108 n=10)
ParseE2E/delete                    998.3n ± 0%   1006.5n ± 1%  +0.82% (p=0.000 n=10)
ParseRW/postgresql/medium          130.3µ ± 0%    127.8µ ± 0%  -1.96% (p=0.000 n=10)
ParseRW/mysql/medium               69.09µ ± 1%    69.49µ ± 1%       ~ (p=0.089 n=10)
ParseRW/mariadb/medium            100.46µ ± 1%    99.33µ ± 1%  -1.12% (p=0.000 n=10)
ParseRW/sqlserver/medium           232.4µ ± 0%    232.2µ ± 0%       ~ (p=0.143 n=10)
ParseRW/oracle/medium              288.0µ ± 0%    287.9µ ± 0%       ~ (p=0.739 n=10)
ParseRW/sqlite/medium              61.96µ ± 0%    61.91µ ± 0%       ~ (p=0.853 n=10)
ParseRW/snowflake/medium           148.5µ ± 1%    148.0µ ± 0%  -0.34% (p=0.009 n=10)
ParseRW/clickhouse/medium          67.01µ ± 1%    66.97µ ± 0%       ~ (p=0.393 n=10)
ParseFresh/simple_select           6.098µ ± 0%    6.101µ ± 1%       ~ (p=0.541 n=10)
ParseFresh/join                    9.459µ ± 0%    9.351µ ± 0%  -1.13% (p=0.000 n=10)
ParseFresh/complex                 16.11µ ± 0%    15.87µ ± 0%  -1.51% (p=0.000 n=10)
PoolGetPut/serial                  94.15n ± 0%    93.72n ± 0%  -0.46% (p=0.000 n=10)
ParseError/syntax_mid/parse        3.045µ ± 0%    3.042µ ± 0%       ~ (p=0.781 n=10)
ParseError/syntax_mid/validate     2.630µ ± 0%    2.625µ ± 1%       ~ (p=0.254 n=10)
ParseError/syntax_end/parse        3.079µ ± 0%    3.079µ ± 0%       ~ (p=0.955 n=10)
ParseError/syntax_end/validate     2.845µ ± 1%    2.857µ ± 1%       ~ (p=0.030 n=10)
ParseError/tokenize/parse          2.077µ ± 1%    2.077µ ± 1%       ~ (p=0.590 n=10)
ParseError/tokenize/validate       3.631µ ± 1%    3.635µ ± 1%       ~ (p=0.726 n=10)
ParseError/long_prefix/parse       56.68µ ± 1%    56.88µ ± 1%       ~ (p=0.247 n=10)
ParseError/long_prefix/validate    55.81µ ± 1%    56.03µ ± 0%       ~ (p=0.052 n=10)
geomean                            8.749µ         8.739µ       -0.11%

                                │     base     │                head                 │
                                │     B/s      │     B/s       vs base               │
ParseE2E/simple_select            19.64Mi ± 0%   19.52Mi ± 0%  -0.61% (p=0.000 n=10)
ParseE2E/select_where             34.78Mi ± 0%   34.98Mi ± 0%  +0.59% (p=0.005 n=10)
ParseE2E/join                     30.23Mi ± 1%   30.35Mi ± 0%       ~ (p=0.025 n=10)
ParseE2E/aggregate                26.79Mi ± 0%   26.25Mi ± 2%  -1.99% (p=0.000 n=10)
ParseE2E/subquery                 30.94Mi ± 0%   30.61Mi ± 0%  -1.06% (p=0.001 n=10)
ParseE2E/cte                      32.77Mi ± 1%   32.81Mi ± 2%       ~ (p=0.643 n=10)
ParseE2E/window                   39.27Mi ± 1%   39.57Mi ± 0%  +0.75% (p=0.001 n=10)
ParseE2E/complex                  34.62Mi ± 0%   35.12Mi ± 1%  +1.45% (p=0.000 n=10)
ParseE2E/insert                   38.34Mi ± 0%   38.05Mi ± 0%  -0.76% (p=0.000 n=10)
ParseE2E/update                   31.30Mi ± 0%   31.37Mi ± 1%       ~ (p=0.128 n=10)
ParseE2E/delete                   49.68Mi ± 0%   49.27Mi ± 1%  -0.83% (p=0.000 n=10)
ParseRW/postgresql/medium         27.53Mi ± 0%   28.08Mi ± 0%  +1.99% (p=0.000 n=10)
ParseRW/mysql/medium              39.17Mi ± 1%   38.95Mi ± 1%       ~ (p=0.079 n=10)
ParseRW/mariadb/medium            35.30Mi ± 1%   35.70Mi ± 1%  +1.13% (p=0.000 n=10)
ParseRW/sqlserver/medium          28.62Mi ± 0%   28.65Mi ± 0%       ~ (p=0.189 n=10)
ParseRW/oracle/medium             29.11Mi ± 0%   29.12Mi ± 0%       ~ (p=0.725 n=10)
ParseRW/sqlite/medium             33.34Mi ± 0%   33.37Mi ± 0%       ~ (p=0.839 n=10)
ParseRW/snowflake/medium          34.24Mi ± 1%   34.35Mi ± 0%  +0.33% (p=0.006 n=10)
ParseRW/clickhouse/medium         33.63Mi ± 1%   33.65Mi ± 0%       ~ (p=0.342 n=10)
ParseError/syntax_mid/parse       15.03Mi ± 0%   15.04Mi ± 0%       ~ (p=0.693 n=10)
ParseError/syntax_mid/validate    17.40Mi ± 0%   17.44Mi ± 1%       ~ (p=0.284 n=10)
ParseError/syntax_end/parse       12.08Mi ± 0%   12.08Mi ± 0%       ~ (p=0.987 n=10)
ParseError/syntax_end/validate    13.07Mi ± 1%   13.02Mi ± 1%       ~ (p=0.030 n=10)
ParseError/tokenize/parse         21.12Mi ± 1%   21.13Mi ± 1%       ~ (p=0.590 n=10)
ParseError/tokenize/validate      12.08Mi ± 1%   12.07Mi ± 1%       ~ (p=0.780 n=10)
ParseError/long_prefix/parse      40.75Mi ± 1%   40.61Mi ± 1%       ~ (p=0.240 n=10)
ParseError/long_prefix/validate   41.38Mi ± 1%   41.23Mi ± 0%       ~ (p=0.045 n=10)
geomean                           27.81Mi        27.81Mi       +0.02%

                                │      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%     1.836Ki ± 0%  -20.34% (p=0.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.398Ki ± 0%     5.023Ki ± 0%  -21.49% (p=0.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.51Ki ± 0%     65.82Ki ± 0%   -6.65% (p=0.000 n=10)
ParseRW/mysql/medium              43.72Ki ± 0%     42.32Ki ± 0%   -3.22% (p=0.000 n=10)
ParseRW/mariadb/medium            64.83Ki ± 0%     60.43Ki ± 0%   -6.80% (p=0.000 n=10)
ParseRW/sqlserver/medium          90.48Ki ± 0%     83.64Ki ± 0%   -7.56% (p=0.000 n=10)
ParseRW/oracle/medium             113.8Ki ± 0%     104.8Ki ± 0%   -7.91% (p=0.000 n=10)
ParseRW/sqlite/medium             39.89Ki ± 0%     38.08Ki ± 0%   -4.54% (p=0.000 n=10)
ParseRW/snowflake/medium          83.38Ki ± 0%     77.60Ki ± 0%   -6.93% (p=0.000 n=10)
ParseRW/clickhouse/medium         37.13Ki ± 0%     34.82Ki ± 0%   -6.23% (p=0.000 n=10)
ParseFresh/simple_select            896.0 ± 0%       896.0 ± 0%        ~ (p=1.000 n=10) ¹
ParseFresh/join                   2.383Ki ± 0%     1.914Ki ± 0%  -19.67% (p=0.000 n=10)
ParseFresh/complex                6.477Ki ± 0%     5.102Ki ± 0%  -21.23% (p=0.000 n=10)
PoolGetPut/serial                   0.000 ± 0%       0.000 ± 0%        ~ (p=1.000 n=10) ¹
ParseError/syntax_mid/parse       1.477Ki ± 0%     1.477Ki ± 0%        ~ (p=1.000 n=10) ¹
ParseError/syntax_mid/validate    1.477Ki ± 0%     1.477Ki ± 0%        ~ (p=1.000 n=10) ¹
ParseError/syntax_end/parse       2.227Ki ± 0%     2.227Ki ± 0%        ~ (p=1.000 n=10) ¹
ParseError/syntax_end/validate    2.227Ki ± 0%     2.227Ki ± 0%        ~ (p=1.000 n=10) ¹
ParseError/tokenize/parse         1.570Ki ± 0%     1.570Ki ± 0%        ~ (p=1.000 n=10) ¹
ParseError/tokenize/validate      3.789Ki ± 0%     3.789Ki ± 0%        ~ (p=1.000 n=10) ¹
ParseError/long_prefix/parse      23.52Ki ± 0%     23.52Ki ± 0%        ~ (p=0.635 n=10)
ParseError/long_prefix/validate   23.48Ki ± 0%     23.48Ki ± 0%        ~ (p=1.000 n=10) ¹
geomean                                        ²                  -4.55%                ²
¹ 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%      49.00 ± 0%  -2.00% (p=0.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%      644.0 ± 0%  -0.31% (p=0.000 n=10)
ParseRW/mysql/medium               365.0 ± 0%      365.0 ± 0%       ~ (p=1.000 n=10) ¹
ParseRW/mariadb/medium             523.0 ± 0%      521.0 ± 0%  -0.38% (p=0.000 n=10)
ParseRW/sqlserver/medium           928.0 ± 0%      926.0 ± 0%  -0.22% (p=0.000 n=10)
ParseRW/oracle/medium             1.093k ± 0%     1.089k ± 0%  -0.37% (p=0.000 n=10)
ParseRW/sqlite/medium              346.0 ± 0%      345.0 ± 0%  -0.29% (p=0.000 n=10)
ParseRW/snowflake/medium           777.0 ± 0%      774.0 ± 0%  -0.39% (p=0.000 n=10)
ParseRW/clickhouse/medium          384.0 ± 0%      383.0 ± 0%  -0.26% (p=0.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%      50.00 ± 0%  -1.96% (p=0.000 n=10)
PoolGetPut/serial                  0.000 ± 0%      0.000 ± 0%       ~ (p=1.000 n=10) ¹
ParseError/syntax_mid/parse        21.00 ± 0%      21.00 ± 0%       ~ (p=1.000 n=10) ¹
ParseError/syntax_mid/validate     21.00 ± 0%      21.00 ± 0%       ~ (p=1.000 n=10) ¹
ParseError/syntax_end/parse        25.00 ± 0%      25.00 ± 0%       ~ (p=1.000 n=10) ¹
ParseError/syntax_end/validate     25.00 ± 0%      25.00 ± 0%       ~ (p=1.000 n=10) ¹
ParseError/tokenize/parse          19.00 ± 0%      19.00 ± 0%       ~ (p=1.000 n=10) ¹
ParseError/tokenize/validate       30.00 ± 0%      30.00 ± 0%       ~ (p=1.000 n=10) ¹
ParseError/long_prefix/parse       229.0 ± 0%      229.0 ± 0%       ~ (p=1.000 n=10) ¹
ParseError/long_prefix/validate    229.0 ± 0%      229.0 ± 0%       ~ (p=1.000 n=10) ¹
geomean                                       ²                -0.20%                ²
¹ all samples are equal
² summaries must be >0 to compute geomean

                          │    base     │                head                │
                          │  sec/stmt   │  sec/stmt    vs base               │
ParseRW/postgresql/medium   10.86µ ± 0%   10.65µ ± 0%  -1.96% (p=0.000 n=10)
ParseRW/mysql/medium        8.637µ ± 1%   8.687µ ± 1%       ~ (p=0.089 n=10)
ParseRW/mariadb/medium      11.16µ ± 1%   11.04µ ± 1%  -1.12% (p=0.000 n=10)
ParseRW/sqlserver/medium    11.07µ ± 0%   11.06µ ± 0%       ~ (p=0.149 n=10)
ParseRW/oracle/medium       12.00µ ± 0%   11.99µ ± 0%       ~ (p=0.724 n=10)
ParseRW/sqlite/medium       10.33µ ± 0%   10.32µ ± 0%       ~ (p=0.810 n=10)
ParseRW/snowflake/medium    11.42µ ± 1%   11.39µ ± 0%  -0.34% (p=0.008 n=10)
ParseRW/clickhouse/medium   9.573µ ± 1%   9.567µ ± 0%       ~ (p=0.343 n=10)
geomean                     10.58µ        10.54µ       -0.39%

pkg: github.com/ajitpratap0/GoSQLX/pkg/linter
                  │    base     │                head                │
                  │   sec/op    │   sec/op     vs base               │
LintRW/postgresql   6.574m ± 0%   6.597m ± 0%       ~ (p=0.052 n=10)
LintRW/mysql        6.928m ± 1%   6.981m ± 1%       ~ (p=0.247 n=10)
LintRW/mariadb      6.153m ± 1%   6.198m ± 0%  +0.73% (p=0.009 n=10)
LintRW/sqlserver    6.238m ± 1%   6.261m ± 1%       ~ (p=0.143 n=10)
LintRW/oracle       6.970m ± 1%   6.992m ± 1%       ~ (p=0.075 n=10)
LintRW/sqlite       5.828m ± 1%   5.849m ± 0%       ~ (p=0.481 n=10)
LintRW/snowflake    6.449m ± 1%   6.445m ± 1%       ~ (p=1.000 n=10)
LintRW/clickhouse   6.586m ± 1%   6.588m ± 1%       ~ (p=1.000 n=10)
LintRule/L007       7.470m ± 1%   7.471m ± 1%       ~ (p=0.912 n=10)
LintRule/L010       2.429m ± 0%   2.453m ± 0%  +0.98% (p=0.000 n=10)
geomean             5.941m        5.963m       +0.38%

                  │     base     │                head                 │
                  │     B/s      │     B/s       vs base               │
LintRW/postgresql   4.311Mi ± 0%   4.292Mi ± 0%       ~ (p=0.031 n=10)
LintRW/mysql        4.725Mi ± 1%   4.687Mi ± 1%       ~ (p=0.159 n=10)
LintRW/mariadb      4.959Mi ± 1%   4.921Mi ± 0%       ~ (p=0.011 n=10)
LintRW/sqlserver    4.969Mi ± 1%   4.945Mi ± 1%       ~ (p=0.091 n=10)
LintRW/oracle       5.703Mi ± 1%   5.684Mi ± 1%       ~ (p=0.057 n=10)
LintRW/sqlite       4.926Mi ± 1%   4.907Mi ± 0%       ~ (p=0.466 n=10)
LintRW/snowflake    4.835Mi ± 1%   4.840Mi ± 1%       ~ (p=1.000 n=10)
LintRW/clickhouse   4.930Mi ± 1%   4.926Mi ± 1%       ~ (p=0.926 n=10)
geomean             4.907Mi        4.887Mi       -0.40%

                  │     base     │                 head                  │
                  │     B/op     │     B/op      vs base                 │
LintRW/postgresql   3.508Mi ± 0%   3.497Mi ± 0%  -0.30% (p=0.000 n=10)
LintRW/mysql        4.369Mi ± 0%   4.352Mi ± 0%  -0.40% (p=0.000 n=10)
LintRW/mariadb      4.046Mi ± 0%   4.026Mi ± 0%  -0.50% (p=0.000 n=10)
LintRW/sqlserver    3.608Mi ± 0%   3.594Mi ± 0%  -0.40% (p=0.000 n=10)
LintRW/oracle       4.402Mi ± 0%   4.386Mi ± 0%  -0.34% (p=0.000 n=10)
LintRW/sqlite       3.831Mi ± 0%   3.816Mi ± 0%  -0.40% (p=0.000 n=10)
LintRW/snowflake    3.862Mi ± 0%   3.847Mi ± 0%  -0.38% (p=0.000 n=10)
LintRW/clickhouse   4.308Mi ± 0%   4.295Mi ± 0%  -0.29% (p=0.000 n=10)
LintRule/L007       2.942Mi ± 0%   2.942Mi ± 0%       ~ (p=1.000 n=10) ¹
LintRule/L010       859.6Ki ± 0%   859.6Ki ± 0%       ~ (p=1.000 n=10) ¹
geomean             3.304Mi        3.294Mi       -0.30%
¹ all samples are equal

                  │    base     │                 head                 │
                  │  allocs/op  │  allocs/op   vs base                 │
LintRW/postgresql   36.48k ± 0%   36.48k ± 0%  -0.01% (p=0.000 n=10)
LintRW/mysql        44.19k ± 0%   44.19k ± 0%  -0.01% (p=0.000 n=10)
LintRW/mariadb      40.73k ± 0%   40.73k ± 0%  -0.01% (p=0.000 n=10)
LintRW/sqlserver    36.90k ± 0%   36.89k ± 0%  -0.01% (p=0.000 n=10)
LintRW/oracle       45.29k ± 0%   45.29k ± 0%  -0.01% (p=0.000 n=10)
LintRW/sqlite       40.58k ± 0%   40.58k ± 0%  -0.00% (p=0.000 n=10)
LintRW/snowflake    39.84k ± 0%   39.84k ± 0%  -0.01% (p=0.000 n=10)
LintRW/clickhouse   45.00k ± 0%   44.99k ± 0%  -0.01% (p=0.000 n=10)
LintRule/L007       72.56k ± 0%   72.56k ± 0%       ~ (p=1.000 n=10) ¹
LintRule/L010       20.76k ± 0%   20.76k ± 0%       ~ (p=1.000 n=10) ¹
geomean             40.55k        40.55k       -0.01%
¹ all samples are equal

                  │    base     │                head                │
                  │  sec/stmt   │  sec/stmt    vs base               │
LintRW/postgresql   21.56µ ± 0%   21.63µ ± 0%       ~ (p=0.052 n=10)
LintRW/mysql        16.73µ ± 1%   16.86µ ± 1%       ~ (p=0.239 n=10)
LintRW/mariadb      16.19µ ± 1%   16.31µ ± 0%       ~ (p=0.011 n=10)
LintRW/sqlserver    22.52µ ± 1%   22.60µ ± 1%       ~ (p=0.138 n=10)
LintRW/oracle       18.94µ ± 1%   19.00µ ± 1%       ~ (p=0.075 n=10)
LintRW/sqlite       16.75µ ± 1%   16.81µ ± 0%       ~ (p=0.469 n=10)
LintRW/snowflake    19.72µ ± 1%   19.71µ ± 1%       ~ (p=1.000 n=10)
LintRW/clickhouse   16.72µ ± 1%   16.72µ ± 1%       ~ (p=1.000 n=10)
LintRule/L007       2.656µ ± 1%   2.656µ ± 1%       ~ (p=0.853 n=10)
LintRule/L010       863.6n ± 0%   872.1n ± 0%  +0.98% (p=0.000 n=10)
geomean             11.22µ        11.26µ       +0.39%

pkg: github.com/ajitpratap0/GoSQLX/pkg/sql/tokenizer
                         │    base     │                head                │
                         │   sec/op    │   sec/op     vs base               │
Tokenize/scratch/join      1.355µ ± 0%   1.355µ ± 0%       ~ (p=0.924 n=10)
Tokenize/scratch/complex   3.530µ ± 0%   3.531µ ± 0%       ~ (p=0.897 n=10)
Tokenize/scratch/insert    713.4n ± 0%   713.9n ± 0%       ~ (p=0.446 n=10)
geomean                    1.505µ        1.506µ       +0.04%

                         │     base     │                head                 │
                         │     B/s      │     B/s       vs base               │
Tokenize/scratch/join      88.67Mi ± 0%   88.69Mi ± 0%       ~ (p=0.781 n=10)
Tokenize/scratch/complex   102.1Mi ± 0%   102.1Mi ± 0%       ~ (p=0.971 n=10)
Tokenize/scratch/insert    101.6Mi ± 0%   101.5Mi ± 0%       ~ (p=0.446 n=10)
geomean                    97.26Mi        97.23Mi       -0.03%

                         │     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.270µ ± 0%   1.277µ ± 0%  +0.55% (p=0.000 n=10)
TransformParse/join            4.386µ ± 0%   4.210µ ± 0%  -4.00% (p=0.000 n=10)
TransformParse/complex         11.58µ ± 0%   11.35µ ± 1%  -1.99% (p=0.000 n=10)
geomean                        4.010µ        3.937µ       -1.83%

                             │     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%   1.836Ki ± 0%  -20.34% (p=0.000 n=10)
TransformParse/complex         6.398Ki ± 0%   5.023Ki ± 0%  -21.49% (p=0.000 n=10)
geomean                        2.273Ki        1.944Ki       -14.48%
¹ 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%   49.00 ± 0%  -2.00% (p=0.000 n=10)
geomean                        16.20        16.09       -0.67%
¹ all samples are equal

Run, raw data and benchstat

…ss [W4-03]

The preprocess-loop counter cost about 1.3% instructions on join-free
queries (CI perf gate). Scan a 256-token lookahead only when the first
join is appended, so join-free statements pay nothing.

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 2, 2026 •

Copy link
Copy Markdown

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


Review of PR #929: size joins slice from a JOIN-count hint

I read the current head (b0eeb75) and did not run tests or benchmarks. The earlier review comment and the PR description are out of date. The second commit removed the joinHint field and the preprocessTokensCount change. joinsCap() now scans lazily, so most of the previous review's points no longer apply.

What the code does now

  • parseJoinClauses calls p.joinsCap() only when the first join is appended to an empty slice (select_clauses.go:412).
  • joinsCap counts TokenTypeJoin in the next 256 tokens and starts at 1 for the join being appended. It stops at 8.
  • The change touches no parser state, Reset or preprocess, so the stale-hint issue is gone. Join-free statements pay nothing. That should clear the +1.2–1.7% instructions/op rise the perf gate flagged on the first commit.

Good

  • The change is simple and safe. The count only affects capacity, and append still grows past the cap.
  • The tests cover capacity and content for 1–12 joins, the window/cap and end-of-stream cases, and a multi-statement script.

Points to consider

  1. Over-allocation across statements. The 256-token window can run into later statements and subqueries. In a script of many short SELECT … JOIN … statements, each one can see up to 7 extra JOINs from its neighbours. Each then allocates cap up to 8 instead of 1–2. That is about 8 × sizeof(JoinClause) per statement, a bounded memory regression on batch input. TestJoinsCapMultiStatement checks only join counts, not capacity.
    • A cheap fix is to stop the scan at the first top-level ; (TokenTypeSemicolon). Subquery over-counting would remain, which is acceptable.
    • Alternatively, add a many-statement benchmark so the trade-off is visible.
  2. Perf gate. The gate report was run on the earlier head (0f39989) and says REGRESSION. The slower aggregate, subquery, cte, window and insert results are on queries with no joins. Those have an A/A control of +2–2.5% on some rows, so they look like noise. The +1.2–1.7% instructions/op rise came from the preprocess counter, which this head removed. Please re-run the gate on b0eeb75 and fill in the "CI perf-gate verdict: pending" line.
  3. PR description is stale. It still describes Parser.joinHint, the Reset refresh, the Oracle path, and tests for "hints -1..50" and "Reset/refresh behaviour". None of these exist now. Update the summary and test plan. Check that the CHANGELOG entry matches too.
  4. Nit. The comment on joinsCap reads "the 1 plus the number…". Reword it to "1 plus the number…". A comment noting that p.currentPos is just past the consumed JOIN token would help readers.

Verdict: The code is fine. Before merging, I'd refresh the PR description and perf-gate result and decide on point 1. A semicolon stop is a one-line change.

ajitpratap0 and others added 2 commits November 2, 2026 20:22
…daries [W4-03]

The 256-token lookahead counted JOINs of neighbouring statements, UNION
branches and nested subqueries, so short single-join statements in scripts
allocated cap 8 (416 B per JoinClause), doubling B/op. Count only depth-0
JOINs and stop at ';', UNION/EXCEPT/INTERSECT or the closing ')'.

Adds capacity assertions for scripts, set-op branches and subquery depth,
and a many-short-join-statements benchmark.

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 2, 2026 •

Copy link
Copy Markdown

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


Review of PR #929: size joins slice from a JOIN lookahead

I read the diff against origin/main at the current head. I did not run tests or benchmarks.

Summary: The code is fine to merge. The earlier review points are addressed in this head.

What changed

  • joinsCap() (pkg/sql/parser/select_clauses.go) runs only when the first JOIN is appended to an empty slice (cap(joins) == 0). Join-free statements pay nothing.
  • The scan counts depth-0 TokenTypeJoin tokens, starting from 1 for the join being appended. It stops at 8.
  • The scan returns early at a depth-0 ;, UNION, EXCEPT or INTERSECT, and at the ) that closes the enclosing level. It looks at most 256 tokens ahead.

Earlier concerns

  • Over-allocation across statements: now handled. The scan stops at ;, and it ignores nested subquery JOINs through depth tracking. The set-operation stop also keeps UNION branches from inflating each other.
  • Stale hint state: gone. The change touches no parser state, Reset or preprocess.
  • Preprocess counting cost: gone, since the counting moved to the lazy lookahead.

Remaining points (minor)

  1. PR description wording: the summary matches the code now. The Test Plan lists the CI perf-gate numbers for head 80a11f917a23. Make sure those still apply to the final head, because the head changed after that run.
  2. Perf gate: the "slower" flags (select_where, aggregate, cte) are on join-free queries whose instructions/op are unchanged. That points to layout noise, as you say. A re-run on the final head would confirm it, but since the gate is shadow-mode, I don't see it as blocking.
  3. Test coverage: if TestJoinsCapMultiStatement only checks join content and not capacity, consider asserting the capacity of the statements around the join-free one. That would lock in the ; stop.
  4. Possible edge case: this assumes compound join tokens (for example INNER JOIN, LEFT OUTER JOIN) reach the parser as a token stream that contains TokenTypeJoin. If some dialect token variant, such as NATURAL_JOIN or a ClickHouse ARRAY JOIN, skips that type, the count is simply low and append still grows the slice. This is harmless.

Verdict: The change is simple and safe, since it only affects capacity. The memory gain is real (about -20% B/op on join queries), and time is neutral within noise. Approve-worthy from my side.

@github-actions

github-actions Bot commented Nov 2, 2026 •

Copy link
Copy Markdown

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


Review of PR #929: size joins slice from a JOIN lookahead

I read the diff against origin/main. I did not run tests or benchmarks.

Verdict: The change is safe to merge. It only affects slice capacity, so it can't change the AST.

What it does

  • parseJoinClauses now calls slices.Grow(joins, p.joinsCap()) whenever len == cap. joinsCap() returns 1 plus the depth-0 TokenTypeJoin tokens ahead, capped at 8.
  • The scan stops at a depth-0 ;, UNION, EXCEPT or INTERSECT, or at the ) that closes the enclosing level. It looks at most 256 tokens ahead.
  • Join-free statements never reach the scan.

Good

  • The depth-aware scan fixes the over-allocation on multi-statement scripts that the earlier reviews raised. Nested-subquery JOINs and neighbouring statements no longer inflate the hint.
  • The comment documents the Oracle MINUS gap.
  • The tests assert capacity, not only content. BenchmarkParseManyShortJoinStatements keeps the trade-off visible.
  • The CHANGELOG entry matches the code and states the known edge cost.

Minor points (none blocking)

  1. Rescans past 8 joins. After the slice reaches cap 8, every later join (9th, 10th and so on) finds len == cap and calls joinsCap() again. Each call scans up to 256 tokens, and slices.Grow then grows the slice. The cost is bounded and only affects unusual 9+ join queries. A cheap guard would be to call joinsCap() only when cap(joins) == 0 or the slice was handed in. Otherwise use plain append growth.
  2. Grow size is small once the slice is full. When a full slice is handed in by a comma join or a parenthesized group, joinsCap() returns at least 1. slices.Grow then allocates roughly len + hint, so growth is only as good as the hint. The CHANGELOG already notes the (a JOIN b) JOIN c shapes the lookahead can't see.
  3. Stale numbers in the PR description. The Test Plan still contains the superseded numbers for head 80a11f917a23 and a stray "reports REGRESSION" fragment. The latest gate run on head 043ce317bb8a is PASS: ParseE2E/join B/op -20.34%, complex -21.49%, and the geomean -0.70%. I'd trim the description to the final-head numbers so a reader doesn't take the old regression flags as current.
  4. Compound join tokens. If a dialect emits join variants without TokenTypeJoin, such as ARRAY JOIN, the count comes out low. append still grows, so this is harmless.

…[W4-03]

Add TestJoinsFullSliceRegrownFromHint, which fails if the grow condition
reverts to cap(joins) == 0 (cap 8 from repeated doubling instead of a
hint-sized grow). Correct the joinsCap comment: it also runs when a full
slice is handed in. Merges origin/main.

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 2, 2026 •

Copy link
Copy Markdown

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


Review of PR #929: size joins slice from a JOIN lookahead

I read the diff against origin/main (select_clauses.go, CHANGELOG). I did not run tests or benchmarks.

Verdict: Safe to merge. The hint only changes slice capacity, so the AST can't change. append and slices.Grow still grow past the hint when it is too low.

What I checked

  • joinsCap() starts at n=1 for the JOIN being appended. p.currentPos is already past the consumed JOIN token, so that JOIN is not counted twice.
  • It counts JOINs only at depth 0 and returns at a depth-0 ;, UNION, EXCEPT or INTERSECT. It also returns at the ) that closes the enclosing level. The loop exits at n == 8 or after 256 tokens. This addresses the earlier cross-statement and nested-subquery over-allocation concerns.
  • slices.Grow(joins, hint) with len == cap is correct. For an empty slice it allocates hint slots. For a full slice it grows to at least len+hint.
  • The Go 1.26 toolchain supports slices and the builtin min.

Minor points (none blocking)

  1. Rescan beyond 8 joins. After the slice reaches cap 8, every further JOIN in the same statement hits len == cap and re-runs joinsCap(). Each call scans up to 256 tokens (the scan ends early once 8 more JOINs are counted). slices.Grow then falls back to roughly doubling. For 9+ joins this is O(joins × window), which is bounded and rare. A cheap guard is to scan only when cap(joins) == 0 or the slice was handed in by a caller. Otherwise use plain append. You could also skip the scan when len(joins) >= 8.
  2. Doc comment formatting. The joinsCap comment has a ragged line break ("nothing. The result / is a capacity hint only"). Re-wrap it.
  3. Hint accuracy for the full-slice case. When a parenthesized group or comma join hands in a full slice, joinsCap() returns at least 1, so the growth is only as good as the lookahead. The CHANGELOG already documents the (a JOIN b) JOIN c and a JOIN b, c edge costs, which is the right place for it.
  4. Compound or variant join tokens. If a dialect emits a join variant without TokenTypeJoin (for example ClickHouse ARRAY JOIN), the count comes out low. That is harmless.
  5. PR description. The Test Plan still carries numbers from earlier heads. The description says the gate "re-runs on the new head and its verdict will be added here". The latest gate comment (PASS) shows ParseE2E/join B/op -20.34% and complex -21.49%. Its time rows show +2.25% on aggregate and +1.22% on subquery, but those queries have no joins and instructions/op are unchanged. They look like layout noise. Update the description with that verdict.

Tests: The new join_hint_test.go asserts capacity as well as content, covering multi-statement scripts, set-operation branches and nested subqueries. BenchmarkParseManyShortJoinStatements keeps the multi-statement trade-off visible. Coverage looks good.

@github-actions

github-actions Bot commented Nov 3, 2026 •

Copy link
Copy Markdown

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


Review of PR #929: size joins slice from a JOIN lookahead

I read the diff against origin/main (select_clauses.go, CHANGELOG). I did not run tests or benchmarks. I did not re-read join_hint_test.go, so I'm relying on the PR description for what it covers.

Verdict: Safe to merge. The hint only changes slice capacity, so the AST can't change. append and slices.Grow still grow past a low hint.

What I checked

  • joinsCap() starts at n=1 for the JOIN being appended. p.currentPos is already past that JOIN token, so it isn't counted twice.
  • Only depth-0 JOINs are counted. The scan stops at a depth-0 ;, UNION, EXCEPT or INTERSECT, and at the ) that closes the enclosing level. It also stops at 8 or after 256 tokens. This fixes the earlier over-allocation across statements, set-operation branches and nested subqueries.
  • len == cap with slices.Grow(joins, hint) is correct. An empty slice gets hint slots. A full slice grows to at least len+hint.
  • Join-free statements never reach the scan, so they pay nothing.
  • The perf gate is PASS: ParseE2E/join B/op is -20.34% and complex is -21.49%. The join-free time movements are under the 3% threshold, and their instructions/op are unchanged.

Minor points (none blocking)

  1. Rescan beyond 8 joins. Once the slice reaches cap 8, every further JOIN in the same statement finds len == cap and calls joinsCap() again. Each call scans up to 256 tokens, then slices.Grow roughly doubles the slice. The cost is bounded and only affects rare 9+ join queries. If you want to avoid it, skip the scan when len(joins) >= 8 and use plain append.
  2. Doc comment wrapping. In the joinsCap comment, "nothing. The result" breaks onto a line of its own before "is a capacity hint only". Re-wrap that paragraph.
  3. Edge shapes. (a JOIN b) JOIN c and a JOIN b, c cost one extra allocation. The CHANGELOG already documents this, and I agree with accepting it.
  4. Join variants. If a dialect emits a join variant without TokenTypeJoin, the count comes out low. That is harmless.

@ajitpratap0
ajitpratap0 merged commit 36482b4 into main Nov 3, 2026
48 checks passed

This branch was successfully deployed

1 active deployment
Preview — 92bd5edc Deployed Nov 3, 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