Skip to content

perf(cbinding): pool JSON scratch, release ASTs and cache linters [W8-01] - #943

Open
ajitpratap0 wants to merge 4 commits into
mainfrom
perf/w8-01-cbinding-overhead
Open

ajitpratap0 wants to merge 4 commits into
mainfrom
perf/w8-01-cbinding-overhead

Conversation

@ajitpratap0

@ajitpratap0 ajitpratap0 commented Nov 3, 2026 •

Copy link
Copy Markdown
Owner

Summary

Removes per-call Go overhead from the C binding (CB-1 Go side). C ABI, JSON bytes and Go API are unchanged.

  • JSON is encoded into a pooled buffer + json.Encoder and copied once into C memory (C.CBytes); the Encode newline is replaced by the NUL terminator.
  • Parse/validate/format results are built in a pooled scratch (result structs + reused statement-type slice), so no boxing or slice alloc. The scratch is zeroed on Put and dropped when its buffer exceeds 64 KiB or its slice exceeds 1024.
  • ast.ReleaseAST on the parse, extract and metadata paths.
  • Extract exports use typed result structs instead of map[string]interface{} (same key order, error before error_code).
  • Linters cached per (max_line_length, dialect), capped at 64 entries (max_line_length is any positive int); stored linters are never mutated. Relies on the shared-linter invariant of gosqlx.defaultLinter, W6-05a (fix(linter): make Context.Nodes safe for concurrent rule Check [W6-05a] #928) and D16 (rules must not retain AST/token references after Check).

#566 objection ("some paths may retain node pointers"): checked per path. Parse results hold only string constants (statement type names); extract results hold string copies; metadata holds string copies built before release; nothing points into the AST after ReleaseAST.

Rejected (PLAN section 6): reading the caller's C string without copying (use-after-free risk) and a hand-written JSON encoder (would re-implement encoding/json escaping).

Test Plan

  • task check (clean on rerun; first run hit load-induced timeouts in unrelated packages, load average ~80 on the shared M4), task test:cbinding, go test -count=1 ./pkg/compatibility/..., staticcheck ./pkg/cbinding/: pass.
  • New tests: corpus-wide byte equality for every export against reference builders plus edge inputs (invalid UTF-8, U+2028/9, <&>, unknown dialect, >200 KB), no state leak across success/error/zero-statement calls, cached-vs-fresh linter for all dialects x maxLen, cache bound (counter == entries <= 64), 16-goroutine concurrent equivalence under -race, keepScratch boundaries, and !race AllocsPerRun guards (parse -5, lint -13 on medium). Six mutations (no newline trim, no zeroing, dialect-only key, no cap, no ReleaseAST on parse, metadata and extract paths) each fail a new test: the equivalence, cache and scratch mutations fail under -race (task test:cbinding); the alloc and ReleaseAST mutations fail under the non-race step now run by task test:cbinding (go test -count=1 -timeout 60s -run 'TestCBindingAlloc' ./pkg/cbinding/, so CI and release.yml enforce them; extract guard renamed TestCBindingAllocExtractASTRelease to match) (metadata/extract release guards added in the fix round via no-release reference helpers). The nil-initial-stmtTypes mutation survives by design: a nil dst falls back to a non-nil allocation, so the zero-statement '[]' is preserved.
  • The equivalence tests run in parallel subtests so the package stays under the 60s test:cbinding timeout (timeout not changed).
  • ab.sh forces CGO off, so cbinding was A/B'd with a hand-run interleaved ABBA, 10 rounds, GOGC=off GOMEMLIMIT=64MiB GOMAXPROCS=1, base = origin/main @ 6399dc0 with only the new bench file copied in; benchstat -alpha 0.01.
BenchmarkCBinding allocs/op base -> head B/op
parse small/medium/large 17->11, 50->44, 534->528 -34.8%, -13.9%, -1.6%
validate 14->11, 47->44, 531->528 -11.5%, -3.7%, -0.4%
format 20->16, 74->70, 540->536 -17.6%, -12.1%, -5.5%
lint 56->42, 176->162, 1216->1202 -18.5%, -6.2%, -1.0%
extract_metadata 29->26, 78->75, 704->701 -24.0%, -16.4%, -1.4%
extract_tables 25->17, 70->62, 700->692 -34.7%, -12.5%, -1.4%
parse_dialect 18->12, 51->45, 535->529 -34.5%, -13.9%, -1.6%

allocs/op and B/op are deterministic (p=0.000, n=10 every row). Target (parse -4 allocs/op) met: -6 at all sizes; lint -14. ns/op: geomean -13.5% but every row is ~ (load average 80-118 on the shared machine, CIs 50-500%), so ns/op is indicative only.

  • CI perf gate (layout-randomized, base a72ec74, 20 -randlayout layouts per arm, 2026-11-03T18:06Z): verdict PASS, no gated metric moved past its threshold; all ParseE2E/ParseRW rows ok with allocs unchanged. pkg/cbinding is not gated because ab.sh line 162 sets CGO_ENABLED=0, so the evidence for it is the deterministic allocs/op and B/op from the manual CGO ABBA A/B above (parse -6 at all sizes vs target -4).
  • Fix round 1: putScratch zeroes result structs; sc.buf may retain the last encoded JSON (up to 64 KiB) until the next Reset, which is accepted and now documented in a comment. The cachedLinter lost-LoadOrStore decrement is correct but rarely exercised by tests.

Breaking Changes

None. C ABI, JSON bytes and Go API unchanged.

🤖 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.

1 Skipped Deployment
Project Deployment Actions Updated
gosqlx-website Ignored Ignored Preview Nov 3, 2026 7:33pm UTC

@github-actions

github-actions Bot commented Nov 3, 2026 •

Copy link
Copy Markdown

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


Review of PR #943: cbinding overhead

I read the main changes (cbinding_json.go, cbinding_ext.go). I didn't run the tests or benchmarks. This is a code read plus the PR description's numbers. I found no blocking issues.

What looks correct

  • encode: the trailing \n is overwritten with the NUL, and C.CBytes copies before the deferred putScratch runs. There is no use-after-free. Encode always ends with a newline, so b[len(b)-1] is safe, and the error path returns before it.
  • Scratch pooling: putScratch zeroes the result structs and truncates the slice. Oversized scratches (buffer over 64 KiB or slice over 1024) are dropped, so one large query can't pin memory. The getScratch fallback is harmless.
  • parseJSON: sc.stmtTypes is only reassigned on success, so any grown backing array is retained. The slice holds only constant strings, so reusing it can't leak data between calls.
  • ast.ReleaseAST: StmtTypes and the extract results contain only strings, as the PR says. Nothing references the AST after release.
  • Linter cache: the Add/LoadOrStore bookkeeping is balanced on every path, so the counter stays equal to the number of entries. Tests cover that.

Minor suggestions (non-blocking)

  1. No eviction in the linter cache (cbinding_ext.go:~517). The first 64 (maxLen, dialect) keys stay cached forever, and every later key rebuilds a linter on each call. A host that passes many distinct max_line_length values early would permanently lose the benefit for common values. Reasonable alternatives: skip caching for non-default lengths, or document this as intentional.
  2. Safety depends on rules never mutating state. Sharing linters across goroutines relies on the D16 and W6-05a invariants. Please make sure a regression test in pkg/linter fails if a rule gains per-call state. The concurrent -race test here covers only the current rules.
  3. pkg/cbinding isn't covered by the perf gate. The A/B numbers came from a hand-run on a heavily loaded machine, and every ns/op row was ~. The deterministic allocs/op and B/op deltas and the AllocsPerRun guards are good evidence. The PR title and CHANGELOG entry should claim reduced allocations and not speed.
  4. Test-only helpers in a non-test file. cbinding_testhelpers.go has no _test suffix, so it ships in the binary. This matches the existing pattern in the package if the file already did this before. If it didn't, consider moving the new additions into a _test.go file. cgo restrictions on import "C" in tests may be the reason it's done this way.

Test coverage: thorough. It includes corpus byte-equality, edge inputs (invalid UTF-8, U+2028/9, <&>, inputs over 200 KB), state-leak checks, cache bounds, concurrency, and the mutation checks described in the PR.

Overall this looks good to merge once CI is green.

@github-actions github-actions Bot added documentation Improvements or additions to documentation performance python labels Nov 3, 2026
@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 ec166ada2fae. 20 interleaved rounds, 20 randomized code layouts per arm (-randlayout seeds 417042318902..417042318921; A/A' 417042318922..417042318941), 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.13% 0.776 -0.03% 4.45% +0.00% 5 → 5 ok
ParseE2E/select_where -0.15% 0.695 -0.35% 1.25% +0.00% 18 → 18 ok
ParseE2E/join +0.32% 0.330 +0.40% 0.82% +0.00% 17 → 17 ok
ParseE2E/aggregate +0.42% 0.655 +0.59% 4.39% +0.00% 18 → 18 ok
ParseE2E/subquery -0.20% 0.745 +0.56% 2.22% +0.00% 14 → 14 ok
ParseE2E/cte -0.22% 0.646 -0.42% 1.88% +0.00% 21 → 21 ok
ParseE2E/window -0.02% 0.989 +0.00% 1.78% +0.00% 11 → 11 ok
ParseE2E/complex -0.34% 0.636 -0.32% 2.62% +0.00% 49 → 49 ok
ParseE2E/insert +0.24% 0.675 -0.09% 1.23% +0.00% 17 → 17 ok
ParseE2E/update +0.40% 0.358 -0.05% 1.27% +0.00% 13 → 13 ok
ParseE2E/delete +0.08% 0.829 -0.59% 1.37% +0.00% 5 → 5 ok
ParseRW/postgresql/medium -1.18% 0.201 -1.01% 3.32% -0.00% 646 → 646 ok
ParseRW/mysql/medium +0.02% 0.989 -0.15% 0.92% +0.00% 368 → 368 ok
ParseRW/mariadb/medium +0.12% 0.758 -0.21% 0.58% +0.00% 528 → 528 ok
ParseRW/sqlserver/medium -0.07% 0.883 -0.46% 1.23% -0.00% 927 → 927 ok
ParseRW/oracle/medium +0.06% 0.841 +0.23% 0.64% +0.00% 1090 → 1090 ok
ParseRW/sqlite/medium -0.80% 0.165 -0.10% 1.41% +0.00% 347 → 347 ok
ParseRW/snowflake/medium +0.40% 0.383 +0.47% 0.95% +0.00% 776 → 776 ok
ParseRW/clickhouse/medium -0.92% 0.277 -1.26% 2.64% +0.00% 383 → 383 ok
ParseFresh/simple_select -0.16% 0.776 +0.02% 2.00% +0.00% 6 → 6 ok
ParseFresh/join -0.19% 0.579 -0.09% 0.69% +0.00% 18 → 18 ok
ParseFresh/complex +0.03% 0.935 +0.28% 1.27% +0.00% 50 → 50 ok
PoolGetPut/serial +0.04% 0.946 +0.04% 0.93% +0.00% 0 → 0 ok
Tokenize/scratch/join -0.03% 0.989 +0.17% 2.30% +0.00% 0 → 0 ok
Tokenize/scratch/complex -0.13% 0.839 -0.28% 3.19% +0.00% 0 → 0 ok
Tokenize/scratch/insert -0.29% 0.776 +0.36% 2.77% +0.00% 0 → 0 ok
Normalize/simple_select -0.13% 0.675 +0.13% 0.88% +0.00% 7 → 7 ok
Normalize/join -0.21% 0.598 +0.37% 1.11% +0.00% 23 → 23 ok
Normalize/complex -0.07% 0.925 +0.37% 1.99% +0.00% 68 → 68 ok
TransformParse/simple_select +0.03% 0.946 +0.60% 1.37% +0.00% 5 → 5 ok
TransformParse/join +0.33% 0.402 +0.25% 0.99% +0.00% 17 → 17 ok
TransformParse/complex -0.18% 0.829 +0.33% 2.57% +0.00% 49 → 49 ok
LintParallel/rules +0.15% 0.429 -0.31% 0.60% +0.00% 229685.5 → 229686 ok
LintParallel/lint -0.14% 0.758 -0.18% 0.97% +0.00% 308265 → 308266 ok
LintRW/postgresql +0.19% 0.678 +0.27% 2.81% +0.00% 34189 → 34189 ok
LintRW/mysql +0.74% 0.127 -0.03% 1.06% +0.00% 41489 → 41490 ok
LintRW/mariadb +0.08% 0.862 +0.28% 0.90% -0.00% 38235 → 38235 ok
LintRW/sqlserver +0.10% 0.883 -0.45% 1.32% -0.00% 34620 → 34620 ok
LintRW/oracle +0.11% 0.779 -0.41% 1.65% -0.00% 42243 → 42243 ok
LintRW/sqlite -0.31% 0.659 +0.47% 3.60% +0.00% 37576 → 37576 ok
LintRW/snowflake +0.31% 0.602 -0.21% 1.27% -0.00% 37559 → 37559 ok
LintRW/clickhouse +1.07% 0.174 +0.51% 3.22% +0.00% 42343 → 42343 ok
LintRule/L007 +0.25% 0.620 +0.26% 1.73% +0.00% 72565 → 72565 ok
LintRule/L010 +0.05% 0.779 +0.11% 17.73% +0.00% 0 → 0 ok
ParseError/syntax_mid/parse +0.19% 0.685 -0.30% 1.29% +0.00% 12 → 12 ok
ParseError/syntax_mid/validate +0.24% 0.449 -0.66% 0.81% +0.00% 12 → 12 ok
ParseError/syntax_end/parse +0.15% 0.685 -0.50% 1.47% +0.00% 16 → 16 ok
ParseError/syntax_end/validate +0.75% 0.172 +0.14% 1.39% +0.00% 16 → 16 ok
ParseError/tokenize/parse +0.59% 0.409 -1.35% 1.96% +0.00% 4 → 4 ok
ParseError/tokenize/validate +0.48% 0.409 -0.44% 1.39% +0.00% 21 → 21 ok
ParseError/long_prefix/parse +0.31% 0.583 +0.25% 2.79% +0.00% 219 → 219 ok
ParseError/long_prefix/validate +0.42% 0.678 +0.69% 3.31% +0.00% 219 → 219 ok
geomean +0.06% 0.602 -0.04%

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.04%, p 0.841.

Low power

6 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 17.73% 0% +30.0%
ParseE2E/simple_select 4.45% 37% +6.8%
ParseE2E/aggregate 4.39% 39% +6.7%
LintRW/sqlite 3.60% 68% +5.5%
ParseRW/postgresql/medium 3.32% 79% +5.0%
ParseError/long_prefix/validate 3.31% 80% +5.0%

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.02% ok
e2e/complex +0.02% ok
e2e/cte +0.00% ok
e2e/delete +0.10% ok
e2e/insert +0.03% ok
e2e/join +0.01% ok
e2e/select_where +0.03% ok
e2e/simple_select +0.03% ok
e2e/subquery -0.01% ok
e2e/update +0.05% ok
e2e/window -0.02% ok
rw/postgresql/medium +0.05% ok
Secondary metric: default GC, GOMAXPROCS=1 (informational)
benchmark time p
ParseE2E/simple_select +0.23% 0.570
ParseE2E/select_where -0.12% 0.623
ParseE2E/join +0.35% 0.796
ParseE2E/aggregate +0.94% 0.162
ParseE2E/subquery +0.15% 0.496
ParseE2E/cte +0.32% 0.364
ParseE2E/window -0.12% 0.739
ParseE2E/complex +0.18% 0.684
ParseE2E/insert +0.40% 0.481
ParseE2E/update -0.25% 0.677
ParseE2E/delete -0.06% 0.940
ParseRW/postgresql/medium +0.15% 0.971
ParseRW/mysql/medium -0.24% 0.796
ParseRW/mariadb/medium -0.13% 0.796
ParseRW/sqlserver/medium +0.58% 0.143
ParseRW/oracle/medium +0.06% 0.912
ParseRW/sqlite/medium +0.23% 0.739
ParseRW/snowflake/medium +0.74% 0.436
ParseRW/clickhouse/medium +1.03% 0.353
ParseFresh/simple_select -0.20% 0.912
ParseFresh/join -0.54% 0.315
ParseFresh/complex +0.03% 0.971
PoolGetPut/serial -0.03% 0.912
Tokenize/scratch/join +0.07% 0.971
Tokenize/scratch/complex -0.23% 0.791
Tokenize/scratch/insert -0.03% 0.971
Normalize/simple_select -0.12% 0.940
Normalize/join -1.20% 0.043
Normalize/complex -0.53% 0.247
TransformParse/simple_select +0.29% 0.623
TransformParse/join +0.46% 0.684
TransformParse/complex +0.32% 0.631
LintParallel/rules +0.50% 0.393
LintParallel/lint -0.46% 0.190
LintRW/postgresql -0.47% 0.436
LintRW/mysql +0.14% 0.579
LintRW/mariadb -0.14% 0.684
LintRW/sqlserver -0.27% 0.631
LintRW/oracle +0.01% 0.971
LintRW/sqlite +0.07% 0.739
LintRW/snowflake -0.17% 0.796
LintRW/clickhouse -0.05% 0.971
LintRule/L007 -0.21% 0.796
LintRule/L010 +0.02% 1.000
ParseError/syntax_mid/parse -0.50% 0.272
ParseError/syntax_mid/validate -1.26% 0.088
ParseError/syntax_end/parse -0.08% 0.912
ParseError/syntax_end/validate -0.03% 0.880
ParseError/tokenize/parse -0.13% 0.971
ParseError/tokenize/validate -0.17% 0.677
ParseError/long_prefix/parse +0.15% 0.853
ParseError/long_prefix/validate +1.08% 0.393
geomean +0.01%
Environment
date:        2026-11-03T19:38:17Z
base:        a72ec74daf9a3bd7ca53e7642ff407795df5525d
head:        working tree of ec166ada2fae5116a9317ce41230719aba1d205f
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 417042318902..417042318921 (base, head), 417042318922..417042318941 (A/A')
pinning:     pinned to CPU 3 (SMT siblings 2-3) with taskset
gcoff env:   GOGC=off GOMEMLIMIT=64MiB
clocksource: tsc
load:        8.87, 3.03, 1.08
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.273µ ± 1%   2.274µ ± 1%       ~ (p=0.673 n=20)
Normalize/join            6.999µ ± 1%   6.996µ ± 1%       ~ (p=0.597 n=20)
Normalize/complex         19.38µ ± 1%   19.36µ ± 1%       ~ (p=0.925 n=20)
geomean                   6.755µ        6.754µ       -0.02%

                        │     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.565µ ± 4%   1.561µ ± 3%       ~ (p=0.773 n=20)
ParseE2E/select_where             3.438µ ± 1%   3.436µ ± 1%       ~ (p=0.693 n=20)
ParseE2E/join                     4.621µ ± 1%   4.641µ ± 1%       ~ (p=0.330 n=20)
ParseE2E/aggregate                3.432µ ± 3%   3.479µ ± 2%       ~ (p=0.654 n=20)
ParseE2E/subquery                 3.010µ ± 1%   2.999µ ± 1%       ~ (p=0.743 n=20)
ParseE2E/cte                      4.302µ ± 1%   4.293µ ± 1%       ~ (p=0.644 n=20)
ParseE2E/window                   2.829µ ± 1%   2.821µ ± 1%       ~ (p=0.984 n=20)
ParseE2E/complex                  12.08µ ± 2%   12.00µ ± 1%       ~ (p=0.634 n=20)
ParseE2E/insert                   2.107µ ± 1%   2.114µ ± 2%       ~ (p=0.673 n=20)
ParseE2E/update                   1.868µ ± 1%   1.877µ ± 2%       ~ (p=0.358 n=20)
ParseE2E/delete                   1.193µ ± 1%   1.192µ ± 1%       ~ (p=0.825 n=20)
ParseRW/postgresql/medium         165.7µ ± 2%   164.7µ ± 6%       ~ (p=0.201 n=20)
ParseRW/mysql/medium              95.54µ ± 1%   95.59µ ± 1%       ~ (p=0.984 n=20)
ParseRW/mariadb/medium            136.3µ ± 0%   136.2µ ± 1%       ~ (p=0.758 n=20)
ParseRW/sqlserver/medium          269.6µ ± 1%   268.9µ ± 1%       ~ (p=0.883 n=20)
ParseRW/oracle/medium             318.6µ ± 0%   319.0µ ± 1%       ~ (p=0.841 n=20)
ParseRW/sqlite/medium             72.28µ ± 1%   71.81µ ± 1%       ~ (p=0.165 n=20)
ParseRW/snowflake/medium          187.5µ ± 1%   188.5µ ± 1%       ~ (p=0.383 n=20)
ParseRW/clickhouse/medium         90.69µ ± 2%   89.87µ ± 3%       ~ (p=0.277 n=20)
ParseFresh/simple_select          7.032µ ± 1%   6.983µ ± 1%       ~ (p=0.773 n=20)
ParseFresh/join                   11.27µ ± 0%   11.25µ ± 1%       ~ (p=0.578 n=20)
ParseFresh/complex                20.12µ ± 1%   20.15µ ± 1%       ~ (p=0.931 n=20)
PoolGetPut/serial                 67.19n ± 1%   67.17n ± 1%       ~ (p=0.941 n=20)
ParseError/syntax_mid/parse       2.357µ ± 1%   2.354µ ± 1%       ~ (p=0.683 n=20)
ParseError/syntax_mid/validate    2.110µ ± 1%   2.116µ ± 1%       ~ (p=0.449 n=20)
ParseError/syntax_end/parse       2.614µ ± 1%   2.617µ ± 1%       ~ (p=0.683 n=20)
ParseError/syntax_end/validate    2.564µ ± 1%   2.582µ ± 1%       ~ (p=0.172 n=20)
ParseError/tokenize/parse         902.0n ± 1%   908.8n ± 1%       ~ (p=0.410 n=20)
ParseError/tokenize/validate      2.712µ ± 1%   2.727µ ± 1%       ~ (p=0.410 n=20)
ParseError/long_prefix/parse      60.16µ ± 2%   60.48µ ± 2%       ~ (p=0.583 n=20)
ParseError/long_prefix/validate   56.82µ ± 3%   56.95µ ± 2%       ~ (p=0.678 n=20)
geomean                           9.340µ        9.343µ       +0.04%

                                │     base     │                head                 │
                                │     B/s      │     B/s       vs base               │
ParseE2E/simple_select            15.85Mi ± 4%   15.88Mi ± 3%       ~ (p=0.763 n=20)
ParseE2E/select_where             28.58Mi ± 1%   28.59Mi ± 1%       ~ (p=0.683 n=20)
ParseE2E/join                     26.00Mi ± 1%   25.89Mi ± 1%       ~ (p=0.304 n=20)
ParseE2E/aggregate                23.35Mi ± 3%   23.03Mi ± 2%       ~ (p=0.654 n=20)
ParseE2E/subquery                 25.99Mi ± 1%   26.08Mi ± 1%       ~ (p=0.763 n=20)
ParseE2E/cte                      27.49Mi ± 1%   27.55Mi ± 1%       ~ (p=0.616 n=20)
ParseE2E/window                   31.69Mi ± 1%   31.78Mi ± 1%       ~ (p=0.973 n=20)
ParseE2E/complex                  29.85Mi ± 2%   30.05Mi ± 1%       ~ (p=0.625 n=20)
ParseE2E/insert                   34.40Mi ± 1%   34.29Mi ± 2%       ~ (p=0.683 n=20)
ParseE2E/update                   28.09Mi ± 1%   27.94Mi ± 2%       ~ (p=0.358 n=20)
ParseE2E/delete                   41.59Mi ± 1%   41.59Mi ± 1%       ~ (p=0.794 n=20)
ParseRW/postgresql/medium         21.65Mi ± 2%   21.79Mi ± 6%       ~ (p=0.199 n=20)
ParseRW/mysql/medium              28.33Mi ± 1%   28.31Mi ± 1%       ~ (p=0.995 n=20)
ParseRW/mariadb/medium            26.02Mi ± 0%   26.03Mi ± 1%       ~ (p=0.753 n=20)
ParseRW/sqlserver/medium          24.68Mi ± 1%   24.75Mi ± 1%       ~ (p=0.888 n=20)
ParseRW/oracle/medium             26.31Mi ± 0%   26.28Mi ± 1%       ~ (p=0.888 n=20)
ParseRW/sqlite/medium             28.58Mi ± 1%   28.77Mi ± 1%       ~ (p=0.155 n=20)
ParseRW/snowflake/medium          27.12Mi ± 1%   26.97Mi ± 1%       ~ (p=0.380 n=20)
ParseRW/clickhouse/medium         24.85Mi ± 2%   25.08Mi ± 3%       ~ (p=0.274 n=20)
ParseError/syntax_mid/parse       19.42Mi ± 1%   19.45Mi ± 1%       ~ (p=0.673 n=20)
ParseError/syntax_mid/validate    21.69Mi ± 1%   21.64Mi ± 1%       ~ (p=0.441 n=20)
ParseError/syntax_end/parse       14.23Mi ± 1%   14.21Mi ± 1%       ~ (p=0.713 n=20)
ParseError/syntax_end/validate    14.51Mi ± 1%   14.41Mi ± 1%       ~ (p=0.176 n=20)
ParseError/tokenize/parse         48.64Mi ± 1%   48.27Mi ± 1%       ~ (p=0.410 n=20)
ParseError/tokenize/validate      16.18Mi ± 1%   16.09Mi ± 1%       ~ (p=0.425 n=20)
ParseError/long_prefix/parse      38.40Mi ± 2%   38.19Mi ± 2%       ~ (p=0.569 n=20)
ParseError/long_prefix/validate   40.66Mi ± 3%   40.56Mi ± 3%       ~ (p=0.664 n=20)
geomean                           25.99Mi        25.97Mi       -0.08%

                                │      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.240 n=20)
ParseRW/mysql/medium              42.37Ki ± 0%     42.37Ki ± 0%       ~ (p=0.342 n=20)
ParseRW/mariadb/medium            60.61Ki ± 0%     60.61Ki ± 0%       ~ (p=0.403 n=20)
ParseRW/sqlserver/medium          83.68Ki ± 0%     83.68Ki ± 0%       ~ (p=0.856 n=20)
ParseRW/oracle/medium             104.8Ki ± 0%     104.8Ki ± 0%       ~ (p=0.836 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.487 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=0.832 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   13.81µ ± 2%   13.72µ ± 6%       ~ (p=0.201 n=20)
ParseRW/mysql/medium        11.94µ ± 1%   11.95µ ± 1%       ~ (p=0.995 n=20)
ParseRW/mariadb/medium      15.14µ ± 0%   15.13µ ± 1%       ~ (p=0.763 n=20)
ParseRW/sqlserver/medium    12.84µ ± 1%   12.81µ ± 1%       ~ (p=0.878 n=20)
ParseRW/oracle/medium       13.28µ ± 0%   13.29µ ± 1%       ~ (p=0.857 n=20)
ParseRW/sqlite/medium       12.05µ ± 1%   11.97µ ± 1%       ~ (p=0.165 n=20)
ParseRW/snowflake/medium    14.42µ ± 1%   14.50µ ± 1%       ~ (p=0.387 n=20)
ParseRW/clickhouse/medium   12.96µ ± 2%   12.84µ ± 3%       ~ (p=0.277 n=20)
geomean                     13.26µ        13.23µ       -0.22%

pkg: github.com/ajitpratap0/GoSQLX/pkg/linter
                   │     base     │                head                 │
                   │    sec/op    │    sec/op     vs base               │
LintParallel/rules   34.59m ±  0%   34.64m ±  1%       ~ (p=0.429 n=20)
LintParallel/lint    54.18m ±  1%   54.19m ±  2%       ~ (p=0.758 n=20)
LintRW/postgresql    7.047m ±  2%   7.068m ±  2%       ~ (p=0.678 n=20)
LintRW/mysql         7.303m ±  1%   7.389m ±  1%       ~ (p=0.127 n=20)
LintRW/mariadb       6.728m ±  1%   6.726m ±  4%       ~ (p=0.862 n=20)
LintRW/sqlserver     6.360m ±  1%   6.374m ±  1%       ~ (p=0.883 n=20)
LintRW/oracle        7.602m ±  1%   7.631m ±  1%       ~ (p=0.779 n=20)
LintRW/sqlite        6.291m ±  2%   6.212m ±  3%       ~ (p=0.659 n=20)
LintRW/snowflake     6.615m ±  1%   6.622m ±  1%       ~ (p=0.602 n=20)
LintRW/clickhouse    7.107m ±  3%   7.251m ±  3%       ~ (p=0.174 n=20)
LintRule/L007        7.694m ±  2%   7.708m ±  3%       ~ (p=0.620 n=20)
LintRule/L010        793.4µ ± 12%   797.3µ ± 12%       ~ (p=0.779 n=20)
geomean              7.872m         7.897m        +0.31%

                  │     base     │                head                 │
                  │     B/s      │     B/s       vs base               │
LintRW/postgresql   4.020Mi ± 2%   4.005Mi ± 2%       ~ (p=0.615 n=20)
LintRW/mysql        4.478Mi ± 1%   4.425Mi ± 1%       ~ (p=0.154 n=20)
LintRW/mariadb      4.535Mi ± 1%   4.535Mi ± 4%       ~ (p=0.899 n=20)
LintRW/sqlserver    4.869Mi ± 1%   4.859Mi ± 1%       ~ (p=0.846 n=20)
LintRW/oracle       5.226Mi ± 1%   5.207Mi ± 1%       ~ (p=0.888 n=20)
LintRW/sqlite       4.563Mi ± 2%   4.621Mi ± 3%       ~ (p=0.654 n=20)
LintRW/snowflake    4.716Mi ± 1%   4.706Mi ± 1%       ~ (p=0.559 n=20)
LintRW/clickhouse   4.568Mi ± 3%   4.473Mi ± 3%       ~ (p=0.136 n=20)
geomean             4.610Mi        4.592Mi       -0.39%

                   │      base      │                 head                  │
                   │      B/op      │     B/op      vs base                 │
LintParallel/rules   10.96Mi ± 0%     10.96Mi ± 0%       ~ (p=0.304 n=20)
LintParallel/lint    30.93Mi ± 0%     30.93Mi ± 0%       ~ (p=0.654 n=20)
LintRW/postgresql    3.402Mi ± 0%     3.402Mi ± 0%       ~ (p=0.723 n=20)
LintRW/mysql         4.243Mi ± 0%     4.243Mi ± 0%       ~ (p=0.075 n=20)
LintRW/mariadb       3.924Mi ± 0%     3.924Mi ± 0%       ~ (p=0.499 n=20)
LintRW/sqlserver     3.488Mi ± 0%     3.488Mi ± 0%       ~ (p=0.433 n=20)
LintRW/oracle        4.242Mi ± 0%     4.242Mi ± 0%       ~ (p=0.857 n=20)
LintRW/sqlite        3.697Mi ± 0%     3.697Mi ± 0%       ~ (p=0.351 n=20)
LintRW/snowflake     3.745Mi ± 0%     3.745Mi ± 0%       ~ (p=0.262 n=20)
LintRW/clickhouse    4.190Mi ± 0%     4.190Mi ± 0%       ~ (p=0.693 n=20)
LintRule/L007        2.942Mi ± 0%     2.942Mi ± 0%       ~ (p=0.695 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.282 n=20)
LintParallel/lint    308.3k ± 0%     308.3k ± 0%       ~ (p=0.115 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.048 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%       ~ (p=1.000 n=20)
LintRW/sqlite        37.58k ± 0%     37.58k ± 0%       ~ (p=1.000 n=20) ¹
LintRW/snowflake     37.56k ± 0%     37.56k ± 0%       ~ (p=1.000 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   23.11µ ±  2%   23.17µ ±  2%       ~ (p=0.678 n=20)
LintRW/mysql        17.64µ ±  1%   17.85µ ±  1%       ~ (p=0.126 n=20)
LintRW/mariadb      17.70µ ±  1%   17.70µ ±  4%       ~ (p=0.862 n=20)
LintRW/sqlserver    22.96µ ±  1%   23.01µ ±  1%       ~ (p=0.878 n=20)
LintRW/oracle       20.66µ ±  1%   20.74µ ±  1%       ~ (p=0.779 n=20)
LintRW/sqlite       18.08µ ±  2%   17.85µ ±  3%       ~ (p=0.659 n=20)
LintRW/snowflake    20.23µ ±  1%   20.25µ ±  1%       ~ (p=0.588 n=20)
LintRW/clickhouse   18.04µ ±  3%   18.40µ ±  3%       ~ (p=0.167 n=20)
LintRule/L007       2.735µ ±  2%   2.740µ ±  3%       ~ (p=0.578 n=20)
LintRule/L010       282.1n ± 12%   283.4n ± 12%       ~ (p=0.794 n=20)
geomean             10.57µ         10.61µ        +0.35%

pkg: github.com/ajitpratap0/GoSQLX/pkg/sql/tokenizer
                         │    base     │                head                │
                         │   sec/op    │   sec/op     vs base               │
Tokenize/scratch/join      1.480µ ± 2%   1.481µ ± 2%       ~ (p=0.984 n=20)
Tokenize/scratch/complex   3.906µ ± 2%   3.913µ ± 2%       ~ (p=0.836 n=20)
Tokenize/scratch/insert    737.5n ± 3%   734.0n ± 2%       ~ (p=0.773 n=20)
geomean                    1.621µ        1.620µ       -0.06%

                         │     base     │                head                 │
                         │     B/s      │     B/s       vs base               │
Tokenize/scratch/join      81.22Mi ± 2%   81.13Mi ± 2%       ~ (p=0.973 n=20)
Tokenize/scratch/complex   92.32Mi ± 2%   92.14Mi ± 2%       ~ (p=0.847 n=20)
Tokenize/scratch/insert    98.27Mi ± 2%   98.74Mi ± 2%       ~ (p=0.773 n=20)
geomean                    90.32Mi        90.37Mi       +0.06%

                         │     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.511µ ± 1%   1.513µ ± 2%       ~ (p=0.941 n=20)
TransformParse/join            4.867µ ± 1%   4.913µ ± 1%       ~ (p=0.402 n=20)
TransformParse/complex         13.07µ ± 2%   13.06µ ± 2%       ~ (p=0.825 n=20)
geomean                        4.580µ        4.595µ       +0.32%

                             │     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 19s —— View job


Review of PR #943: cbinding overhead

I read cbinding_json.go and the linter cache in cbinding_ext.go, and grepped the ReleaseAST and putScratch call sites. I didn't run tests or benchmarks. git diff origin/main...HEAD failed with "no merge base" because the checkout is shallow, so I didn't review the full diff. I found no blocking issues.

Correct

  • encode: the trailing newline is replaced by the NUL and C.CBytes copies before the deferred putScratch runs. Encode always appends \n, so b[len(b)-1] is safe, and the error path returns before that line.
  • Scratch pooling: every export defers putScratch. It zeroes the result structs and truncates stmtTypes. Oversized scratches are dropped.
  • ReleaseAST: it is deferred at cbinding_ext.go:115, 246 and 271. The results hold only strings, so nothing refers to the AST afterwards.
  • Linter cache counter: the Add/LoadOrStore bookkeeping balances on every path. The counter can briefly exceed 64 under contention, but it self-corrects, and the cap is only a retention bound.
  • Perf gate: it passes. Allocs/op are unchanged on the gated suite, as expected, since pkg/cbinding isn't in it.

Non-blocking suggestions

  1. Pooled buffer keeps SQL text (cbinding_json.go:60). The comment already notes that a pooled buffer, up to 64 KiB, can hold encoded SQL until it is reused. For hosts that process sensitive queries, consider clear(sc.buf.AvailableBuffer()) or similar, or drop the buffer when it holds more than a small size. This is a minor concern.
  2. No eviction in the linter cache (cbinding_ext.go:517). The first 64 (maxLen, dialect) keys are cached permanently. Later keys rebuild a linter on every call, which is correct but loses the speedup. This is fine if documented as intentional.
  3. Linter sharing relies on stateless rules. The shared linters are safe only while rules stay stateless. Make sure pkg/linter has a test that fails if a rule gains per-call state. The concurrent -race test here only covers the current rules.
  4. Claims. Claim reduced allocations in the title and CHANGELOG, not speed. Every ns/op row in the hand-run A/B was ~, and the perf gate doesn't cover this package.
  5. Non-test helpers. cbinding_testhelpers.go has no _test suffix, so it ships in the binary. That is probably forced by the cgo restriction on import "C" in tests, but confirm it was already the pattern.
  6. Unreachable fallback. The getScratch type-assertion fallback can't be reached, since the pool only stores *exportScratch. It is harmless.

Tests: thorough. They cover corpus byte equality, edge inputs, state-leak checks, cache bounds, concurrency, keepScratch boundaries and the AllocsPerRun guards.

Overall this looks good to merge once CI is green.

…nding [W8-01]

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

Copy link
Copy Markdown

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


Review of PR #943: cbinding overhead

I read pkg/cbinding/cbinding_json.go again and relied on the PR description and the earlier review passes for the rest. I didn't run tests or benchmarks. I found no blocking issues.

Correct

  • encode: Encoder.Encode always ends with \n, so b[len(b)-1] = 0 is safe, and the error path returns first. C.CBytes copies before the caller's deferred putScratch, so there is no use-after-free.
  • HTML escaping: Encoder escapes HTML by default, like json.Marshal, so the output bytes match the old output. The corpus byte-equality tests cover this.
  • Partial output on error: a failed Encode can leave a partial write in buf. getScratch calls Reset before every use, so nothing leaks into the next call.
  • Pooling: putScratch zeroes the result structs and truncates stmtTypes. It drops scratches whose buffer or slice grew too large. The slice holds only constant strings, so reusing it can't carry data between calls.
  • ReleaseAST: the results contain only string copies, so nothing refers to the AST after release.

Non-blocking suggestions

  1. Linter cache has no eviction. The first 64 (maxLen, dialect) keys stay cached forever, and later keys rebuild a linter on every call. That is correct but loses the speedup. Either document it as intentional or skip caching for unusual max_line_length values.
  2. Sharing linters depends on stateless rules. Add or confirm a pkg/linter test that fails if a rule gains per-call state. The concurrent -race test here covers only the current rules.
  3. The pooled buffer can keep SQL text. It can hold up to 64 KiB of encoded SQL until the next Reset. You documented this, so it is acceptable. For hosts that process sensitive SQL, you could lower the retention cap or clear the buffer in putScratch.
  4. Claim reduced allocations, not speed. Every ns/op row in the hand-run A/B was ~, and the perf gate doesn't cover pkg/cbinding because ab.sh forces CGO off. The allocs/op and B/op deltas and the AllocsPerRun guards are the real evidence. Keep the CHANGELOG entry worded that way.
  5. The getScratch fallback can't be reached. The pool only stores *exportScratch, so the type-assertion fallback is harmless dead code.
  6. cbinding_testhelpers.go ships in the binary. It has no _test suffix. Confirm this is the existing pattern, probably forced by cgo restrictions on import "C" in tests.

Perf gate: the gate reports PASS on the gated suite. Allocs/op are unchanged there, as expected.

Tests: thorough. They cover corpus byte equality, edge inputs, state-leak checks, cache bounds, concurrency, the keepScratch boundaries, and the allocation guards.

This looks good to merge once CI is green.

@github-actions github-actions Bot added the ci-cd label Nov 3, 2026

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ci-cd documentation Improvements or additions to documentation performance python

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant