Skip to content

perf(formatter): look up lower-case keywords in a constant table [W5-07] - #936

Open
ajitpratap0 wants to merge 5 commits into
mainfrom
perf/w5-07-lowercase-keyword-table
Open

ajitpratap0 wants to merge 5 commits into
mainfrom
perf/w5-07-lowercase-keyword-table

Conversation

@ajitpratap0

@ajitpratap0 ajitpratap0 commented Nov 3, 2026 •

Copy link
Copy Markdown
Owner

Summary

W5-07 (FMT-5). nodeFormatter.kw called strings.ToLower for every keyword in lower-case mode, allocating a string each time. It now uses lowerKeyword: a constant table of every keyword spelling the renderers emit, falling back to strings.ToLower only for user text, so output is byte-identical. The table covers DML/query keywords, the DDL writers in pkg/sql/ast (ast.DDLStyle{Keyword: f.kw}, sql_ddl_objects.go, sql_alter.go), the callback helpers (CTE search/cycle, sample, ROWS FROM, table-ref pre/post alias, table hints, PIVOT/UNPIVOT) and the ast helper return sets (join, set-op, alter-action, merge, lock, fetch/offset rows). Fix round 1 closed the DDL/helper gap found in review.

Test Plan

  • Tests: table entries equal strings.ToLower; lowerKeyword equals strings.ToLower on hits, misses, mixed-case, non-ASCII; kw allocates 0 for known keywords; every kw("literal") site in pkg/formatter AND every writeKw/kw literal site in pkg/sql/ast is in the table (go/parser scan); buildLowerKeywordTable panics on duplicates (tested); helper return sets (incl. parser join combinations) are checked against the table; TestLowerKeywordCorpusMisses renders the dialect corpus with a fallback counter and asserts zero misses for parser-produced ASTs.
  • Gate (after fix round 1): task check ok, task test:cbinding ok, go test -count=1 ./pkg/compatibility/... ok, staticcheck ./pkg/formatter clean.
  • Local task bench:ab BASE=origin/main BENCH='./pkg/formatter=^BenchmarkRender' (layout-randomized, shared M4, indicative only): Render/lower allocs/op geomean -29.0% (simple_select 6->4, select_where 12->7, join 27->20, aggregate 12->8, subquery 16->10, cte 25->17, window 7->5, complex 86->73, insert 18->15, update 15->12, delete 10->7), B/op -2.6..-7.5% per row, time geomean -10.4% (all p<0.001, -7.7..-13.1%). Compact/readable/sql rows unchanged (allocs equal, time within noise).
  • PLAN threshold, reported not hidden: allocs <= -30% is narrowly MISSED (-29.0% geomean; complex -15%, insert -17%, update -20%); time <= -15% is MISSED (-10.4%, local M4 only). The allocs/op cut is deterministic and platform-independent, so it carries to amd64 (same basis as W5-01/W5-02).
  • CI perf gate (layout-randomized, PR ci(perf): randomize code layout in the A/B perf gate so layout noise cannot read as a regression #934, base 6399dc0, head 33518fc): PASS, 52/52 ok, geomean -0.03%, A/A' 0/52 moved more than 3%, allocs/op and B/op unchanged, instructions/op within +-0.11%, low power on ParseE2E/aggregate only. The gate check on the current head also passes (shadow). LIMITATION: the gated suite has no Render/lower row, so amd64 time for the target is unmeasured; only the deterministic allocs/op drop carries over.
  • CodeQL reports one high alert in pkg/fingerprint/fingerprint.go:150 (SHA256 fingerprint, not touched by this PR).

Breaking Changes

None. No API or output change.

🤖 Generated with Claude Code

https://claude.ai/code/session_01DnbRnTFErhoMJdAjVEeCg2

@vercel

vercel Bot commented Nov 3, 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 6:37pm UTC

@github-actions github-actions Bot added the documentation Improvements or additions to documentation label Nov 3, 2026
@github-actions

github-actions Bot commented Nov 3, 2026 •

Copy link
Copy Markdown

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


Review of PR #936: lower-case keyword lookup table

Verdict: LGTM. I found no correctness issues. I read the diff but did not run the tests or benchmarks. The numbers below are from the PR description.

Correctness

  • lowerKeyword is equivalent to strings.ToLower for every input.
    • It returns the input unchanged only when no byte is A-Z or ≥0x80. In that case ToLower would also return it unchanged.
    • A table hit returns a precomputed strings.ToLower value.
    • Everything else falls back to strings.ToLower.
    • A missing table entry therefore costs an allocation and never changes the output.
  • TestLowerKeywordTable checks each entry against strings.ToLower. TestLowerKeywordMatchesToLower covers non-ASCII input, including İ, Σ and ß.

Performance

  • The input scan is a cheap linear pass over short strings. The map lookup adds some hashing cost, but it is much cheaper than an allocation.
  • The PR reports about -29% allocs/op and -8% time. That is believable and reported honestly, including the missed time target.
  • The table is built once at package init, which is a small one-time cost.

Minor suggestions (non-blocking)

  1. Duplicate entries in keyword_lower.go: "DELETE", "MATCHED", "NOT MATCHED", "NOT MATCHED BY SOURCE", "NOT MATCHED BY TARGET", "ROWS" and "DO NOTHING" appear in both the literal section and the helper section. This is harmless because the map dedupes them, but removing the repeats would tidy the list.
  2. Test blind spot. TestKwLiteralsAreInLowerKeywordTable only checks kw("literal") calls. Many call sites pass dynamic values, such as ast.JoinKeywordsText, AlterTableActionText, MergeActionText and LockTypeText. The "helper" section of the table is hand-maintained for those, and nothing enforces that it stays in sync. A missed entry only costs perf, so this is low risk. A comment in the table pointing at those helpers is already there, which is good. If the helpers expose an enumerable set of values, a test that iterates them would close the gap.
  3. Test hygiene. In TestKwLowerDoesNotAllocateForKnownKeywords, sink is assigned inside the closure and then discarded with _ = sink. That is fine, but AllocsPerRun can be flaky under -race. The package's race job should confirm it passes. If it flakes there, skip it when the race detector is on, as TestPerformanceRegression already does.
  4. Scanner robustness. The go/parser scan matches any method named kw with a single string-literal argument, not just nodeFormatter.kw. That is fine today, but it is a weak coupling. The seen < 50 sanity check is a good guard.

Security / API

No security concerns and no public API change. The CHANGELOG entry is accurate and follows the existing style.

@github-actions

github-actions Bot commented Nov 3, 2026 •

Copy link
Copy Markdown

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

Base a72ec74daf9a, head f022fceca5c5. 20 interleaved rounds, 20 randomized code layouts per arm (-randlayout seeds 416954327742..416954327761; A/A' 416954327762..416954327781), benchtime 100ms, -cpu 1, GOMEMLIMIT 64MiB, pinned to CPU 3 (SMT siblings 2-3) with taskset.

Runner CPU: AMD EPYC 7763 64-Core Processor (x86_64). Base and head ran interleaved in this job, so the tables show their ratios only; absolute timings (raw files in the artifact) hold for this CPU and this job alone and are never compared across jobs.

  • no gated metric moved past its threshold

Rules: each arm runs 20 randomized code layouts (-ldflags=-randlayout); time regresses at >= +3% when significant at a false discovery rate of 0.01 over the suite (Benjamini-Hochberg on exact Mann-Whitney U p-values of the per-layout medians, so a regression must hold across layouts, not within one binary), or a suite geomean >= +1.5% (per-layout suite 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%. The A/A' control runs the base revision under a disjoint set of layout seeds; when more than 10% of benchmarks move over 3% in it, the run is noisy: re-run once, then inconclusive (neutral).

Gated suite (GOGC=off, GOMAXPROCS=1)

benchmark time p A/A' spread B/op allocs/op status
ParseE2E/simple_select +0.30% 0.607 +0.09% 1.30% +0.00% 5 → 5 ok
ParseE2E/select_where -0.10% 0.871 +1.11% 2.68% +0.00% 18 → 18 ok
ParseE2E/join +0.28% 0.323 +0.66% 1.20% +0.00% 17 → 17 ok
ParseE2E/aggregate +0.10% 0.903 +1.48% 2.72% +0.00% 18 → 18 ok
ParseE2E/subquery -0.06% 0.882 +0.32% 2.15% +0.00% 14 → 14 ok
ParseE2E/cte +0.23% 0.735 +0.83% 1.85% +0.00% 21 → 21 ok
ParseE2E/window +0.41% 0.317 +0.41% 1.24% +0.00% 11 → 11 ok
ParseE2E/complex -0.77% 0.208 +0.28% 2.56% +0.00% 49 → 49 ok
ParseE2E/insert -0.46% 0.199 -0.28% 1.56% +0.00% 17 → 17 ok
ParseE2E/update -0.28% 0.525 +0.08% 1.37% +0.00% 13 → 13 ok
ParseE2E/delete +0.00% 0.989 -0.36% 1.55% +0.00% 5 → 5 ok
ParseRW/postgresql/medium +0.44% 0.429 +0.33% 2.42% +0.00% 646 → 646 ok
ParseRW/mysql/medium -0.22% 0.565 +0.10% 1.08% +0.00% 368 → 368 ok
ParseRW/mariadb/medium -0.27% 0.441 +0.13% 1.02% -0.00% 528 → 528 ok
ParseRW/sqlserver/medium +0.09% 0.904 -0.03% 1.05% +0.00% 927 → 927 ok
ParseRW/oracle/medium +0.05% 0.904 -0.13% 0.92% +0.00% 1090 → 1090 ok
ParseRW/sqlite/medium +0.14% 0.862 -0.68% 4.53% +0.00% 347 → 347 ok
ParseRW/snowflake/medium +0.78% 0.076 +0.43% 0.75% +0.00% 776 → 776 ok
ParseRW/clickhouse/medium +2.36% 0.030 +1.17% 3.17% +0.00% 383 → 383 ok
ParseFresh/simple_select +0.25% 0.570 +0.30% 1.60% +0.00% 6 → 6 ok
ParseFresh/join +0.37% 0.304 +0.77% 1.18% +0.00% 18 → 18 ok
ParseFresh/complex +0.28% 0.402 +0.29% 0.95% +0.00% 50 → 50 ok
PoolGetPut/serial +0.14% 0.914 +0.17% 2.02% +0.00% 0 → 0 ok
Tokenize/scratch/join +0.47% 0.675 +0.13% 1.76% +0.00% 0 → 0 ok
Tokenize/scratch/complex -0.17% 0.914 -0.42% 1.55% +0.00% 0 → 0 ok
Tokenize/scratch/insert -0.56% 0.646 -0.21% 2.74% +0.00% 0 → 0 ok
Normalize/simple_select -0.34% 0.417 -0.34% 1.13% +0.00% 7 → 7 ok
Normalize/join +0.06% 0.871 +0.36% 1.53% +0.00% 23 → 23 ok
Normalize/complex +0.09% 0.818 +0.10% 1.41% +0.00% 68 → 68 ok
TransformParse/simple_select -0.30% 0.588 -0.52% 2.16% +0.00% 5 → 5 ok
TransformParse/join +0.34% 0.304 +0.45% 1.08% +0.00% 17 → 17 ok
TransformParse/complex +0.56% 0.478 -0.07% 1.88% +0.00% 49 → 49 ok
LintParallel/rules +0.19% 0.383 +0.18% 0.41% +0.00% 229685 → 229686 ok
LintParallel/lint +0.24% 0.478 +0.23% 1.86% -0.00% 308265.5 → 308265 ok
LintRW/postgresql -0.27% 0.620 +1.09% 1.52% +0.00% 34190 → 34190 ok
LintRW/mysql +0.20% 0.620 +0.89% 1.38% -0.00% 41490 → 41490 ok
LintRW/mariadb +0.22% 0.620 +0.39% 1.39% +0.00% 38235 → 38235 ok
LintRW/sqlserver +0.53% 0.183 +0.39% 1.55% +0.00% 34620 → 34620 ok
LintRW/oracle +0.70% 0.277 +0.33% 1.83% -0.00% 42244 → 42243 -allocs
LintRW/sqlite +0.22% 0.678 +0.12% 1.05% +0.00% 37576 → 37576 ok
LintRW/snowflake +1.15% 0.102 +1.51% 2.33% +0.00% 37559 → 37559 ok
LintRW/clickhouse -2.35% 0.005 -1.85% 1.54% +0.00% 42343 → 42343 ok
LintRule/L007 +0.26% 0.820 -0.32% 0.82% -0.00% 72565 → 72565 ok
LintRule/L010 -0.07% 0.738 -0.14% 7.47% +0.00% 0 → 0 ok
ParseError/syntax_mid/parse -0.37% 0.372 +0.57% 0.94% +0.00% 12 → 12 ok
ParseError/syntax_mid/validate -0.27% 0.598 +0.68% 1.29% +0.00% 12 → 12 ok
ParseError/syntax_end/parse +0.68% 0.310 -0.02% 1.60% +0.00% 16 → 16 ok
ParseError/syntax_end/validate +0.78% 0.310 +0.96% 2.37% +0.00% 16 → 16 ok
ParseError/tokenize/parse -0.87% 0.314 -0.76% 1.65% +0.00% 4 → 4 ok
ParseError/tokenize/validate +1.04% 0.137 +0.50% 2.07% +0.00% 21 → 21 ok
ParseError/long_prefix/parse +0.17% 0.678 -0.46% 1.57% -0.00% 219 → 219 ok
ParseError/long_prefix/validate +0.15% 0.820 -0.49% 2.48% +0.00% 219 → 219 ok
geomean +0.12% 0.883 +0.21%

time and A/A' are Hodges-Lehmann shifts over the per-layout medians. spread: the base arm's robust standard deviation across its layouts (1.4826 x MAD, relative), i.e. how much this benchmark moves between builds that differ only in function placement, plus run-to-run noise; a change much smaller than it cannot be resolved in one run.

A/A' control (base revision, disjoint layout seeds): 0 of 52 benchmarks moved more than 3%; 0 would have been flagged by the time rule; suite geomean +0.21%, p 0.327.

Low power

2 benchmark(s) have under 80% estimated power to be flagged on their own for a +5% slowdown (their spread across layouts is too wide for 20 layouts at the Benjamini-Hochberg level alpha/m = 0.000192). A slowdown below the last column usually goes unflagged there; instructions/op, allocs/op and the suite geomean still guard them.

benchmark spread power at +5% slowdown caught 80% of the time
LintRule/L010 7.47% 4% +11.7%
ParseRW/sqlite/medium 4.53% 34% +6.9%

Power is estimated with the normal approximation for the U test (Noether): power = Phi(ln(1+shift)/s * sqrt(K/2) * sqrt(3/pi) - z(1 - alpha/(2m))), with s the spread and K the layouts per arm.

Instructions per operation (cmd/perfdriver)

case instructions/op status
e2e/aggregate +0.01% ok
e2e/complex -0.01% ok
e2e/cte +0.00% ok
e2e/delete +0.08% ok
e2e/insert -0.01% ok
e2e/join -0.06% ok
e2e/select_where -0.05% ok
e2e/simple_select +0.07% ok
e2e/subquery -0.04% ok
e2e/update -0.08% ok
e2e/window -0.01% ok
rw/postgresql/medium +0.05% ok
Secondary metric: default GC, GOMAXPROCS=1 (informational)
benchmark time p
ParseE2E/simple_select +0.19% 0.791
ParseE2E/select_where +0.67% 0.579
ParseE2E/join +0.29% 0.853
ParseE2E/aggregate +0.26% 0.796
ParseE2E/subquery +0.27% 0.880
ParseE2E/cte -0.13% 0.971
ParseE2E/window +0.14% 0.880
ParseE2E/complex +0.10% 0.796
ParseE2E/insert +0.28% 0.623
ParseE2E/update -0.46% 0.650
ParseE2E/delete -0.67% 0.384
ParseRW/postgresql/medium +0.31% 0.529
ParseRW/mysql/medium -0.25% 0.912
ParseRW/mariadb/medium +0.91% 0.579
ParseRW/sqlserver/medium -1.00% 0.019
ParseRW/oracle/medium -1.11% 0.123
ParseRW/sqlite/medium -0.09% 0.912
ParseRW/snowflake/medium +1.16% 0.190
ParseRW/clickhouse/medium +0.56% 0.739
ParseFresh/simple_select +0.95% 0.218
ParseFresh/join +1.63% 0.075
ParseFresh/complex +2.51% 0.015
PoolGetPut/serial -0.38% 0.597
Tokenize/scratch/join +0.95% 0.290
Tokenize/scratch/complex +0.72% 0.481
Tokenize/scratch/insert -0.26% 0.971
Normalize/simple_select +0.15% 0.940
Normalize/join +1.29% 0.035
Normalize/complex +0.77% 0.123
TransformParse/simple_select +1.08% 0.198
TransformParse/join -0.05% 0.970
TransformParse/complex -0.14% 0.739
LintParallel/rules +0.15% 0.971
LintParallel/lint +0.40% 0.853
LintRW/postgresql -0.37% 0.796
LintRW/mysql +0.34% 0.739
LintRW/mariadb -0.45% 0.853
LintRW/sqlserver -0.14% 0.853
LintRW/oracle +1.07% 0.353
LintRW/sqlite -1.15% 0.529
LintRW/snowflake +0.27% 0.796
LintRW/clickhouse -1.35% 0.353
LintRule/L007 +0.26% 0.739
LintRule/L010 -0.35% 0.579
ParseError/syntax_mid/parse +2.09% 0.151
ParseError/syntax_mid/validate +1.35% 0.218
ParseError/syntax_end/parse +0.78% 0.631
ParseError/syntax_end/validate +1.15% 0.190
ParseError/tokenize/parse +1.82% 0.545
ParseError/tokenize/validate -0.06% 0.971
ParseError/long_prefix/parse +2.09% 0.190
ParseError/long_prefix/validate +3.18% 0.043
geomean +0.41%
Environment
date:        2026-11-03T19:10:00Z
base:        a72ec74daf9a3bd7ca53e7642ff407795df5525d
head:        working tree of f022fceca5c5ab4171f80a070753c6d24003ff2b
cpu model:   AMD EPYC 7763 64-Core Processor (x86_64)
go:          go version go1.26.9 linux/amd64
kernel:      Linux 7.0.0-1017-azure x86_64
layouts:     20 per arm, -ldflags=-randlayout seeds 416954327742..416954327761 (base, head), 416954327762..416954327781 (A/A')
pinning:     pinned to CPU 3 (SMT siblings 2-3) with taskset
gcoff env:   GOGC=off GOMEMLIMIT=64MiB
clocksource: tsc
load:        7.11, 2.60, 0.94
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:                              AMD EPYC 7763 64-Core Processor
Thread(s) per core:                      2
Core(s) per socket:                      2
Socket(s):                               1
Hypervisor vendor:                       Microsoft
L1d cache:                               64 KiB (2 instances)
L1i cache:                               64 KiB (2 instances)
L2 cache:                                1 MiB (2 instances)
L3 cache:                                32 MiB (1 instance)
benchstat (primary metric, samples pooled across layouts: informational, the verdict uses the per-layout test)
goos: linux
goarch: amd64
pkg: github.com/ajitpratap0/GoSQLX/pkg/fingerprint
cpu: AMD EPYC 7763 64-Core Processor                
                        │    base     │                head                │
                        │   sec/op    │   sec/op     vs base               │
Normalize/simple_select   2.354µ ± 1%   2.344µ ± 1%       ~ (p=0.417 n=20)
Normalize/join            7.206µ ± 1%   7.218µ ± 1%       ~ (p=0.867 n=20)
Normalize/complex         19.79µ ± 1%   19.80µ ± 2%       ~ (p=0.815 n=20)
geomean                   6.950µ        6.945µ       -0.07%

                        │     base     │                 head                  │
                        │     B/op     │     B/op      vs base                 │
Normalize/simple_select     896.0 ± 0%     896.0 ± 0%       ~ (p=1.000 n=20) ¹
Normalize/join            2.211Ki ± 0%   2.211Ki ± 0%       ~ (p=1.000 n=20) ¹
Normalize/complex         6.117Ki ± 0%   6.117Ki ± 0%       ~ (p=1.000 n=20) ¹
geomean                   2.279Ki        2.279Ki       +0.00%
¹ 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=20) ¹
Normalize/join            23.00 ± 0%   23.00 ± 0%       ~ (p=1.000 n=20) ¹
Normalize/complex         68.00 ± 0%   68.00 ± 0%       ~ (p=1.000 n=20) ¹
geomean                   22.20        22.20       +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.657µ ± 2%   1.654µ ± 1%       ~ (p=0.606 n=20)
ParseE2E/select_where             3.480µ ± 3%   3.476µ ± 3%       ~ (p=0.867 n=20)
ParseE2E/join                     4.639µ ± 1%   4.637µ ± 1%       ~ (p=0.324 n=20)
ParseE2E/aggregate                3.483µ ± 2%   3.473µ ± 1%       ~ (p=0.899 n=20)
ParseE2E/subquery                 3.142µ ± 1%   3.138µ ± 0%       ~ (p=0.878 n=20)
ParseE2E/cte                      4.447µ ± 1%   4.458µ ± 1%       ~ (p=0.733 n=20)
ParseE2E/window                   2.925µ ± 1%   2.943µ ± 1%       ~ (p=0.317 n=20)
ParseE2E/complex                  12.30µ ± 2%   12.26µ ± 2%       ~ (p=0.209 n=20)
ParseE2E/insert                   2.187µ ± 2%   2.182µ ± 1%       ~ (p=0.199 n=20)
ParseE2E/update                   1.948µ ± 1%   1.943µ ± 1%       ~ (p=0.525 n=20)
ParseE2E/delete                   1.247µ ± 1%   1.252µ ± 1%       ~ (p=0.984 n=20)
ParseRW/postgresql/medium         171.5µ ± 2%   172.7µ ± 1%       ~ (p=0.429 n=20)
ParseRW/mysql/medium              97.04µ ± 1%   96.99µ ± 1%       ~ (p=0.565 n=20)
ParseRW/mariadb/medium            138.7µ ± 1%   138.6µ ± 1%       ~ (p=0.441 n=20)
ParseRW/sqlserver/medium          278.5µ ± 1%   279.2µ ± 1%       ~ (p=0.904 n=20)
ParseRW/oracle/medium             329.0µ ± 1%   330.1µ ± 1%       ~ (p=0.904 n=20)
ParseRW/sqlite/medium             74.44µ ± 4%   73.99µ ± 1%       ~ (p=0.862 n=20)
ParseRW/snowflake/medium          189.8µ ± 1%   191.4µ ± 1%       ~ (p=0.076 n=20)
ParseRW/clickhouse/medium         87.14µ ± 3%   89.21µ ± 3%       ~ (p=0.030 n=20)
ParseFresh/simple_select          7.109µ ± 2%   7.124µ ± 1%       ~ (p=0.569 n=20)
ParseFresh/join                   11.48µ ± 1%   11.50µ ± 0%       ~ (p=0.304 n=20)
ParseFresh/complex                20.73µ ± 0%   20.79µ ± 1%       ~ (p=0.402 n=20)
PoolGetPut/serial                 67.25n ± 1%   67.05n ± 1%       ~ (p=0.909 n=20)
ParseError/syntax_mid/parse       2.441µ ± 1%   2.424µ ± 1%       ~ (p=0.372 n=20)
ParseError/syntax_mid/validate    2.189µ ± 1%   2.187µ ± 1%       ~ (p=0.597 n=20)
ParseError/syntax_end/parse       2.827µ ± 2%   2.835µ ± 0%       ~ (p=0.311 n=20)
ParseError/syntax_end/validate    2.659µ ± 2%   2.692µ ± 2%       ~ (p=0.311 n=20)
ParseError/tokenize/parse         916.8n ± 2%   909.1n ± 2%       ~ (p=0.314 n=20)
ParseError/tokenize/validate      2.863µ ± 2%   2.893µ ± 1%       ~ (p=0.136 n=20)
ParseError/long_prefix/parse      60.55µ ± 1%   60.54µ ± 1%       ~ (p=0.678 n=20)
ParseError/long_prefix/validate   57.29µ ± 2%   57.20µ ± 2%       ~ (p=0.820 n=20)
geomean                           9.585µ        9.600µ       +0.15%

                                │     base     │                head                 │
                                │     B/s      │     B/s       vs base               │
ParseE2E/simple_select            14.97Mi ± 2%   15.00Mi ± 1%       ~ (p=0.588 n=20)
ParseE2E/select_where             28.22Mi ± 2%   28.26Mi ± 3%       ~ (p=0.846 n=20)
ParseE2E/join                     25.90Mi ± 1%   25.92Mi ± 1%       ~ (p=0.324 n=20)
ParseE2E/aggregate                23.00Mi ± 2%   23.06Mi ± 1%       ~ (p=0.888 n=20)
ParseE2E/subquery                 24.89Mi ± 1%   24.92Mi ± 0%       ~ (p=0.867 n=20)
ParseE2E/cte                      26.59Mi ± 1%   26.53Mi ± 1%       ~ (p=0.763 n=20)
ParseE2E/window                   30.65Mi ± 1%   30.46Mi ± 1%       ~ (p=0.317 n=20)
ParseE2E/complex                  29.31Mi ± 2%   29.41Mi ± 2%       ~ (p=0.204 n=20)
ParseE2E/insert                   33.14Mi ± 2%   33.21Mi ± 1%       ~ (p=0.218 n=20)
ParseE2E/update                   26.93Mi ± 1%   27.00Mi ± 1%       ~ (p=0.533 n=20)
ParseE2E/delete                   39.76Mi ± 1%   39.63Mi ± 2%       ~ (p=0.995 n=20)
ParseRW/postgresql/medium         20.91Mi ± 2%   20.78Mi ± 1%       ~ (p=0.410 n=20)
ParseRW/mysql/medium              27.89Mi ± 1%   27.91Mi ± 1%       ~ (p=0.588 n=20)
ParseRW/mariadb/medium            25.57Mi ± 1%   25.59Mi ± 1%       ~ (p=0.433 n=20)
ParseRW/sqlserver/medium          23.89Mi ± 1%   23.83Mi ± 1%       ~ (p=0.909 n=20)
ParseRW/oracle/medium             25.48Mi ± 1%   25.40Mi ± 1%       ~ (p=0.899 n=20)
ParseRW/sqlite/medium             27.75Mi ± 3%   27.92Mi ± 1%       ~ (p=0.857 n=20)
ParseRW/snowflake/medium          26.79Mi ± 1%   26.56Mi ± 1%       ~ (p=0.075 n=20)
ParseRW/clickhouse/medium         25.86Mi ± 3%   25.26Mi ± 2%       ~ (p=0.028 n=20)
ParseError/syntax_mid/parse       18.76Mi ± 1%   18.89Mi ± 1%       ~ (p=0.379 n=20)
ParseError/syntax_mid/validate    20.91Mi ± 1%   20.93Mi ± 1%       ~ (p=0.578 n=20)
ParseError/syntax_end/parse       13.16Mi ± 2%   13.12Mi ± 1%       ~ (p=0.298 n=20)
ParseError/syntax_end/validate    13.99Mi ± 2%   13.82Mi ± 2%       ~ (p=0.317 n=20)
ParseError/tokenize/parse         47.85Mi ± 1%   48.25Mi ± 2%       ~ (p=0.314 n=20)
ParseError/tokenize/validate      15.32Mi ± 2%   15.16Mi ± 1%       ~ (p=0.136 n=20)
ParseError/long_prefix/parse      38.15Mi ± 1%   38.16Mi ± 1%       ~ (p=0.683 n=20)
ParseError/long_prefix/validate   40.32Mi ± 2%   40.38Mi ± 2%       ~ (p=0.815 n=20)
geomean                           25.29Mi        25.25Mi       -0.15%

                                │      base      │                 head                  │
                                │      B/op      │     B/op      vs base                 │
ParseE2E/simple_select              816.0 ± 0%       816.0 ± 0%       ~ (p=1.000 n=20) ¹
ParseE2E/select_where             1.523Ki ± 0%     1.523Ki ± 0%       ~ (p=1.000 n=20) ¹
ParseE2E/join                     1.836Ki ± 0%     1.836Ki ± 0%       ~ (p=1.000 n=20) ¹
ParseE2E/aggregate                1.812Ki ± 0%     1.812Ki ± 0%       ~ (p=1.000 n=20) ¹
ParseE2E/subquery                 1.828Ki ± 0%     1.828Ki ± 0%       ~ (p=1.000 n=20) ¹
ParseE2E/cte                      2.336Ki ± 0%     2.336Ki ± 0%       ~ (p=1.000 n=20) ¹
ParseE2E/window                   1.281Ki ± 0%     1.281Ki ± 0%       ~ (p=1.000 n=20) ¹
ParseE2E/complex                  5.025Ki ± 0%     5.025Ki ± 0%       ~ (p=1.000 n=20) ¹
ParseE2E/insert                     824.0 ± 0%       824.0 ± 0%       ~ (p=1.000 n=20) ¹
ParseE2E/update                     736.0 ± 0%       736.0 ± 0%       ~ (p=1.000 n=20) ¹
ParseE2E/delete                     400.0 ± 0%       400.0 ± 0%       ~ (p=1.000 n=20) ¹
ParseRW/postgresql/medium         65.91Ki ± 0%     65.91Ki ± 0%       ~ (p=0.159 n=20)
ParseRW/mysql/medium              42.37Ki ± 0%     42.37Ki ± 0%       ~ (p=1.000 n=20)
ParseRW/mariadb/medium            60.61Ki ± 0%     60.61Ki ± 0%       ~ (p=0.483 n=20)
ParseRW/sqlserver/medium          83.68Ki ± 0%     83.68Ki ± 0%       ~ (p=0.827 n=20)
ParseRW/oracle/medium             104.8Ki ± 0%     104.8Ki ± 0%       ~ (p=0.474 n=20)
ParseRW/sqlite/medium             38.12Ki ± 0%     38.12Ki ± 0%       ~ (p=1.000 n=20) ¹
ParseRW/snowflake/medium          77.63Ki ± 0%     77.63Ki ± 0%       ~ (p=0.022 n=20)
ParseRW/clickhouse/medium         34.82Ki ± 0%     34.82Ki ± 0%       ~ (p=1.000 n=20) ¹
ParseFresh/simple_select            896.0 ± 0%       896.0 ± 0%       ~ (p=1.000 n=20) ¹
ParseFresh/join                   1.914Ki ± 0%     1.914Ki ± 0%       ~ (p=1.000 n=20) ¹
ParseFresh/complex                5.102Ki ± 0%     5.102Ki ± 0%       ~ (p=1.000 n=20) ¹
PoolGetPut/serial                   0.000 ± 0%       0.000 ± 0%       ~ (p=1.000 n=20) ¹
ParseError/syntax_mid/parse         680.0 ± 0%       680.0 ± 0%       ~ (p=1.000 n=20) ¹
ParseError/syntax_mid/validate      680.0 ± 0%       680.0 ± 0%       ~ (p=1.000 n=20) ¹
ParseError/syntax_end/parse       1.430Ki ± 0%     1.430Ki ± 0%       ~ (p=1.000 n=20) ¹
ParseError/syntax_end/validate    1.430Ki ± 0%     1.430Ki ± 0%       ~ (p=1.000 n=20) ¹
ParseError/tokenize/parse           272.0 ± 0%       272.0 ± 0%       ~ (p=1.000 n=20) ¹
ParseError/tokenize/validate      1.758Ki ± 0%     1.758Ki ± 0%       ~ (p=1.000 n=20) ¹
ParseError/long_prefix/parse      22.75Ki ± 0%     22.75Ki ± 0%       ~ (p=1.000 n=20)
ParseError/long_prefix/validate   22.68Ki ± 0%     22.68Ki ± 0%       ~ (p=1.000 n=20) ¹
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=20) ¹
ParseE2E/select_where              18.00 ± 0%      18.00 ± 0%       ~ (p=1.000 n=20) ¹
ParseE2E/join                      17.00 ± 0%      17.00 ± 0%       ~ (p=1.000 n=20) ¹
ParseE2E/aggregate                 18.00 ± 0%      18.00 ± 0%       ~ (p=1.000 n=20) ¹
ParseE2E/subquery                  14.00 ± 0%      14.00 ± 0%       ~ (p=1.000 n=20) ¹
ParseE2E/cte                       21.00 ± 0%      21.00 ± 0%       ~ (p=1.000 n=20) ¹
ParseE2E/window                    11.00 ± 0%      11.00 ± 0%       ~ (p=1.000 n=20) ¹
ParseE2E/complex                   49.00 ± 0%      49.00 ± 0%       ~ (p=1.000 n=20) ¹
ParseE2E/insert                    17.00 ± 0%      17.00 ± 0%       ~ (p=1.000 n=20) ¹
ParseE2E/update                    13.00 ± 0%      13.00 ± 0%       ~ (p=1.000 n=20) ¹
ParseE2E/delete                    5.000 ± 0%      5.000 ± 0%       ~ (p=1.000 n=20) ¹
ParseRW/postgresql/medium          646.0 ± 0%      646.0 ± 0%       ~ (p=1.000 n=20) ¹
ParseRW/mysql/medium               368.0 ± 0%      368.0 ± 0%       ~ (p=1.000 n=20) ¹
ParseRW/mariadb/medium             528.0 ± 0%      528.0 ± 0%       ~ (p=1.000 n=20) ¹
ParseRW/sqlserver/medium           927.0 ± 0%      927.0 ± 0%       ~ (p=1.000 n=20) ¹
ParseRW/oracle/medium             1.090k ± 0%     1.090k ± 0%       ~ (p=1.000 n=20) ¹
ParseRW/sqlite/medium              347.0 ± 0%      347.0 ± 0%       ~ (p=1.000 n=20) ¹
ParseRW/snowflake/medium           776.0 ± 0%      776.0 ± 0%       ~ (p=1.000 n=20) ¹
ParseRW/clickhouse/medium          383.0 ± 0%      383.0 ± 0%       ~ (p=1.000 n=20) ¹
ParseFresh/simple_select           6.000 ± 0%      6.000 ± 0%       ~ (p=1.000 n=20) ¹
ParseFresh/join                    18.00 ± 0%      18.00 ± 0%       ~ (p=1.000 n=20) ¹
ParseFresh/complex                 50.00 ± 0%      50.00 ± 0%       ~ (p=1.000 n=20) ¹
PoolGetPut/serial                  0.000 ± 0%      0.000 ± 0%       ~ (p=1.000 n=20) ¹
ParseError/syntax_mid/parse        12.00 ± 0%      12.00 ± 0%       ~ (p=1.000 n=20) ¹
ParseError/syntax_mid/validate     12.00 ± 0%      12.00 ± 0%       ~ (p=1.000 n=20) ¹
ParseError/syntax_end/parse        16.00 ± 0%      16.00 ± 0%       ~ (p=1.000 n=20) ¹
ParseError/syntax_end/validate     16.00 ± 0%      16.00 ± 0%       ~ (p=1.000 n=20) ¹
ParseError/tokenize/parse          4.000 ± 0%      4.000 ± 0%       ~ (p=1.000 n=20) ¹
ParseError/tokenize/validate       21.00 ± 0%      21.00 ± 0%       ~ (p=1.000 n=20) ¹
ParseError/long_prefix/parse       219.0 ± 0%      219.0 ± 0%       ~ (p=1.000 n=20) ¹
ParseError/long_prefix/validate    219.0 ± 0%      219.0 ± 0%       ~ (p=1.000 n=20) ¹
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   14.29µ ± 2%   14.39µ ± 1%       ~ (p=0.425 n=20)
ParseRW/mysql/medium        12.13µ ± 1%   12.12µ ± 1%       ~ (p=0.570 n=20)
ParseRW/mariadb/medium      15.41µ ± 1%   15.40µ ± 1%       ~ (p=0.433 n=20)
ParseRW/sqlserver/medium    13.26µ ± 1%   13.30µ ± 1%       ~ (p=0.899 n=20)
ParseRW/oracle/medium       13.71µ ± 1%   13.75µ ± 1%       ~ (p=0.889 n=20)
ParseRW/sqlite/medium       12.41µ ± 4%   12.33µ ± 1%       ~ (p=0.862 n=20)
ParseRW/snowflake/medium    14.60µ ± 1%   14.73µ ± 1%       ~ (p=0.075 n=20)
ParseRW/clickhouse/medium   12.45µ ± 3%   12.74µ ± 3%       ~ (p=0.029 n=20)
geomean                     13.49µ        13.55µ       +0.46%

pkg: github.com/ajitpratap0/GoSQLX/pkg/linter
                   │     base     │                head                 │
                   │    sec/op    │    sec/op     vs base               │
LintParallel/rules   35.98m ±  0%   36.03m ±  0%       ~ (p=0.383 n=20)
LintParallel/lint    56.57m ±  1%   56.64m ±  1%       ~ (p=0.478 n=20)
LintRW/postgresql    7.443m ±  2%   7.393m ±  1%       ~ (p=0.620 n=20)
LintRW/mysql         7.585m ±  1%   7.605m ±  1%       ~ (p=0.620 n=20)
LintRW/mariadb       7.016m ±  1%   7.031m ±  1%       ~ (p=0.620 n=20)
LintRW/sqlserver     6.556m ±  1%   6.580m ±  1%       ~ (p=0.183 n=20)
LintRW/oracle        7.891m ±  1%   7.965m ±  1%       ~ (p=0.277 n=20)
LintRW/sqlite        6.671m ±  1%   6.674m ±  1%       ~ (p=0.678 n=20)
LintRW/snowflake     6.819m ±  4%   6.850m ±  1%       ~ (p=0.102 n=20)
LintRW/clickhouse    7.738m ±  1%   7.598m ±  4%  -1.81% (p=0.005 n=20)
LintRule/L007        8.091m ±  1%   8.085m ±  3%       ~ (p=0.820 n=20)
LintRule/L010        735.7µ ± 21%   807.2µ ± 13%       ~ (p=0.738 n=20)
geomean              8.162m         8.225m        +0.77%

                  │     base     │                head                 │
                  │     B/s      │     B/s       vs base               │
LintRW/postgresql   3.805Mi ± 2%   3.834Mi ± 1%       ~ (p=0.624 n=20)
LintRW/mysql        4.315Mi ± 1%   4.301Mi ± 1%       ~ (p=0.578 n=20)
LintRW/mariadb      4.349Mi ± 1%   4.339Mi ± 1%       ~ (p=0.683 n=20)
LintRW/sqlserver    4.725Mi ± 1%   4.711Mi ± 1%       ~ (p=0.228 n=20)
LintRW/oracle       5.035Mi ± 1%   4.983Mi ± 1%       ~ (p=0.261 n=20)
LintRW/sqlite       4.301Mi ± 1%   4.301Mi ± 1%       ~ (p=0.644 n=20)
LintRW/snowflake    4.573Mi ± 4%   4.554Mi ± 1%       ~ (p=0.115 n=20)
LintRW/clickhouse   4.196Mi ± 1%   4.272Mi ± 5%  +1.82% (p=0.005 n=20)
geomean             4.399Mi        4.400Mi       +0.03%

                   │      base      │                 head                  │
                   │      B/op      │     B/op      vs base                 │
LintParallel/rules   10.96Mi ± 0%     10.96Mi ± 0%       ~ (p=0.753 n=20)
LintParallel/lint    30.93Mi ± 0%     30.93Mi ± 0%       ~ (p=0.167 n=20)
LintRW/postgresql    3.402Mi ± 0%     3.402Mi ± 0%       ~ (p=0.931 n=20)
LintRW/mysql         4.243Mi ± 0%     4.243Mi ± 0%       ~ (p=0.763 n=20)
LintRW/mariadb       3.924Mi ± 0%     3.924Mi ± 0%       ~ (p=0.920 n=20)
LintRW/sqlserver     3.488Mi ± 0%     3.488Mi ± 0%       ~ (p=0.507 n=20)
LintRW/oracle        4.242Mi ± 0%     4.242Mi ± 0%       ~ (p=0.239 n=20)
LintRW/sqlite        3.697Mi ± 0%     3.697Mi ± 0%       ~ (p=0.465 n=20)
LintRW/snowflake     3.745Mi ± 0%     3.745Mi ± 0%       ~ (p=0.615 n=20)
LintRW/clickhouse    4.190Mi ± 0%     4.190Mi ± 0%       ~ (p=0.208 n=20)
LintRule/L007        2.942Mi ± 0%     2.942Mi ± 0%  -0.00% (p=0.006 n=20)
LintRule/L010          0.000 ± 0%       0.000 ± 0%       ~ (p=1.000 n=20) ¹
geomean                           ²                 +0.00%                ²
¹ all samples are equal
² summaries must be >0 to compute geomean

                   │     base      │                 head                 │
                   │   allocs/op   │  allocs/op   vs base                 │
LintParallel/rules   229.7k ± 0%     229.7k ± 0%       ~ (p=0.552 n=20)
LintParallel/lint    308.3k ± 0%     308.3k ± 0%       ~ (p=0.366 n=20)
LintRW/postgresql    34.19k ± 0%     34.19k ± 0%       ~ (p=1.000 n=20)
LintRW/mysql         41.49k ± 0%     41.49k ± 0%       ~ (p=0.407 n=20)
LintRW/mariadb       38.23k ± 0%     38.23k ± 0%       ~ (p=1.000 n=20)
LintRW/sqlserver     34.62k ± 0%     34.62k ± 0%       ~ (p=1.000 n=20) ¹
LintRW/oracle        42.24k ± 0%     42.24k ± 0%  -0.00% (p=0.008 n=20)
LintRW/sqlite        37.58k ± 0%     37.58k ± 0%       ~ (p=1.000 n=20) ¹
LintRW/snowflake     37.56k ± 0%     37.56k ± 0%       ~ (p=0.695 n=20)
LintRW/clickhouse    42.34k ± 0%     42.34k ± 0%       ~ (p=1.000 n=20)
LintRule/L007        72.56k ± 0%     72.56k ± 0%       ~ (p=1.000 n=20) ¹
LintRule/L010         0.000 ± 0%      0.000 ± 0%       ~ (p=1.000 n=20) ¹
geomean                          ²                -0.00%                ²
¹ all samples are equal
² summaries must be >0 to compute geomean

                  │     base     │                head                 │
                  │   sec/stmt   │   sec/stmt    vs base               │
LintRW/postgresql   24.40µ ±  2%   24.24µ ±  1%       ~ (p=0.620 n=20)
LintRW/mysql        18.32µ ±  1%   18.37µ ±  1%       ~ (p=0.607 n=20)
LintRW/mariadb      18.46µ ±  1%   18.50µ ±  1%       ~ (p=0.625 n=20)
LintRW/sqlserver    23.67µ ±  1%   23.75µ ±  1%       ~ (p=0.180 n=20)
LintRW/oracle       21.44µ ±  1%   21.64µ ±  1%       ~ (p=0.277 n=20)
LintRW/sqlite       19.17µ ±  1%   19.18µ ±  1%       ~ (p=0.678 n=20)
LintRW/snowflake    20.85µ ±  4%   20.95µ ±  1%       ~ (p=0.102 n=20)
LintRW/clickhouse   19.64µ ±  1%   19.28µ ±  4%  -1.81% (p=0.005 n=20)
LintRule/L007       2.877µ ±  1%   2.874µ ±  3%       ~ (p=0.815 n=20)
LintRule/L010       261.5n ± 21%   286.9n ± 13%       ~ (p=0.774 n=20)
geomean             10.95µ         11.05µ        +0.90%

pkg: github.com/ajitpratap0/GoSQLX/pkg/sql/tokenizer
                         │    base     │                head                │
                         │   sec/op    │   sec/op     vs base               │
Tokenize/scratch/join      1.473µ ± 2%   1.484µ ± 2%       ~ (p=0.673 n=20)
Tokenize/scratch/complex   3.930µ ± 2%   3.921µ ± 2%       ~ (p=0.910 n=20)
Tokenize/scratch/insert    742.2n ± 2%   733.1n ± 2%       ~ (p=0.644 n=20)
geomean                    1.626µ        1.622µ       -0.22%

                         │     base     │                head                 │
                         │     B/s      │     B/s       vs base               │
Tokenize/scratch/join      81.56Mi ± 2%   80.95Mi ± 2%       ~ (p=0.678 n=20)
Tokenize/scratch/complex   91.74Mi ± 2%   91.92Mi ± 2%       ~ (p=0.904 n=20)
Tokenize/scratch/insert    97.65Mi ± 2%   98.88Mi ± 2%       ~ (p=0.654 n=20)
geomean                    90.07Mi        90.28Mi       +0.23%

                         │     base     │                head                 │
                         │     B/op     │    B/op     vs base                 │
Tokenize/scratch/join      0.000 ± 0%     0.000 ± 0%       ~ (p=1.000 n=20) ¹
Tokenize/scratch/complex   0.000 ± 0%     0.000 ± 0%       ~ (p=1.000 n=20) ¹
Tokenize/scratch/insert    0.000 ± 0%     0.000 ± 0%       ~ (p=1.000 n=20) ¹
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=20) ¹
Tokenize/scratch/complex   0.000 ± 0%     0.000 ± 0%       ~ (p=1.000 n=20) ¹
Tokenize/scratch/insert    0.000 ± 0%     0.000 ± 0%       ~ (p=1.000 n=20) ¹
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.546µ ± 4%   1.542µ ± 1%       ~ (p=0.588 n=20)
TransformParse/join            4.995µ ± 1%   5.027µ ± 1%       ~ (p=0.304 n=20)
TransformParse/complex         13.12µ ± 2%   13.09µ ± 1%       ~ (p=0.478 n=20)
geomean                        4.662µ        4.663µ       +0.03%

                             │     base     │                 head                  │
                             │     B/op     │     B/op      vs base                 │
TransformParse/simple_select     816.0 ± 0%     816.0 ± 0%       ~ (p=1.000 n=20) ¹
TransformParse/join            1.836Ki ± 0%   1.836Ki ± 0%       ~ (p=1.000 n=20) ¹
TransformParse/complex         5.023Ki ± 0%   5.023Ki ± 0%       ~ (p=1.000 n=20) ¹
geomean                        1.944Ki        1.944Ki       +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=20) ¹
TransformParse/join            17.00 ± 0%   17.00 ± 0%       ~ (p=1.000 n=20) ¹
TransformParse/complex         49.00 ± 0%   49.00 ± 0%       ~ (p=1.000 n=20) ¹
geomean                        16.09        16.09       +0.00%
¹ all samples are equal

Run, raw data and benchstat

@github-actions

github-actions Bot commented Nov 3, 2026 •

Copy link
Copy Markdown

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


Review of PR #936: lower-case keyword lookup table

Verdict: LGTM. I read the diff but did not run the tests or benchmarks. The numbers below come from the PR description.

Correctness

  • lowerKeyword matches strings.ToLower for every input.
    • It returns the input unchanged only when no byte is A-Z or ≥0x80, which is exactly when ToLower returns it unchanged.
    • A table hit returns a value precomputed with strings.ToLower.
    • Everything else falls back to strings.ToLower.
    • A missing table entry therefore costs an allocation and never changes output.
  • TestLowerKeywordTable checks every entry. TestLowerKeywordMatchesToLower covers non-ASCII input (İ, Σ, ß).

Performance

Minor suggestions (non-blocking)

  1. Duplicate entries in pkg/formatter/keyword_lower.go:132-142.
    • MATCHED, NOT MATCHED, NOT MATCHED BY SOURCE, NOT MATCHED BY TARGET, DELETE, DO NOTHING and ROWS already appear in the literal section above.
    • The map dedupes them, so it's harmless, but removing them would tidy the list.
  2. The helper section is not enforced. TestKwLiteralsAreInLowerKeywordTable only covers kw("literal"). Entries that arrive through ast helpers (join, set-op, lock and alter-action text) are hand-maintained, and nothing checks them. A miss costs only an allocation. If those helpers expose an enumerable set, a test over it would close the gap.
  3. AllocsPerRun under -race. TestKwLowerDoesNotAllocateForKnownKeywords could be flaky with the race detector on. Confirm the race job passes. If it flakes, skip the test under race, as TestPerformanceRegression does.
  4. Loose scanner match. The go/parser scan matches any method named kw that takes one string literal, not only nodeFormatter.kw. That is fine today, and the seen < 50 guard helps.
  5. Lookup order (micro-optimisation). For all-uppercase keywords the byte scan breaks on the first character, so it's cheap. If you ever wanted to skip it, a direct map lookup first would also be correct. This is not worth changing.

Security / API

No security concerns and no public API change. The CHANGELOG entry is fine.

@github-actions

github-actions Bot commented Nov 3, 2026 •

Copy link
Copy Markdown

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


Review of PR #936: lower-case keyword lookup table

Verdict: LGTM. I read the code but could not run the tests or benchmarks here. Shell commands that chain operations needed approval, so the test results come from the PR description and the CI perf-gate comment.

Correctness

  • lowerKeyword (pkg/formatter/keyword_lower.go:748) matches strings.ToLower.
    • Input with no A-Z byte and no byte ≥0x80 is returned unchanged, and ToLower would also leave it unchanged.
    • A table hit returns a value precomputed with strings.ToLower.
    • Anything else falls through to strings.ToLower.
    • A missing table entry therefore costs one allocation and never changes output.
  • buildLowerKeywordTable panics on a duplicate key, and TestBuildLowerKeywordTableRejectsDuplicates covers that. The duplicate-entry nit in earlier reviews no longer applies.
  • TestLowerKeywordCorpusMisses (keyword_lower_corpus_test.go) renders the whole bench corpus in lower-case mode and fails if any keyword reaches the fallback. This closes the gap on keywords that come from ast helpers instead of kw("literal") calls. Earlier reviews flagged that gap as unenforced.

Performance

  • Table hits skip the allocation. The -29% allocs/op is deterministic. The -8% time is local only, and the PR reports that it misses the -15% target.
  • The latest CI perf gate (head 33518fc, 20 randomized layouts) shows no regression. Geomean is -0.03%, and LintRule/L010 is -0.18%. That supports the layout-noise explanation for the earlier L010 flag.
  • The gated suite has no Render/lower row, so CI cannot confirm the target gain. The PR description already says so.

Minor, non-blocking

  1. AllocsPerRun under -race. TestKwLowerDoesNotAllocateForKnownKeywords (keyword_lower_test.go:60) asserts exactly zero allocations. The race detector can change allocation counts, so check that the race job passes. If it flakes, skip the test under -race. Other tests in this package, such as name_quote_test.go:127, use the same pattern.
  2. Test hook in a package variable. lowerKeywordFallback is a package-level var that is swapped for tests. It is not goroutine-safe, so TestLowerKeywordCorpusMisses must not run with t.Parallel() or alongside other formatting tests that run in parallel. It is fine as written. A one-line comment saying so would help.
  3. Table maintenance. New keyword spellings added to the renderers need a table entry. The two scan and corpus tests catch most misses. A miss only costs an allocation, so this is low risk.

Security / API

No security concerns, no public API change, and the CHANGELOG entry is accurate.

This branch was successfully deployed

1 active deployment
Preview — 5c3b58d2 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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant