Repository navigation
perf: repo-wide optimization audit (2026-10-11) — tracking issue #566
Description
Summary
Tracking issue for the repo-wide code optimization audit run on 2026-10-11 against main at 31b5b1c (v1.14.0). Nine package-group finders (tokenizer, parser, AST and a pprof-driven pass on Opus; the rest on Sonnet) proposed 80 findings. A per-group adversarial verifier re-read every cited line, re-ran or wrote benchmarks, and refuted 24; 1 was already covered by #460. The 54 confirmed findings were clustered into the 22 issues below. Every issue cites current file:line references and measured numbers.
Headline measurements (Apple M4, prototypes in a scratch copy, not committed)
| Path | Today | Prototype |
|---|---|---|
| Tokenize 1 MB input | 1.74 s | 23-29 ms (#544) |
| Tokenize 1k-element IN list | 4.9 ms | ~90 µs (#544) |
| Tokenize ComplexSQL | 10.0 µs / 159 allocs | ~6 µs / 13 allocs (#544 #545 #546 #549) |
| Pooled parse, realistic join | 4.1 µs / 80 allocs / 11 KB | 2.75 µs / 42 allocs / 3.5 KB (#551 #552 #553 #554) |
| Any dialect-aware call | +20-25 µs, +43 KB overhead | removed (#550) |
Issues by area, in suggested landing order
Tokenizer (hot path)
- perf(tokenizer): make toSQLPosition O(1) amortized to remove quadratic tokenization #544 P1/S toSQLPosition O(1) amortized. Standalone, largest single win, also closes a long-line CPU-DoS vector.
- perf(tokenizer): replace per-identifier strings.ToUpper with allocation-free ASCII upper-casing #545 P1/S allocation-free ASCII upper-casing in readIdentifier.
- perf(tokenizer): add ASCII lookup-table fast path to identifier character classification #547 P1/S ASCII lookup table for identifier classification.
- perf(tokenizer): eliminate per-token Word and string-copy allocations (nil Word for bare words, substring token values) #546 P1/M nil Word for bare words + substring token values. Stacks on perf(tokenizer): replace per-identifier strings.ToUpper with allocation-free ASCII upper-casing #545. Public-semantics change, needs maintainer decision.
- perf(tokenizer): stop rebuilding keyword maps on every dialect-aware call; pool dialect tokenizers and parsers #550 P1/M stop rebuilding keyword maps per dialect-aware call; pool dialect tokenizers/parsers.
- perf(tokenizer): fast-path escape-free quoted identifiers/strings and remove the unused pooled buffer from Tokenize #548 P2/M escape-free quoted identifier/string fast path; settle the unused bufferPool.
- perf(tokenizer): fix the len/4 token-slice capacity estimate (regrows on dense SQL, over-allocates on typical SQL) #549 P2/M fix the len/4 token-slice capacity estimate.
Parser (hot path)
- perf(parser): remove allocations from preprocess keyword remapping (toUpper) and compound-token expansion #551 P1/S allocation-free preprocess toUpper + compound expansion.
- perf(parser): reuse the first Identifier for qualified column refs instead of allocating a second #553 P1/S reuse first Identifier for qualified refs.
- perf(parser): reuse a parser-owned preprocess buffer and the pooled AST Statements slice #552 P1/M parser-owned preprocess buffer + reuse pooled Statements slice. Pairs with perf(parser): remove allocations from preprocess keyword remapping (toUpper) and compound-token expansion #551 and perf(tokenizer): fix the len/4 token-slice capacity estimate (regrows on dense SQL, over-allocates on typical SQL) #549.
- perf(parser): replace fmt and strings.ToUpper in clause parsing with allocation-free equivalents #554 P2/S drop fmt/ToUpper in clause parsing.
AST (pools and visitors)
- perf(ast): fix release-path defects: double Put via JoinClause.Left, incomplete node reset, unbounded work-queue clearing #555 P1/M release-path defects: double Put via JoinClause.Left, incomplete reset, unbounded work queue. Prerequisite for perf(ast): stop ReleaseAST Putting node types the parser never draws from (or make the parser draw them) #556 option (a).
- perf(ast): stop ReleaseAST Putting node types the parser never draws from (or make the parser draw them) #556 P1/M stop ReleaseAST Putting node types the parser never draws (or make the parser draw them).
- perf(ast): stop heap-copying nodes in Children() so Walk/Inspect allocate less #557 P2/M stop heap-copying nodes in Children(). Also fixes visitors mutating copies.
- perf(ast): make SQL() serialization allocation-light (drop fmt/ToUpper, fix builderPool, writer-based rendering) #558 P2/L allocation-light SQL() serialization.
Higher-level packages
- perf(gosqlx): make SplitStatements slice the source instead of copying every byte through strings.Builder #559 P2/S SplitStatements slices the source.
- perf(gosqlx): extract metadata in one AST pass and key qualified collectors by struct #561 P2/M single-pass ExtractMetadata.
- perf(security): hoist per-call maps in ScanSQL and add verified literal prefilters ahead of regex groups #563 P2/M security ScanSQL: hoist maps, literal prefilters.
- perf(lsp): stop re-splitting and copying document text per request; hoist hover docs map; single-write sendMessage #564 P2/S LSP: stop re-splitting document text per request.
- perf(schema): remove N+1 queries and nested open cursors from the Postgres/MySQL/SQLite schema loaders #565 P2/M schema loaders: remove N+1 queries and nested cursors (also a connection-pool deadlock risk).
- perf(gosqlx): release ASTs and use the parser pool in batch helpers, transform parse helpers and Transpile #560 P3/S release ASTs / use parser pool in batch, transform, Transpile. Pays off more after perf(ast): stop ReleaseAST Putting node types the parser never draws from (or make the parser draw them) #556.
- perf(linter): build the default rule set once in gosqlx.Lint and drop the full-SQL ToUpper in SelectIntoOutfileRule #562 P3/S build linter rule set once; drop full-SQL ToUpper in SelectIntoOutfileRule.
Already tracked
- Metrics pool counters take a global RWMutex + map lookup on every pool Get/Put (
pkg/metrics/pool_stats.go). Covered by P3: Pool statistics and monitoring API — expose hit rates and memory savings #460, which is closed; the verifier measured ~99 ns/op for RecordNamedPoolGet vs ~37 ns for a cached counter. Reopen P3: Pool statistics and monitoring API — expose hit rates and memory savings #460 or file a follow-up if that overhead matters.
Refuted or dropped (not filed, for the record)
- LSP synchronous message loop: a design change, not a perf fix; risks ordering bugs vs didChange.
- Missing ReleaseAST in cbinding, schema validators, advisor, linter.LintString: real, but measured gain is noise and some paths may retain node pointers.
- LSP completion ToUpper/ToLower per item: no allocation, Go's ASCII fast path returns the original string.
- Config cache os.Stat + Clone per lookup: deliberate invalidation and mutation protection, cold path.
- WASM MarshalIndent and build flags: output contract and build-size items, not runtime hot path.
- Linter per-rule AST walks and per-line re-tokenizing: plausible but unmeasured; would change public Rule/Context API.
- Formatter nodeFormatter/Builder per nested call, transpiler per-rule passes, FromSQL re-parse: cold or dominated by parse cost.
- TableReference field reordering, Fingerprint %x formatting, renderSelect shallow copies: negligible.
- Validate's TrimSpace + []byte copy: the proposed unsafe view would break aliasing guarantees.
- Global spanInfo map (ast/span.go:77): unsynchronized and unbounded, but no production caller writes to it.
Process
Each child issue has acceptance criteria: the named benchmarks must improve or not regress (benchstat, -count=6), task test:race passes, pooling invariants hold, and performance_baselines.json is updated when a baseline moves. Please benchmark before and after on the same machine and paste benchstat output in the PR.
Generated by an automated optimization audit (9 finders, 9 adversarial verifiers, 1 synthesizer) on 2026-10-11.
Activity
ajitpratap0 commentedon Oct 11, 2026
Fix status (2026-10-11)
Fixes were implemented by a multi-agent workflow (Fable plans and validates; Opus/Sonnet implement and review). Every PR below passed the full race gate (
go test -raceon all packages except cbinding),go vet,golangci-lint, an independent code review, and a benchstat comparison; branches deliberately do not touch CHANGELOG.md or performance_baselines.json so they merge without conflicts. One consolidated CHANGELOG/baselines PR should follow once these land.Deferred by maintainer decision (not started): #546, #548, #550 (the tokenizer-2 chain, stacked on #577), #552, #557. Branch
perf/tokenizer-zero-copy-and-dialectexists at the #577 head with no own commits.Note: external PR #576 (fold identifier case without per-word allocation) overlaps #545, which is included in #577; one of the two should be preferred.
docs(changelog): restore CHANGELOG.md emptied by the merge in #526 (#593
ajitpratap0 commentedon Oct 13, 2026
Status update 2026-10-13 (batch 2)
Batch 1 (#544 #545 #547 #549 #551 #553 #554 #555 #556 #558 #559 #560 #561 #562 #563 #564 #565) is fully merged (PRs #568–#575, #577, #578, #581).
Batch 2 PRs, each with an independent Opus review and validation (full race gate, vet, lint, benchstat A/B before/after, golden/differential tests where output equivalence matters):
#595 is the largest: #520 (Oracle
:1/:namebinds), #546 (substring token values, slab Words,TokenizeString), #548 (zero-copy escape-free quoted literals, 400+ → 2 allocs on quote-heavy input), #550 (drop the never-read keyword table; dialect-aware entry points now cost the same as the default path, e.g.ParseBytesWithDialect70 → 28 allocs). It also drops the token slice inParser.Resetso a pooled parser no longer retains the previous caller's SQL string.Side findings while doing this work:
gosqlx formatnil-pointer panic onNOT EXISTS), [BUG] SQL() and formatter drop numeric window-frame bounds (ROWS BETWEEN 1 PRECEDING ... renders as ROWS BETWEEN PRECEDING ...) #589 (SQL()/formatter drop numeric window-frame bounds), [BUG] Pooled parser (GetParser) reports different Capabilities() than NewParser() for the default dialect #590 (GetParser()capabilities are zero whileNewParser()is permissive).CHANGELOG.mdto zero bytes; restored (merged)..gitattributesnow enforces LF (tests in perf(ast): stop heap-copying nodes in Children() so Walk/Inspect allocate less (#557) #588/perf(tokenizer): Oracle bind variables, substring token values, zero-copy quoted literals, dialect pooling (#520, #546, #548, #550) #595 also normalize as belt and braces).Still owed: one consolidated PR updating
CHANGELOG.mdandperformance_baselines.jsonfor all merged perf PRs (branches deliberately did not touch either).ajitpratap0 commentedon Oct 13, 2026
Status update (2026-10-13): merge train complete
All open PRs from this effort are merged. Each was rebased onto the current main, passed the full race test suite locally, went green on every CI check (including Vercel), and had its review comments answered. Each was then squash-merged.
Merged in this train
.gitattributeswith LF line endings, fixing Windows CRLF golden failuresgosqlx formatkeeps leading CTEs on INSERT, UPDATE and DELETEgosqlx format(#531)#597 contains:
MaxTokens$nameplaceholders keeping their textConsolidated docs: #598 adds CHANGELOG
[Unreleased]entries for everything since v1.14.0, starting with a Behavior changes section, and refreshesperformance_baselines.json.Follow-up issues found during review were filed as #599–#620. Each was reproduced on main 61ca6c8, and none duplicates an existing issue.
gosqlx formatsilently corrupts many expressions; switch the CLI to pkg/formatter #599: umbrella issue; movegosqlx formatontopkg/formatterNOT (EXISTS (...) NOT ), which does not re-parse #601, formatter: readable style emits;inside CTE/IN/EXISTS subqueries and set operations, and;;after INSERT ... SELECT / CREATE VIEW #602, parser/ast: JOIN ... USING (cols) is stored as a plain condition and renders asON cols#603, ast/formatter: SQL Server table hints WITH (NOLOCK) are never rendered; SQL() also drops PIVOT #604, ast/formatter: SQL() and FormatAST drop table-function call arguments in FROM (generate_series(1,10) -> generate_series) #607, ast/formatter: named windows render as OVER (w) ... WINDOW w AS (w ...) and do not round-trip; golden file pins the broken output #608, ast/formatter: quoted identifiers that are keywords or case-sensitive lose their quotes on render (SELECT "select", CTE/view column lists, table names) #611Also open from earlier: #596 (arithmetic on the right-hand side of a comparison).
docs: consolidate CHANGELOG for the perf audit and batch-2 fixes; ref…
docs: consolidate CHANGELOG for the perf audit and batch-2/batch-3 fi…
ajitpratap0 commentedon Oct 14, 2026
Batch 3 (#599–#620) is complete
All 22 issues are fixed in 12 PRs, merged through one merge train. Every PR got an Opus adversarial and security review before merge, and the fix commits were independently validated by Opus.
The CHANGELOG and refreshed baselines are in #598 (merged as d7338ce).
Security: the review of #632 found render-time SQL injection, and main had it too. A quoted alias such as
"a; DROP TABLE x; --"came back fromgosqlx.FormatandSQL()as two statements. #632 closes the whole class:It was validated with hostile-name fuzzing (549k cases) and AST-mutation fuzzing: 0 injected statements and 0 injectable paths.
Follow-ups found during batch 3, not yet filed
Per the earlier instruction not to add new issues for now, these are recorded here rather than filed. Each one is already on main.
Rendering and round-trip, all fail-safe: the output does not re-parse, or the CLI refuses it
CROSS APPLY/OUTER APPLYrender asCROSS APPLY JOIN …, andTOPis lost in the derived table.MATCH (…) AGAINST (…)renders asAGAINST AGAINST(…).START WITH/CONNECT BYandQUALIFYare not rendered.ALTER TABLE … ADD CONSTRAINTrenders as an empty string fromSQL()and asALTERfrom the formatter.ALTER ROLE/POLICY/CONNECTORare not rendered.ALTER TABLE … ADD COLUMN IF NOT EXISTS/DROP COLUMN IF EXISTSare not parsed. The AST fields exist.SQL()dropsORDER BYafterENGINE = MergeTree(), the formatter drops ARRAY JOIN, andINTO OUTFILEafter SETTINGS is not rendered.CREATE TABLE t AS SELECT …renders asCREATE TABLE t (), and SnowflakeCREATE SEQUENCErenders as an empty string. SnowflakeCREATE STAGERawSQL is lost.fn()(x)rendering, JOIN-side table functions, andINSERT … SET t.a/= DEFAULT.SETform when transpiling.Quoting and dialects
"…"everywhere. MySQL output needs backticks unlessANSI_QUOTESis set. The CLI now refuses rather than emit wrong MySQL."MixedCase") renders bare, because the AST does not record quoting for aliases or column lists. RecordingQuotedeverywhere would let case-sensitive names round-trip."#x"renders bare and does not re-parse.'a\b'becomes'a\\b'in SQL Server, Oracle, SQLite, Snowflake and ClickHouse. The MySQL tokenizer does not handle\"inside double-quoted strings.any(...)turns into ALL.Security scanner
API, schema and linter
cfg.Ruleswarning order in the linter config.CreateTableBuilder.Build()sharesColumns/Constraintsstorage between built statements. Since ast: CreateTable, FunctionDesc, TriggerExecBody and TriggerReferencing return child nodes by value from Children() #617,Inspectedits propagate across them.TableReferencepadding: +16 B/op from parser: parenthesized set operations not accepted as derived tables or statements; ((SELECT ...)) t gives a misleading join-group error #612.Identifiergrew from 48 to 64 B.CI
TestFormatAST_SetOperationChainLinearTimeuses a 2 s wall-clock limit. It failed once on a starved windows-latest runner (4.6 s) alongside unrelated lsp/security timeouts. A rerun passed. It should use a relative or operation-count bound.sprintf-js) is untouched.ajitpratap0 commentedon Oct 18, 2026
Status update for the issues tracked under this one:
Issue #566 stays open.
ajitpratap0 commentedon Oct 18, 2026
Follow-up outcomes for #781-#784 (found while fixing #596/#590):
gh pr update-branchtwice to catch up with main. Merged while Vercel was still deploying on the final head; not re-checked after merge.gh pr update-branch. All checks passed, Vercel passed.parser.goParser-struct conflict by keeping both sides. Limited one test's concat expectation to Oracle, since main now makes||logical OR in MariaDB by default. Merge commit 6c0cbe5 pushed to the PR branch; all checks passed.All four issues are closed. No admin merges, force-pushes, or CHANGELOG edits were made, and no scratch worktrees remain from this work.