Skip to content

perf(gosqlx): build wrapped error messages lazily [W1-03] - #905

Merged
ajitpratap0 merged 7 commits into
mainfrom
perf/w1-03-lazy-error-wrap
Nov 3, 2026
Merged

ajitpratap0 merged 7 commits into
mainfrom
perf/w1-03-lazy-error-wrap

Conversation

@ajitpratap0

@ajitpratap0 ajitpratap0 commented Oct 31, 2026 •

Copy link
Copy Markdown
Owner

Summary

Wave-1 task W1-03 (MEM-3) from the 2026-11 performance audit, including the review change from CRITIQUE item 14. Codex ported the change, and Opus reviewed it and ran the full gate.

pkg/gosqlx had 21 call sites using fmt.Errorf("%w: %w", sentinel, err). Each one built the full message on every error, even though many callers only check the error with errors.Is. They now return an unexported wrapErr:

  • Storage: both errors live in a [2]error array.
  • Error(): builds the same "sentinel: inner" text on demand.
  • Unwrap() []error: returns a slice of that array, so errors.Is and errors.As walk it without allocating. Same contract as the two-%w fmt.Errorf.

Call sites whose message adds extra context text keep fmt.Errorf as before.

Wave 0 is now on main; this branch has merged origin/main, the W0-02 goldens pass, and our private BenchmarkParseError/BenchmarkParseSuccess file was dropped in favour of the wave-0 BenchmarkParseError/* matrix. A CHANGELOG [Unreleased] entry was added.

Test Plan

  • Compatibility suite (wrap_compat_test.go, wrap_test.go): 242 distinct invalid inputs across every public entry point that wraps errors. Expected errors are built separately from direct tokenizer and parser calls with the old fmt.Errorf construction, so a missing sentinel or a changed prefix can't pass by matching its own output.
    • Error() text is byte-identical.
    • errors.Is matches exactly the same sentinels.
    • errors.As finds the same structured error, with the same code and position.
    • It also preserves an existing quirk: context-based parsing reports a different location for empty input.
  • Review fix: I fixed a staticcheck ST1008 in the test helper; its error return now comes last.
  • Gate, run outside the Codex sandbox:
Check Result
go run honnef.co/go/tools/cmd/staticcheck@latest ./pkg/gosqlx/... ✅
go test -race -count=1 ./pkg/gosqlx/... ✅
task check ✅
task test:cbinding ✅
go test -count=1 ./pkg/compatibility/... ✅

Local A/B (wave-0 BenchmarkParseError/*, task bench:ab BASE=origin/main). Apple M4, shared and heavily loaded machine (load ~14, timing CIs of +-40-60%), so timings are indicative only; B/op and allocs/op are deterministic.

Case (parse / validate) time (local, noisy) B/op allocs/op
syntax_mid -27% / -29% -55% 21 -> 12
syntax_end -32% / -23% -36% 25 -> 16
tokenize -60% / -41% -83% / -54% 19 -> 4 / 30 -> 21
long_prefix -14% / -16% (p 0.25 / 0.08, not significant) -3.4% 229 -> 219
geomean -30% (p=0.052) -43.7% -34.8%

CI amd64 perf gate (shadow): PASS, "no gated metric moved past its threshold". The gated suite (ParseE2E, ParseRW, ParseFresh, PoolGetPut, Tokenize) is neutral: geomean -0.13% (p=0.436), no allocs/op or B/op change, instructions/op within +-0.41%, A/A control clean. These are success-path benchmarks, so neutrality is expected and confirms no regression.

Message-materialized case (reviewer re-measurement, n=6, Parse then err.Error(), base origin/main vs PR). BenchmarkParseError never calls err.Error(), so the table above mostly reflects never formatting the inner *errors.Error. When the message is built, which the LSP (createDiagnosticFromError) and CLI do, the gain is small: allocs/op syntax_mid 21 -> 19, syntax_end 25 -> 23, tokenize 19 -> 17, long_prefix 229 -> 227; B/op -2.8% to -4.2% (long_prefix not significant); time indistinguishable on the loaded M4. The large gain applies to callers that only check errors.Is/errors.As or drop the message.

Behaviour note. Error() is now built lazily, so it reflects later mutation of the inner *errors.Error reached via errors.As; with fmt.Errorf the text was fixed at creation. Not a documented contract; no in-repo caller mutates errors. Noted in CHANGELOG.

Review follow-up. Added wrap_ctx_test.go (context whose Err() flips to Canceled after N calls) which kills the two previously surviving mutants (newWrap(ErrTimeout, ctxErr) in ParseWithContext mid-tokenize and mid-parse paths); verified by mutating both lines. Gate re-run on the new head: task check, task test:cbinding, ./pkg/compatibility/..., staticcheck on ./pkg/gosqlx/... all pass.

amd64 CI perf gate on the target benchmarks (shadow): PASS. The ParseError matrix is now in scripts/perf/gate-suite.txt (#927). Base 5b0818f8, head 47f43382, AMD EPYC 7763, 10 interleaved rounds; A/A control: 0 of 50 benchmarks moved more than 3%.

Case time (parse / validate) B/op allocs/op
syntax_mid -37.3% / -34.4% -55.0% 21 -> 12
syntax_end -25.9% / -30.0% -35.8% 25 -> 16
tokenize -67.4% / -34.5% -83.1% / -53.6% 19 -> 4 / 30 -> 21
long_prefix -3.7% / -0.9% -3.4% 229 -> 219

All p < 0.01; suite geomean -6.1% (p < 0.001), ParseError geomean -9.6%. The default-GC secondary run agrees (syntax_mid -30.9%, tokenize/parse -63.6%). The success-path suite stays neutral (instructions/op and allocs unchanged).

Targets and caveats (reported, not hidden):

  • PLAN gain (ParseError/* <= -12%, tokenize <= -40%): met on syntax_mid, syntax_end and tokenize/parse (-60%); tokenize/validate is -34.5% on amd64 CI (-41% locally).
  • long_prefix is not met: only -3.4% B/op and -4.4% allocs, and time -3.7% (parse) / -0.9% (validate) on amd64. Its cost is tokenizing and parsing 128 identifiers, which lazy wrapping cannot move.
  • Coverage gap: the post-success timeout branch in ParseWithTimeout (pkg/gosqlx/gosqlx.go:258, newWrap(ErrTimeout, ctxErr) when the deadline expires after a successful parse) is not exercised by a test. It only fires when the deadline elapses during a parse that still succeeds, and its wrap is identical to the covered mid-parse paths.

Breaking Changes

None. The concrete type of the returned error changes from fmt's unexported wrapper to this package's unexported wrapErr. Message text and errors.Is/errors.As behaviour are identical.

🤖 Generated with Claude Code

https://claude.ai/code/session_01DnbRnTFErhoMJdAjVEeCg2

The 21 fmt.Errorf("%w: %w", sentinel, err) sites in pkg/gosqlx formatted
the full message on every error, although many callers only test the error
with errors.Is. They now return an unexported wrapErr holding both errors
in a [2]error array: Error() builds the identical "sentinel: inner" text on
demand, and Unwrap() []error returns a slice of the array, so errors.Is and
errors.As traverse it without allocating. Sites with extra context text
keep fmt.Errorf unchanged.

Tests: a compatibility suite over 242 distinct invalid inputs across every
public entry point that wraps errors compares Error() text, errors.Is for
every sentinel and errors.As results against the former fmt.Errorf
construction, computed independently from the tokenizer and parser.

M4, GOGC=off -cpu 1, 10 interleaved process pairs:
Parse syntax_mid -28.1%, syntax_end -28.8%, tokenize -62.8%;
Validate syntax_mid -26.9%, syntax_end -29.9%, tokenize -28.3%
(all p <= 0.029); allocs e.g. 21 -> 12, 20 -> 4. long_prefix (128
identifiers before the error) -1% to -3%. Success paths unchanged.

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

vercel Bot commented Oct 31, 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 7:20am UTC

@github-actions

github-actions Bot commented Oct 31, 2026 •

Copy link
Copy Markdown

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


Review of PR #905: lazy wrapErr

I reviewed the diff only. I did not run the tests or benchmarks, and I did not read wrap_compat_test.go or error_bench_test.go.

Overall: The change is small and the approach is sound. Unwrap() []error over an inline [2]error array matches the two-%w fmt.Errorf contract. Every call site I read passes non-nil values (lint.go is guarded by its switch, and err != nil guards the others). The countedWrapError test shows that nothing is formatted until Error() is called. The compat suite compares against independently built legacy errors, which is the right way to test this.

Things to check before merging

  1. Lazy formatting and pooled state (the main risk).

    • The old code captured the message text at wrap time. Now Error() runs later, after PutTokenizer/PutParser and the tokscratch buffers have been released by the deferred calls.
    • If an inner error (a tokenizer or parser *errors.Error) holds a slice, a []byte view, or a pointer into pooled or scratch memory, the text could change or be corrupted once those objects are reused.
    • It would show up under concurrency, and only if you call Error() after the pools are reused.
    • Please confirm the inner errors own their strings. A cheap safeguard is a -race test that:
      1. gets an error from Parse;
      2. runs many other parses concurrently;
      3. compares err.Error() to the text captured immediately.
    • The existing tests call Error() right away, so they wouldn't catch this.
  2. errors.Join in tree.go. This path now returns newWrap(ErrSyntax, joined). errors.Is and errors.As still traverse correctly. A joined of a single error still prints as before, so the text should be the same. The compat suite seems to cover it, but it is worth confirming that a recovery case with 2+ diagnostics is among the 242 inputs.

  3. Nil safety. Error() panics if either error is nil, whereas fmt.Errorf would print %!w(<nil>). The doc comment says both must be non-nil, which is fine. For an unexported helper you could add a cheap guard, or just keep the comment.

  4. Aliasing in Unwrap. w.errs[:] hands callers a mutable view of the internal array. Standard library walkers don't write to it, so this is fine in practice. Mention it only if you want to be defensive.

  5. Concrete-type change. The PR says there are no breaking changes. Anyone who type-asserted on *fmt.wrapErrors could not have done so, because the type is unexported, so I agree.

  6. Missed targets. The PR states honestly that long_prefix and Validate/tokenize missed the plan's thresholds. Since this is a draft gated on W0-02 and W0-09, I don't see that as a blocker.

  7. Minor. error_bench_test.go should keep benchmarks deterministic. Error() builds the string on every call, so callers that log an error repeatedly pay the cost each time. Caching it with a sync.Once is probably not worth it.

Verdict: Looks good. Please address item 1 (pooled-memory lifetime) with a test or a quick audit of the inner error types. The rest are nits.

@ajitpratap0
ajitpratap0 marked this pull request as ready for review November 1, 2026 15:41
@github-actions github-actions Bot added the documentation Improvements or additions to documentation label Nov 1, 2026
@github-actions

github-actions Bot commented Nov 1, 2026 •

Copy link
Copy Markdown

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


Review of PR #905: lazy wrapErr

I reviewed the diff and the pkg/errors.Error type. I did not run the tests or benchmarks.

Verdict: Looks good. The change is small, and the approach matches the old two-%w fmt.Errorf contract.

Correctness

  • Unwrap() []error over an inline [2]error array behaves like fmt.Errorf("%w: %w") for errors.Is and errors.As.
  • TestWrapErrDefersFormatting checks that nothing is formatted until Error() is called. It also checks that the text matches the legacy output and that errors.As visits the sentinel first.
  • The dialect-specific test builds its expected errors independently of the code under test. That is the right way to catch a changed prefix or a missing sentinel.
  • Every call site I read passes non-nil values: lint.go is guarded by its switch, and the others are guarded by err != nil.

Pooled-memory lifetime

  • This was the main concern in the earlier review, and I checked it.
  • errors.Error holds only string, Location, *ErrorContext, Cause and HelpURL. ErrorContext.SQL is a string. There are no []byte views or pointers into pooled tokenizer or parser memory.
  • Calling Error() after PutTokenizer/PutParser is therefore safe.
  • If you want a regression guard, add a -race test that:
    1. takes an error from Parse;
    2. runs many parses concurrently;
    3. checks that err.Error() equals the text captured immediately.

Minor points (non-blocking)

  1. In tree.go, the recovery path returns newWrap(ErrSyntax, errors.Join(errs...)). Please confirm the 242 compat inputs include a case with two or more diagnostics. A single-error join prints identically, but a multi-error join prints newline-separated text.
  2. Error() panics on a nil member, whereas fmt.Errorf prints %!w(<nil>). The doc comment states the non-nil requirement, which is enough for an unexported type.
  3. Unwrap returns w.errs[:], which is a mutable view of the internal array. The standard library walkers don't write to it, so this is fine.
  4. Each Error() call rebuilds the string. Callers that log the same error repeatedly pay for it each time. Caching it isn't worth the added complexity.
  5. long_prefix and Validate/tokenize miss the plan's targets. The PR says so openly, and the PR is a draft gated on W0-02 and W0-09, so this isn't a blocker.
  6. Changing the concrete type is not a breaking change. The old type was unexported, so nobody could assert on it.

@github-actions

github-actions Bot commented Nov 1, 2026 •

Copy link
Copy Markdown

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

Base 95bb0c3323c2, head 359d021f5e23. 10 interleaved rounds, benchtime 200ms, -cpu 1, GOMEMLIMIT 64MiB, pinned to CPU 3 (SMT siblings 2-3) with taskset.

Runner CPU: AMD EPYC 9V74 80-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.

  • 3 benchmark(s) slower by 3% or more at p < 0.01
  • 7 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 -1.12% 0.069 -1.16% +0.00% 5 → 5 ok
ParseE2E/select_where +1.39% 0.002 +0.40% +0.00% 18 → 18 ok
ParseE2E/join +1.74% 0.028 +0.50% +0.00% 17 → 17 ok
ParseE2E/aggregate +1.79% 0.028 +0.95% +0.00% 18 → 18 ok
ParseE2E/subquery +1.27% 0.105 +0.47% +0.00% 14 → 14 ok
ParseE2E/cte +1.12% 0.165 +0.49% +0.00% 21 → 21 ok
ParseE2E/window -1.82% 0.970 +0.33% +0.00% 11 → 11 ok
ParseE2E/complex +3.77% <0.001 +3.47% +0.00% 49 → 49 slower
ParseE2E/insert -0.83% 1.000 -0.94% +0.00% 17 → 17 ok
ParseE2E/update -1.09% 0.212 +0.29% +0.00% 13 → 13 ok
ParseE2E/delete -3.31% 0.123 -2.32% +0.00% 5 → 5 ok
ParseRW/postgresql/medium +1.83% 0.002 +0.64% +0.00% 646 → 646 ok
ParseRW/mysql/medium +3.82% 0.003 +1.69% +0.00% 368 → 368 slower
ParseRW/mariadb/medium +2.19% 0.001 +1.28% +0.00% 528 → 528 ok
ParseRW/sqlserver/medium +3.83% 0.003 +0.21% +0.00% 927 → 927 slower
ParseRW/oracle/medium +2.21% 0.280 +1.66% +0.00% 1090 → 1090 ok
ParseRW/sqlite/medium +1.09% 0.063 -0.21% +0.00% 347 → 347 ok
ParseRW/snowflake/medium -3.99% 0.315 -2.54% +0.00% 776 → 776 ok
ParseRW/clickhouse/medium -1.57% 0.165 -1.62% -0.00% 383 → 383 ok
ParseFresh/simple_select +0.78% 0.019 -0.02% +0.00% 6 → 6 ok
ParseFresh/join +0.58% 0.013 +0.51% +0.00% 18 → 18 ok
ParseFresh/complex +1.46% <0.001 +0.02% +0.00% 50 → 50 ok
PoolGetPut/serial +0.04% 0.405 +0.02% +0.00% 0 → 0 ok
Tokenize/scratch/join +0.14% 0.146 +0.47% +0.00% 0 → 0 ok
Tokenize/scratch/complex -0.14% 0.543 +0.10% +0.00% 0 → 0 ok
Tokenize/scratch/insert +0.06% 0.570 +0.06% +0.00% 0 → 0 ok
Normalize/simple_select +0.18% 0.196 +0.09% +0.00% 7 → 7 ok
Normalize/join -0.36% 0.449 +0.10% +0.00% 23 → 23 ok
Normalize/complex +0.12% 0.820 +0.10% +0.00% 68 → 68 ok
TransformParse/simple_select -0.09% 0.543 -0.09% +0.00% 5 → 5 ok
TransformParse/join +0.33% 0.472 -0.43% +0.00% 17 → 17 ok
TransformParse/complex -0.45% 0.853 -0.17% +0.00% 49 → 49 ok
LintRW/postgresql -0.04% 0.796 +0.40% +0.00% 36484 → 36484 ok
LintRW/mysql +1.07% 0.481 +3.14% -0.00% 44207 → 44207 ok
LintRW/mariadb +0.44% 0.739 +2.96% -0.00% 40741 → 40741 ok
LintRW/sqlserver +1.01% 0.684 +1.35% -0.00% 36896 → 36896 ok
LintRW/oracle +0.55% 0.579 +0.79% -0.00% 45291 → 45291 ok
LintRW/sqlite -2.55% 0.023 -1.41% +0.00% 40583 → 40583 ok
LintRW/snowflake -2.18% 0.023 -0.93% +0.00% 39843 → 39843 ok
LintRW/clickhouse -2.29% 0.023 -0.76% -0.00% 44995 → 44995 ok
LintRule/L007 +0.24% 0.796 +0.13% +0.00% 72565 → 72565 ok
LintRule/L010 -0.48% 0.436 +0.37% +0.00% 20758 → 20758 ok
ParseError/syntax_mid/parse -32.35% <0.001 -0.22% -55.03% 21 → 12 faster, -allocs
ParseError/syntax_mid/validate -31.11% <0.001 +0.37% -55.03% 21 → 12 faster, -allocs
ParseError/syntax_end/parse -29.62% <0.001 -0.81% -35.79% 25 → 16 faster, -allocs
ParseError/syntax_end/validate -27.64% <0.001 -0.13% -35.79% 25 → 16 faster, -allocs
ParseError/tokenize/parse -65.36% <0.001 +1.01% -83.08% 19 → 4 faster, -allocs
ParseError/tokenize/validate -29.60% <0.001 +3.94% -53.61% 30 → 21 faster, -allocs
ParseError/long_prefix/parse -5.50% <0.001 +0.62% -3.43% 229 → 219 faster, -allocs
ParseError/long_prefix/validate -0.88% 0.315 +3.54% -3.43% 229 → 219 -allocs
geomean -5.47% <0.001 +0.37%

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

Instructions per operation (cmd/perfdriver)

case instructions/op status
e2e/aggregate -0.02% ok
e2e/complex +0.03% ok
e2e/cte +0.02% ok
e2e/delete -0.05% ok
e2e/insert -0.00% ok
e2e/join -0.00% ok
e2e/select_where +0.02% ok
e2e/simple_select +0.16% ok
e2e/subquery -0.10% ok
e2e/update +0.02% 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.31% 0.471
ParseE2E/select_where +0.16% 0.910
ParseE2E/join +1.12% 0.045
ParseE2E/aggregate +1.32% <0.001
ParseE2E/subquery -0.10% 0.910
ParseE2E/cte +1.33% <0.001
ParseE2E/window +1.05% 0.081
ParseE2E/complex +1.33% 0.005
ParseE2E/insert +0.17% 0.940
ParseE2E/update -0.44% 0.054
ParseE2E/delete -0.64% 0.241
ParseRW/postgresql/medium +1.20% 0.015
ParseRW/mysql/medium +1.77% <0.001
ParseRW/mariadb/medium +1.14% <0.001
ParseRW/sqlserver/medium +1.13% 0.035
ParseRW/oracle/medium +0.54% 0.035
ParseRW/sqlite/medium +1.79% 0.003
ParseRW/snowflake/medium +0.85% 0.009
ParseRW/clickhouse/medium +1.30% <0.001
ParseFresh/simple_select +0.39% 0.004
ParseFresh/join +0.29% 0.307
ParseFresh/complex +0.91% <0.001
PoolGetPut/serial +0.13% 0.037
Tokenize/scratch/join +0.14% 0.253
Tokenize/scratch/complex -0.31% 0.185
Tokenize/scratch/insert -0.06% 0.853
Normalize/simple_select +0.38% 0.161
Normalize/join +0.30% 1.000
Normalize/complex +0.40% 0.353
TransformParse/simple_select -0.86% 0.038
TransformParse/join -0.07% 0.762
TransformParse/complex -0.06% 0.762
LintRW/postgresql -0.10% 0.631
LintRW/mysql +0.13% 0.912
LintRW/mariadb -0.10% 0.796
LintRW/sqlserver -0.38% 0.123
LintRW/oracle +0.17% 0.579
LintRW/sqlite -0.16% 0.912
LintRW/snowflake -0.79% 0.029
LintRW/clickhouse -0.05% 0.529
LintRule/L007 -0.22% 0.280
LintRule/L010 -0.51% 0.019
ParseError/syntax_mid/parse -29.86% <0.001
ParseError/syntax_mid/validate -27.40% <0.001
ParseError/syntax_end/parse -25.01% <0.001
ParseError/syntax_end/validate -24.42% <0.001
ParseError/tokenize/parse -62.41% <0.001
ParseError/tokenize/validate -29.06% <0.001
ParseError/long_prefix/parse -2.45% 0.011
ParseError/long_prefix/validate -1.08% 0.023
geomean -4.79%
Environment
date:        2026-11-03T07:21:44Z
base:        95bb0c3323c2b835712ed04cae1f3e238175d081
head:        working tree of 359d021f5e237ee8165739c039fa1cd71df2d07b
cpu model:   AMD EPYC 9V74 80-Core Processor (x86_64)
go:          go version go1.26.9 linux/amd64
kernel:      Linux 7.0.0-1017-azure x86_64
pinning:     pinned to CPU 3 (SMT siblings 2-3) with taskset
gcoff env:   GOGC=off GOMEMLIMIT=64MiB
clocksource: tsc
load:        2.63, 0.92, 0.34
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 9V74 80-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:                                2 MiB (2 instances)
L3 cache:                                32 MiB (1 instance)
benchstat (primary metric)
goos: linux
goarch: amd64
pkg: github.com/ajitpratap0/GoSQLX/pkg/fingerprint
cpu: AMD EPYC 9V74 80-Core Processor                
                        │    base     │                head                │
                        │   sec/op    │   sec/op     vs base               │
Normalize/simple_select   1.696µ ± 0%   1.699µ ± 1%       ~ (p=0.194 n=10)
Normalize/join            5.253µ ± 1%   5.234µ ± 1%       ~ (p=0.447 n=10)
Normalize/complex         13.37µ ± 1%   13.39µ ± 1%       ~ (p=0.810 n=10)
geomean                   4.920µ        4.919µ       -0.02%

                        │     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.211Ki ± 0%   2.211Ki ± 0%       ~ (p=1.000 n=10) ¹
Normalize/complex         6.117Ki ± 0%   6.117Ki ± 0%       ~ (p=1.000 n=10) ¹
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=10) ¹
Normalize/join            23.00 ± 0%   23.00 ± 0%       ~ (p=1.000 n=10) ¹
Normalize/complex         68.00 ± 0%   68.00 ± 0%       ~ (p=1.000 n=10) ¹
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.117µ ± 1%   1.104µ ± 1%        ~ (p=0.065 n=10)
ParseE2E/select_where              2.656µ ± 1%   2.693µ ± 1%   +1.39% (p=0.001 n=10)
ParseE2E/join                      3.596µ ± 2%   3.659µ ± 1%        ~ (p=0.024 n=10)
ParseE2E/aggregate                 2.731µ ± 3%   2.780µ ± 2%        ~ (p=0.024 n=10)
ParseE2E/subquery                  2.442µ ± 2%   2.474µ ± 1%        ~ (p=0.105 n=10)
ParseE2E/cte                       3.395µ ± 2%   3.433µ ± 5%        ~ (p=0.165 n=10)
ParseE2E/window                    2.259µ ± 5%   2.218µ ± 4%        ~ (p=0.954 n=10)
ParseE2E/complex                   9.175µ ± 1%   9.521µ ± 2%   +3.77% (p=0.000 n=10)
ParseE2E/insert                    1.755µ ± 2%   1.741µ ± 1%        ~ (p=0.988 n=10)
ParseE2E/update                    1.562µ ± 5%   1.545µ ± 4%        ~ (p=0.210 n=10)
ParseE2E/delete                    965.8n ± 3%   933.9n ± 7%        ~ (p=0.123 n=10)
ParseRW/postgresql/medium          104.4µ ± 2%   106.3µ ± 0%   +1.83% (p=0.002 n=10)
ParseRW/mysql/medium               64.39µ ± 3%   66.85µ ± 1%   +3.82% (p=0.003 n=10)
ParseRW/mariadb/medium             91.23µ ± 2%   93.23µ ± 2%   +2.19% (p=0.001 n=10)
ParseRW/sqlserver/medium           178.5µ ± 3%   185.4µ ± 5%   +3.83% (p=0.003 n=10)
ParseRW/oracle/medium              225.1µ ± 4%   230.1µ ± 5%        ~ (p=0.280 n=10)
ParseRW/sqlite/medium              52.64µ ± 1%   53.21µ ± 2%        ~ (p=0.063 n=10)
ParseRW/snowflake/medium           135.7µ ± 4%   130.3µ ± 6%        ~ (p=0.315 n=10)
ParseRW/clickhouse/medium          63.60µ ± 2%   62.60µ ± 2%        ~ (p=0.165 n=10)
ParseFresh/simple_select           5.468µ ± 1%   5.511µ ± 1%        ~ (p=0.019 n=10)
ParseFresh/join                    8.203µ ± 1%   8.250µ ± 2%   +0.58% (p=0.009 n=10)
ParseFresh/complex                 13.57µ ± 0%   13.77µ ± 1%   +1.46% (p=0.000 n=10)
PoolGetPut/serial                  56.92n ± 0%   56.95n ± 8%        ~ (p=0.403 n=10)
ParseError/syntax_mid/parse        2.700µ ± 1%   1.827µ ± 8%  -32.35% (p=0.000 n=10)
ParseError/syntax_mid/validate     2.456µ ± 1%   1.692µ ± 1%  -31.11% (p=0.000 n=10)
ParseError/syntax_end/parse        2.784µ ± 2%   1.960µ ± 2%  -29.62% (p=0.000 n=10)
ParseError/syntax_end/validate     2.795µ ± 2%   2.023µ ± 1%  -27.64% (p=0.000 n=10)
ParseError/tokenize/parse         1940.0n ± 2%   672.0n ± 1%  -65.36% (p=0.000 n=10)
ParseError/tokenize/validate       2.891µ ± 3%   2.035µ ± 8%  -29.60% (p=0.000 n=10)
ParseError/long_prefix/parse       51.60µ ± 3%   48.76µ ± 2%   -5.50% (p=0.000 n=10)
ParseError/long_prefix/validate    48.86µ ± 5%   48.43µ ± 3%        ~ (p=0.315 n=10)
geomean                            7.753µ        7.091µ        -8.54%

                                │     base     │                 head                  │
                                │     B/s      │     B/s       vs base                 │
ParseE2E/simple_select            22.21Mi ± 1%   22.45Mi ± 1%         ~ (p=0.085 n=10)
ParseE2E/select_where             36.98Mi ± 1%   36.47Mi ± 1%    -1.38% (p=0.001 n=10)
ParseE2E/join                     33.42Mi ± 2%   32.85Mi ± 1%         ~ (p=0.028 n=10)
ParseE2E/aggregate                29.34Mi ± 3%   28.82Mi ± 2%         ~ (p=0.024 n=10)
ParseE2E/subquery                 32.01Mi ± 2%   31.61Mi ± 1%         ~ (p=0.105 n=10)
ParseE2E/cte                      34.83Mi ± 2%   34.45Mi ± 4%         ~ (p=0.165 n=10)
ParseE2E/window                   39.69Mi ± 5%   40.44Mi ± 4%         ~ (p=0.971 n=10)
ParseE2E/complex                  39.29Mi ± 2%   37.87Mi ± 2%    -3.63% (p=0.000 n=10)
ParseE2E/insert                   41.30Mi ± 2%   41.64Mi ± 1%         ~ (p=0.988 n=10)
ParseE2E/update                   33.58Mi ± 6%   33.94Mi ± 5%         ~ (p=0.218 n=10)
ParseE2E/delete                   51.36Mi ± 3%   53.11Mi ± 6%         ~ (p=0.123 n=10)
ParseRW/postgresql/medium         34.38Mi ± 2%   33.76Mi ± 0%    -1.80% (p=0.001 n=10)
ParseRW/mysql/medium              42.03Mi ± 3%   40.49Mi ± 1%    -3.68% (p=0.003 n=10)
ParseRW/mariadb/medium            38.86Mi ± 2%   38.03Mi ± 2%    -2.13% (p=0.001 n=10)
ParseRW/sqlserver/medium          37.28Mi ± 3%   35.89Mi ± 5%    -3.73% (p=0.003 n=10)
ParseRW/oracle/medium             37.25Mi ± 4%   36.44Mi ± 5%         ~ (p=0.280 n=10)
ParseRW/sqlite/medium             39.25Mi ± 1%   38.82Mi ± 1%         ~ (p=0.063 n=10)
ParseRW/snowflake/medium          37.46Mi ± 4%   39.02Mi ± 5%         ~ (p=0.315 n=10)
ParseRW/clickhouse/medium         35.43Mi ± 2%   36.00Mi ± 2%         ~ (p=0.165 n=10)
ParseError/syntax_mid/parse       16.96Mi ± 1%   25.05Mi ± 7%   +47.75% (p=0.000 n=10)
ParseError/syntax_mid/validate    18.64Mi ± 1%   27.05Mi ± 1%   +45.13% (p=0.000 n=10)
ParseError/syntax_end/parse       13.36Mi ± 2%   18.98Mi ± 2%   +42.08% (p=0.000 n=10)
ParseError/syntax_end/validate    13.30Mi ± 2%   18.39Mi ± 1%   +38.24% (p=0.000 n=10)
ParseError/tokenize/parse         22.61Mi ± 2%   65.28Mi ± 1%  +188.72% (p=0.000 n=10)
ParseError/tokenize/validate      15.17Mi ± 3%   21.55Mi ± 7%   +42.05% (p=0.000 n=10)
ParseError/long_prefix/parse      44.76Mi ± 3%   47.36Mi ± 2%    +5.83% (p=0.000 n=10)
ParseError/long_prefix/validate   47.27Mi ± 5%   47.69Mi ± 3%         ~ (p=0.315 n=10)
geomean                           30.87Mi        34.24Mi        +10.91%

                                │      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                     1.836Ki ± 0%     1.836Ki ± 0%        ~ (p=1.000 n=10) ¹
ParseE2E/aggregate                1.812Ki ± 0%     1.812Ki ± 0%        ~ (p=1.000 n=10) ¹
ParseE2E/subquery                 1.828Ki ± 0%     1.828Ki ± 0%        ~ (p=1.000 n=10) ¹
ParseE2E/cte                      2.336Ki ± 0%     2.336Ki ± 0%        ~ (p=1.000 n=10) ¹
ParseE2E/window                   1.281Ki ± 0%     1.281Ki ± 0%        ~ (p=1.000 n=10) ¹
ParseE2E/complex                  5.023Ki ± 0%     5.023Ki ± 0%        ~ (p=1.000 n=10) ¹
ParseE2E/insert                     824.0 ± 0%       824.0 ± 0%        ~ (p=1.000 n=10) ¹
ParseE2E/update                     736.0 ± 0%       736.0 ± 0%        ~ (p=1.000 n=10) ¹
ParseE2E/delete                     400.0 ± 0%       400.0 ± 0%        ~ (p=1.000 n=10) ¹
ParseRW/postgresql/medium         65.86Ki ± 0%     65.86Ki ± 0%        ~ (p=0.033 n=10)
ParseRW/mysql/medium              42.36Ki ± 0%     42.36Ki ± 0%        ~ (p=0.474 n=10)
ParseRW/mariadb/medium            60.56Ki ± 0%     60.56Ki ± 0%   +0.00% (p=0.002 n=10)
ParseRW/sqlserver/medium          83.65Ki ± 0%     83.65Ki ± 0%        ~ (p=0.350 n=10)
ParseRW/oracle/medium             104.8Ki ± 0%     104.8Ki ± 0%        ~ (p=1.000 n=10)
ParseRW/sqlite/medium             38.12Ki ± 0%     38.12Ki ± 0%        ~ (p=1.000 n=10) ¹
ParseRW/snowflake/medium          77.63Ki ± 0%     77.63Ki ± 0%        ~ (p=1.000 n=10) ¹
ParseRW/clickhouse/medium         34.82Ki ± 0%     34.82Ki ± 0%        ~ (p=0.370 n=10)
ParseFresh/simple_select            896.0 ± 0%       896.0 ± 0%        ~ (p=1.000 n=10) ¹
ParseFresh/join                   1.914Ki ± 0%     1.914Ki ± 0%        ~ (p=1.000 n=10) ¹
ParseFresh/complex                5.102Ki ± 0%     5.102Ki ± 0%        ~ (p=1.000 n=10) ¹
PoolGetPut/serial                   0.000 ± 0%       0.000 ± 0%        ~ (p=1.000 n=10) ¹
ParseError/syntax_mid/parse        1512.0 ± 0%       680.0 ± 0%  -55.03% (p=0.000 n=10)
ParseError/syntax_mid/validate     1512.0 ± 0%       680.0 ± 0%  -55.03% (p=0.000 n=10)
ParseError/syntax_end/parse       2.227Ki ± 0%     1.430Ki ± 0%  -35.79% (p=0.000 n=10)
ParseError/syntax_end/validate    2.227Ki ± 0%     1.430Ki ± 0%  -35.79% (p=0.000 n=10)
ParseError/tokenize/parse          1608.0 ± 0%       272.0 ± 0%  -83.08% (p=0.000 n=10)
ParseError/tokenize/validate      3.789Ki ± 0%     1.758Ki ± 0%  -53.61% (p=0.000 n=10)
ParseError/long_prefix/parse      23.51Ki ± 0%     22.71Ki ± 0%   -3.43% (p=0.000 n=10)
ParseError/long_prefix/validate   23.48Ki ± 0%     22.68Ki ± 0%   -3.43% (p=0.000 n=10)
geomean                                        ²                 -15.17%                ²
¹ 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                   49.00 ± 0%      49.00 ± 0%        ~ (p=1.000 n=10) ¹
ParseE2E/insert                    17.00 ± 0%      17.00 ± 0%        ~ (p=1.000 n=10) ¹
ParseE2E/update                    13.00 ± 0%      13.00 ± 0%        ~ (p=1.000 n=10) ¹
ParseE2E/delete                    5.000 ± 0%      5.000 ± 0%        ~ (p=1.000 n=10) ¹
ParseRW/postgresql/medium          646.0 ± 0%      646.0 ± 0%        ~ (p=1.000 n=10) ¹
ParseRW/mysql/medium               368.0 ± 0%      368.0 ± 0%        ~ (p=1.000 n=10) ¹
ParseRW/mariadb/medium             528.0 ± 0%      528.0 ± 0%        ~ (p=1.000 n=10) ¹
ParseRW/sqlserver/medium           927.0 ± 0%      927.0 ± 0%        ~ (p=1.000 n=10) ¹
ParseRW/oracle/medium             1.090k ± 0%     1.090k ± 0%        ~ (p=1.000 n=10) ¹
ParseRW/sqlite/medium              347.0 ± 0%      347.0 ± 0%        ~ (p=1.000 n=10) ¹
ParseRW/snowflake/medium           776.0 ± 0%      776.0 ± 0%        ~ (p=1.000 n=10) ¹
ParseRW/clickhouse/medium          383.0 ± 0%      383.0 ± 0%        ~ (p=1.000 n=10) ¹
ParseFresh/simple_select           6.000 ± 0%      6.000 ± 0%        ~ (p=1.000 n=10) ¹
ParseFresh/join                    18.00 ± 0%      18.00 ± 0%        ~ (p=1.000 n=10) ¹
ParseFresh/complex                 50.00 ± 0%      50.00 ± 0%        ~ (p=1.000 n=10) ¹
PoolGetPut/serial                  0.000 ± 0%      0.000 ± 0%        ~ (p=1.000 n=10) ¹
ParseError/syntax_mid/parse        21.00 ± 0%      12.00 ± 0%  -42.86% (p=0.000 n=10)
ParseError/syntax_mid/validate     21.00 ± 0%      12.00 ± 0%  -42.86% (p=0.000 n=10)
ParseError/syntax_end/parse        25.00 ± 0%      16.00 ± 0%  -36.00% (p=0.000 n=10)
ParseError/syntax_end/validate     25.00 ± 0%      16.00 ± 0%  -36.00% (p=0.000 n=10)
ParseError/tokenize/parse         19.000 ± 0%      4.000 ± 0%  -78.95% (p=0.000 n=10)
ParseError/tokenize/validate       30.00 ± 0%      21.00 ± 0%  -30.00% (p=0.000 n=10)
ParseError/long_prefix/parse       229.0 ± 0%      219.0 ± 0%   -4.37% (p=0.000 n=10)
ParseError/long_prefix/validate    229.0 ± 0%      219.0 ± 0%   -4.37% (p=0.000 n=10)
geomean                                       ²                -12.15%                ²
¹ all samples are equal
² summaries must be >0 to compute geomean

                          │    base     │                head                │
                          │  sec/stmt   │  sec/stmt    vs base               │
ParseRW/postgresql/medium   8.698µ ± 2%   8.858µ ± 0%  +1.83% (p=0.001 n=10)
ParseRW/mysql/medium        8.049µ ± 3%   8.357µ ± 1%  +3.82% (p=0.003 n=10)
ParseRW/mariadb/medium      10.14µ ± 2%   10.36µ ± 2%  +2.19% (p=0.001 n=10)
ParseRW/sqlserver/medium    8.502µ ± 3%   8.827µ ± 5%  +3.82% (p=0.003 n=10)
ParseRW/oracle/medium       9.380µ ± 4%   9.587µ ± 5%       ~ (p=0.280 n=10)
ParseRW/sqlite/medium       8.773µ ± 1%   8.868µ ± 2%       ~ (p=0.063 n=10)
ParseRW/snowflake/medium    10.44µ ± 4%   10.02µ ± 6%       ~ (p=0.315 n=10)
ParseRW/clickhouse/medium   9.085µ ± 2%   8.944µ ± 2%       ~ (p=0.165 n=10)
geomean                     9.102µ        9.206µ       +1.14%

pkg: github.com/ajitpratap0/GoSQLX/pkg/linter
                  │    base     │                head                │
                  │   sec/op    │   sec/op     vs base               │
LintRW/postgresql   5.130m ± 1%   5.128m ± 1%       ~ (p=0.796 n=10)
LintRW/mysql        5.699m ± 3%   5.761m ± 3%       ~ (p=0.481 n=10)
LintRW/mariadb      5.480m ± 3%   5.505m ± 2%       ~ (p=0.739 n=10)
LintRW/sqlserver    5.197m ± 4%   5.250m ± 3%       ~ (p=0.684 n=10)
LintRW/oracle       6.322m ± 2%   6.357m ± 5%       ~ (p=0.579 n=10)
LintRW/sqlite       5.340m ± 3%   5.203m ± 2%       ~ (p=0.023 n=10)
LintRW/snowflake    5.471m ± 3%   5.352m ± 1%       ~ (p=0.023 n=10)
LintRW/clickhouse   6.037m ± 2%   5.899m ± 1%       ~ (p=0.023 n=10)
LintRule/L007       6.732m ± 2%   6.748m ± 2%       ~ (p=0.796 n=10)
LintRule/L010       2.862m ± 5%   2.848m ± 5%       ~ (p=0.436 n=10)
geomean             5.312m        5.289m       -0.43%

                  │     base     │                head                 │
                  │     B/s      │     B/s       vs base               │
LintRW/postgresql   5.522Mi ± 1%   5.522Mi ± 1%       ~ (p=0.690 n=10)
LintRW/mysql        5.741Mi ± 3%   5.679Mi ± 3%       ~ (p=0.516 n=10)
LintRW/mariadb      5.569Mi ± 3%   5.541Mi ± 2%       ~ (p=0.753 n=10)
LintRW/sqlserver    5.965Mi ± 3%   5.903Mi ± 3%       ~ (p=0.671 n=10)
LintRW/oracle       6.285Mi ± 2%   6.247Mi ± 5%       ~ (p=0.566 n=10)
LintRW/sqlite       5.374Mi ± 3%   5.517Mi ± 2%       ~ (p=0.016 n=10)
LintRW/snowflake    5.703Mi ± 3%   5.827Mi ± 1%       ~ (p=0.028 n=10)
LintRW/clickhouse   5.379Mi ± 2%   5.503Mi ± 1%       ~ (p=0.021 n=10)
geomean             5.685Mi        5.712Mi       +0.48%

                  │     base     │                 head                  │
                  │     B/op     │     B/op      vs base                 │
LintRW/postgresql   3.497Mi ± 0%   3.497Mi ± 0%       ~ (p=0.363 n=10)
LintRW/mysql        4.352Mi ± 0%   4.352Mi ± 0%       ~ (p=0.926 n=10)
LintRW/mariadb      4.026Mi ± 0%   4.026Mi ± 0%       ~ (p=0.404 n=10)
LintRW/sqlserver    3.594Mi ± 0%   3.594Mi ± 0%       ~ (p=0.138 n=10)
LintRW/oracle       4.386Mi ± 0%   4.386Mi ± 0%       ~ (p=0.838 n=10)
LintRW/sqlite       3.816Mi ± 0%   3.816Mi ± 0%       ~ (p=0.753 n=10)
LintRW/snowflake    3.847Mi ± 0%   3.847Mi ± 0%       ~ (p=0.148 n=10)
LintRW/clickhouse   4.295Mi ± 0%   4.295Mi ± 0%       ~ (p=1.000 n=10)
LintRule/L007       2.942Mi ± 0%   2.942Mi ± 0%       ~ (p=0.582 n=10)
LintRule/L010       859.6Ki ± 0%   859.6Ki ± 0%       ~ (p=1.000 n=10) ¹
geomean             3.294Mi        3.294Mi       -0.00%
¹ all samples are equal

                  │    base     │                 head                 │
                  │  allocs/op  │  allocs/op   vs base                 │
LintRW/postgresql   36.48k ± 0%   36.48k ± 0%       ~ (p=0.211 n=10)
LintRW/mysql        44.21k ± 0%   44.21k ± 0%       ~ (p=1.000 n=10) ¹
LintRW/mariadb      40.74k ± 0%   40.74k ± 0%       ~ (p=1.000 n=10) ¹
LintRW/sqlserver    36.90k ± 0%   36.90k ± 0%       ~ (p=1.000 n=10) ¹
LintRW/oracle       45.29k ± 0%   45.29k ± 0%       ~ (p=1.000 n=10) ¹
LintRW/sqlite       40.58k ± 0%   40.58k ± 0%       ~ (p=1.000 n=10) ¹
LintRW/snowflake    39.84k ± 0%   39.84k ± 0%       ~ (p=1.000 n=10)
LintRW/clickhouse   44.99k ± 0%   44.99k ± 0%       ~ (p=1.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.00%
¹ all samples are equal

                  │    base     │                head                │
                  │  sec/stmt   │  sec/stmt    vs base               │
LintRW/postgresql   16.82µ ± 1%   16.81µ ± 1%       ~ (p=0.754 n=10)
LintRW/mysql        13.77µ ± 3%   13.91µ ± 3%       ~ (p=0.481 n=10)
LintRW/mariadb      14.42µ ± 3%   14.49µ ± 2%       ~ (p=0.739 n=10)
LintRW/sqlserver    18.76µ ± 4%   18.95µ ± 3%       ~ (p=0.684 n=10)
LintRW/oracle       17.18µ ± 2%   17.27µ ± 5%       ~ (p=0.579 n=10)
LintRW/sqlite       15.34µ ± 3%   14.95µ ± 2%       ~ (p=0.023 n=10)
LintRW/snowflake    16.73µ ± 3%   16.37µ ± 1%       ~ (p=0.023 n=10)
LintRW/clickhouse   15.32µ ± 2%   14.97µ ± 1%       ~ (p=0.023 n=10)
LintRule/L007       2.393µ ± 2%   2.399µ ± 2%       ~ (p=0.754 n=10)
LintRule/L010       1.017µ ± 5%   1.013µ ± 5%       ~ (p=0.447 n=10)
geomean             10.03µ        9.987µ       -0.43%

pkg: github.com/ajitpratap0/GoSQLX/pkg/sql/tokenizer
                         │    base     │                head                │
                         │   sec/op    │   sec/op     vs base               │
Tokenize/scratch/join      1.075µ ± 0%   1.076µ ± 6%       ~ (p=0.142 n=10)
Tokenize/scratch/complex   2.858µ ± 0%   2.854µ ± 2%       ~ (p=0.537 n=10)
Tokenize/scratch/insert    536.0n ± 0%   536.3n ± 0%       ~ (p=0.578 n=10)
geomean                    1.181µ        1.181µ       +0.02%

                         │     base     │                head                 │
                         │     B/s      │     B/s       vs base               │
Tokenize/scratch/join      111.9Mi ± 0%   111.7Mi ± 6%       ~ (p=0.071 n=10)
Tokenize/scratch/complex   126.1Mi ± 0%   126.3Mi ± 2%       ~ (p=0.469 n=10)
Tokenize/scratch/insert    135.2Mi ± 0%   135.1Mi ± 0%       ~ (p=0.565 n=10)
geomean                    124.0Mi        124.0Mi       -0.02%

                         │     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.080µ ± 10%   1.079µ ± 6%       ~ (p=0.540 n=10)
TransformParse/join            3.833µ ±  2%   3.845µ ± 2%       ~ (p=0.470 n=10)
TransformParse/complex         9.508µ ±  3%   9.465µ ± 3%       ~ (p=0.853 n=10)
geomean                        3.401µ         3.399µ       -0.07%

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

Run, raw data and benchstat

…zed-message gain [W1-03]

Add a flipping-context test that kills the two surviving mutants in
ParseWithContext (mid-tokenize and mid-parse cancel paths). CHANGELOG now
states the gain applies to callers that drop the message, gives the
materialized-message numbers, and notes Error() reflects later mutation of
the inner error.

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

github-actions Bot commented Nov 1, 2026 •

Copy link
Copy Markdown

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


Review of PR #905: lazy wrapErr

I read the diff and the new tests. I did not run the tests or benchmarks. My attempt to build and vet was blocked by a permission prompt, so I'm relying on the passing CI gate for that.

Verdict: Looks good to merge. The earlier review items are either resolved or non-blocking.

Correctness

  • wrapErr keeps both errors in a [2]error array. Unwrap() []error returns a slice of that array, so errors.Is and errors.As see the same tree as the two-%w fmt.Errorf, and the sentinel is visited first.
  • Error() builds sentinel: inner on demand. This matches the old text, including the joined multi-error case in tree.go.
  • All 21 call sites pass non-nil values. The ones I checked are in gosqlx.go, lint.go and tree.go.
  • Call sites that add extra context text still use fmt.Errorf, as intended.
  • wrap_ctx_test.go is a good addition. It sweeps the cancellation point and requires ErrTimeout plus context.Canceled, and never ErrTokenize or ErrSyntax.

Earlier review points

  • Pooled-memory lifetime: errors.Error holds only strings, Location, *ErrorContext, Cause and HelpURL. A prior review confirmed this, and I found nothing that changes it. So calling Error() after PutTokenizer/PutParser is safe.
  • Recovery with 2+ diagnostics: wrap_compat_test.go has ParseTreeRecovery, ParseReaderRecovery and ParseWithRecovery subtests. They go through the errors.Join path. I did not confirm that any input in the suite produces two or more diagnostics, so please check that.

Suggestions (non-blocking)

  1. Add a small -race regression test. It would take an error from Parse, run many parses concurrently, then compare err.Error() with the text captured immediately. It would guard against future inner error types holding pooled memory. No current code needs it.
  2. Error() panics on a nil member, where fmt.Errorf would print %!w(<nil>). The doc comment states the non-nil requirement, which is enough for an unexported helper.
  3. Unwrap returns w.errs[:], which is a mutable view of the internal array. The standard library walkers don't write to it, so this is fine.
  4. Error() rebuilds the string on every call. That is the right trade-off for the goal, because caching would add state and cost on the success path.
  5. long_prefix misses the plan target, and ParseError/* has no amd64 CI measurement. The PR discloses both, and the shadow perf gate shows no regression on the success path.

@github-actions

github-actions Bot commented Nov 1, 2026 •

Copy link
Copy Markdown

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


Review of PR #905: lazy wrapErr

I read the changed files and spot-checked the recovery tests. I did not run tests or benchmarks. I'm relying on the passing CI and shadow perf gate.

Verdict: Looks good to merge once the gate-suite follow-up you listed is done. I found no new blocking issues.

Correctness

  • wrapErr stores both errors in a [2]error array. Unwrap() []error matches the two-%w fmt.Errorf contract for errors.Is and errors.As.
  • Error() produces the same sentinel: inner text. Call sites that add extra context still use fmt.Errorf, which is correct.
  • The pooled-memory concern from earlier reviews doesn't apply. errors.Error holds only strings, Location, *ErrorContext, Cause and HelpURL, so calling Error() after PutTokenizer/PutParser is safe.
  • wrap_ctx_test.go closes the mutation gap on the ErrTimeout paths. The Err()-flip approach is a good way to test cancellation mid-tokenize and mid-parse.

Open points

  1. Recovery with 2+ diagnostics. The recovery subtests in wrap_compat_test.go (around lines 266-283) compare against errors.Join(errs...), so the join path is exercised. I could not confirm that any of the 242 inputs yields two or more diagnostics. A multi-error join prints newline-separated text. A one-line assertion len(recoveryInner) >= 2 for at least one input would pin that down.
  2. Mutable Unwrap view. Unwrap returns w.errs[:], an alias of the internal array. Standard walkers don't write to it. Copying would cost an allocation and defeat the purpose, so I'd leave it and keep the doc comment.
  3. Nil members. Error() panics on a nil member, where fmt.Errorf would print %!w(<nil>). All 21 call sites guard against nil, and the doc comment states the requirement. That is enough for an unexported type.
  4. Lazy Error() and mutation. The CHANGELOG notes that Error() now reflects later mutation of the inner error. That is fine. Repeated Error() calls rebuild the string each time, which is the right trade-off for the error-path goal.
  5. Merge blocker (yours). There is still no amd64 CI measurement of BenchmarkParseError/*. Add it to scripts/perf/gate-suite.txt and re-run before merging, as you planned. The materialized-message gain is small (2 allocs and 3-4% B/op), and the PR discloses it.
  6. Optional. A -race test that captures err.Error(), runs many concurrent parses, then compares the text. No current code needs it, but it would guard against future inner error types that hold pooled memory.

ajitpratap0 added a commit that referenced this pull request Nov 2, 2026
#896 (L007), #902 (L010) and #905 (lazy error wrap) target benchmarks the
PR-run gate never measured, so none of them could be accepted or closed
on an amd64 verdict. long_prefix is included so #905's expected miss is
still checked for regressions.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DnbRnTFErhoMJdAjVEeCg2
ajitpratap0 added a commit that referenced this pull request Nov 3, 2026
…w suite (#927)

* ci(perf): add BenchmarkLintRW to the shadow gate suite

PR #906's gate run moved 0 of 26 benchmarks because no linter benchmark was
gated. Add it so W6-05b, W6-06 and W6-07 are measured by the PR-run gate.

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

* ci(perf): gate LintRule L007/L010 and ParseError in the shadow suite

#896 (L007), #902 (L010) and #905 (lazy error wrap) target benchmarks the
PR-run gate never measured, so none of them could be accepted or closed
on an amd64 verdict. long_prefix is included so #905's expected miss is
still checked for regressions.

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Resolve CHANGELOG (and gate-suite) conflicts by placing this PR's entries in main's Unreleased sections.

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 #905: lazy wrapErr

I read the diff for errors.go, tree.go and lint.go, and checked the recovery subtests in wrap_compat_test.go. I did not run tests or benchmarks. I'm relying on CI and the shadow perf gate.

Verdict: Looks good. I found no new blocking issues. The one merge blocker is the one you already listed: the gate-suite follow-up for BenchmarkParseError/*.

Correctness

  • wrapErr keeps both errors in a [2]error array. Unwrap() []error returns a slice of that array. errors.Is and errors.As therefore see the same tree as the two-%w fmt.Errorf, with the sentinel visited first.
  • The Error() text is sentinel: inner, the same as before.
  • Call sites that add context text keep fmt.Errorf, as intended.
  • lint.go is guarded by its switch. The tree.go paths are guarded by err != nil or tokErr != nil. wrapContextErr returns early on nil. In tree.go, joined can't be nil because that branch only runs when errs is non-empty. I only read the tree.go and lint.go hunks, not gosqlx.go.
  • Pooled-memory lifetime: earlier reviews found that errors.Error holds only strings, Location, *ErrorContext, Cause and HelpURL, so calling Error() after PutTokenizer/PutParser is safe. I did not re-verify this.
  • wrap_ctx_test.go closes the mutation gap on the ErrTimeout paths.

Open points (non-blocking)

  1. Recovery with 2+ diagnostics. The recovery subtests in wrap_compat_test.go (lines 266-283) exercise the errors.Join path. ParseWithRecovery compares len(got) with len(recoveryInner). I still can't tell from the code whether any of the 242 inputs produces two or more diagnostics. The %w: %w over a multi-error join prints newline-separated text, which is the case most likely to differ. A one-line assertion that at least one input has len(recoveryInner) >= 2 would pin this down.
  2. Nil members. Error() panics on a nil member, where fmt.Errorf prints %!w(<nil>). The doc comment states the non-nil requirement and the call sites guard against nil. That is enough for an unexported type.
  3. Mutable Unwrap view. w.errs[:] aliases the internal array. The standard walkers don't write to it, and copying would cost an allocation, so I'd leave it.
  4. Optional -race test. It would take an error from Parse, run many parses concurrently, then compare err.Error() with the text captured immediately. This guards against future inner error types that hold pooled memory. No current code needs it.
  5. Lazy Error(). Each call rebuilds the string. That is the right trade-off for the error-path goal, and the CHANGELOG already notes that the text now reflects later mutation of the inner error.

Merge blocker (yours)

  • BenchmarkParseError/* has no amd64 CI measurement. Add it to scripts/perf/gate-suite.txt, re-run the gate, and merge only on a significant gain.
  • The materialized-message gain is small: about 2 allocs/op and 3-4% B/op. long_prefix misses its target. The PR discloses both.
  • The shadow gate is neutral on the success path, so there is no regression there.
    · Branch: perf/w1-03-lazy-error-wrap

@github-actions

github-actions Bot commented Nov 3, 2026 •

Copy link
Copy Markdown

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


Review of PR #905: lazy wrapErr

I read the diffs for errors.go, tree.go and lint.go, and grepped the recovery subtests in wrap_compat_test.go. I did not run tests or benchmarks. I'm relying on CI and the shadow perf gate, which passed.

Verdict: Looks good to merge. I found no blocking issues.

Correctness

  • wrapErr stores both errors in a [2]error array. Unwrap() []error matches the two-%w fmt.Errorf contract for errors.Is and errors.As, with the sentinel visited first.
  • Error() produces sentinel: inner, the same text as before.
  • The call sites I read pass non-nil values:
    • lint.go is guarded by its switch.
    • tree.go is guarded by tokErr/err checks.
    • The errors.Join branch only runs when errs is non-empty.
    • wrapContextErr returns early on nil.
    • I did not read the gosqlx.go hunks.
  • Pooled-memory lifetime: earlier reviews found that errors.Error holds only strings, Location, *ErrorContext, Cause and HelpURL. Calling Error() after PutTokenizer/PutParser is therefore safe. I did not re-verify this.
  • wrap_ctx_test.go closes the mutation gap on the ErrTimeout paths.

Perf

  • ParseError/* improves significantly on amd64 CI. For example, syntax_mid is -37% in time and 21 → 12 allocs.
  • The success-path suite is neutral.
  • The gain is much smaller when Error() is actually called, and the PR already discloses that.
  • long_prefix misses its target, and the PR says so.

Non-blocking suggestions

  1. Recovery with 2+ diagnostics. The recovery subtests (wrap_compat_test.go:266-283) compare against errors.Join output. I still can't tell whether any input yields two or more diagnostics. A multi-error join prints newline-separated text, which is the case most likely to differ. A one-line assertion that at least one input has len(recoveryInner) >= 2 would pin this down.
  2. Nil members. Error() panics on a nil member, where fmt.Errorf prints %!w(<nil>). The doc comment states the non-nil requirement, which is enough for an unexported type.
  3. Unwrap aliasing. w.errs[:] is a mutable view of the internal array. The standard walkers don't write to it, and copying would cost an allocation, so I'd leave it.
  4. Optional -race test. Take an error from Parse, run many parses concurrently, then compare err.Error() with the text captured immediately. No current code needs it.
  5. Coverage gap. The post-success timeout branch in ParseWithTimeout is untested. The PR notes this, and the branch is the same wrap as the covered paths.
    · Branch: perf/w1-03-lazy-error-wrap

@ajitpratap0
ajitpratap0 merged commit f618636 into main Nov 3, 2026
52 checks passed

This branch was successfully deployed

1 active deployment
Preview — 2e9638d9 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 performance

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant