This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
This is a fork of apache/datafusion-sqlparser-rs, an extensible SQL lexer and parser for Rust supporting ANSI SQL:2011 and multiple SQL dialects. This fork contains SYNQ-specific extensions for parsing SQL dialects used in the kernel-cll column-level lineage parser.
# Build the library
cargo build
# Check code compiles without building
cargo check
# Run all tests (preferred method)
cargo nextest run --all-features
# Run tests for a specific dialect
cargo nextest run --test sqlparser_snowflake
cargo nextest run --test sqlparser_postgres
cargo nextest run --test sqlparser_bigquery
# Run specific test by name
cargo nextest run -E 'test(test_name_pattern)'
# Format code (uses default rustfmt settings)
cargo fmt
# Run linter
cargo clippy- Use
cargo run --release --quiet --features json_example --example cli FILE --DIALECT 2>&1 1>/dev/null | grep "Error during parsing"— DEBUG logs flood stderr by default and obscure the actual parse error. - The release CLI (
target/release/examples/cli) is rebuilt independently fromcorpus-runner. After parser edits, runcargo build --release --example clibefore re-running single-file repros — otherwise you're testing the previous build and may report false positives.
cmd > log 2>&1 &in a Bash tool call withrun_in_background: truereturns "completed" immediately because the shell exits while the&-detached child keeps running. Don't trust the completion notification for detached processes — verify withpgrep -f <name>or arm a Monitor that pollspgrep.
Critical: Always profile BEFORE optimizing. Assumptions about bottlenecks are often wrong.
# Profile with corpus-runner on macOS (reliable)
target/release/corpus-runner tests/corpus &
PID=$!
sample $PID 120 -file /tmp/profile.txt
kill $PID
grep "Sort by top of stack" -A 30 /tmp/profile.txt
# Build corpus-runner for testing/profiling
cargo build --release --bin corpus-runnerProfiling lessons:
- Test framework overhead often dominates (0.65s/test in old corpus tests)
- Profile with real workload (corpus-runner), not synthetic benchmarks
- macOS
sampleis more reliable than samply for long-running processes - Look for hot spots in system calls (malloc/free/memmove) not just parser code
- Example from this codebase: 22% time in memory ops, 8% tokenization, only 4% token cloning
Performance patterns:
- Use
rayon::prelude::*and.par_iter()for parallel processing (3-4x speedup on multi-core) Arc<Mutex<Stats>>for shared state,Arc<AtomicUsize>for lock-free counters- Standalone binaries avoid test framework overhead (2,643x faster than libtest-mimic)
std::panic::catch_unwindfor graceful failure handling in parallel code
Tests are organized by SQL dialect in the tests/ directory:
sqlparser_common.rs- Generic/cross-dialect testssqlparser_snowflake.rs- Snowflake-specific testssqlparser_postgres.rs- PostgreSQL-specific testssqlparser_bigquery.rs- BigQuery-specific testssqlparser_clickhouse.rs- ClickHouse-specific testssqlparser_mysql.rs,sqlparser_mssql.rs, etc. - Other dialects
Each test file contains comprehensive parsing tests for dialect-specific syntax.
The corpus-runner binary is a standalone tool for parsing corpus tests without test framework overhead:
# Build the corpus-runner
cargo build --release --bin corpus-runner
# Run all corpus tests (completes in ~7 seconds for 100k files)
# Use RUST_BACKTRACE=1 to get parser call chain in error messages for easier debugging
RUST_MIN_STACK=8388608 RUST_BACKTRACE=1 target/release/corpus-runner tests/corpus
# Run specific dialect directory
RUST_MIN_STACK=8388608 RUST_BACKTRACE=1 target/release/corpus-runner tests/corpus/bigquery
# Compare reports
node scripts/compare-corpus-reports.js target/corpus-report.json target/corpus-results/corpus-report-*.jsonPerformance:
- Processes ~15,000 files/sec on M-series Mac (using 3-4 CPU cores via rayon)
- 99,856 files in 6.7 seconds
- 2,643x faster than old libtest-mimic test harness
- Generates
target/corpus-report.jsonfor CI/CD
Features:
-
Parallel processing with rayon
-
Progress reporting every 1000 files
-
Per-dialect pass/fail stats
-
Handles panics and stack overflows gracefully
-
Same binary used in GitHub Actions (
.github/workflows/corpus.yml) -
Can be used for profiling (same workload as tests)
-
customer_*andsynq_*dialect prefixes automatically stripped (customer_bigquery → bigquery, synq_clickhouse → clickhouse) -
Cannot run corpus-runner on subdirectories — dialect is extracted from first path component relative to corpus root
-
Corpus files are symlinked from
kernel-cll-corpusrepo — commit corpus changes there, not in sqlparser-rs -
Analyze failures: parse
target/corpus-report.jsonwith Python to filter/grouptest_resultsby dialect or error pattern -
Always rebuild before corpus run:
cargo build --release --bin corpus-runner— stale binary produces stale reports -
Refresh baseline after each accepted commit:
cp target/corpus-report.json target/corpus-report-baseline.json. Otherwisecompare-corpus-reports.jscredits old deltas and can hide fresh regressions. -
If the symlinked corpus changes mid-session (someone runs
make processin kernel-cll-corpus — it can add tens of thousands of files), your saved baseline is stale and deltas are meaningless (you'll see bogus +50k/-16k swings). Re-isolate your change:git stashyour edits → rebuild + run corpus →cpbaseline →git stash pop→ rebuild + run + compare. -
compare-corpus-reports.jsonly lists added tests under "New Tests" — deleted/pruned files don't appear there. After a kernel-cll-corpus pipeline run, also checkgit status -sin that repo to see what was removed. -
When the corpus drifts mid-session, trust a file-by-file regression check, not
compare-corpus-reports.jsaggregate deltas. Compare the two reports'test_resultsdicts in Python for keys that wentpass→fail:reg=[k for k in cur if cur[k].startswith('fail') and base.get(k,'').startswith('pass')]. Zeroreg= no regressions even when totals shifted by thousands. -
Pipeline reprocess (
make processin kernel-cll-corpus) takes ~10 minutes for the full corpus. Don't poll — arm a Monitor onwhile pgrep -f pipeline.process; do sleep 15; doneand let it wake you. -
Anonymizer-corruption signature:
's'<word>(the's'placeholder string directly abutting an identifier/keyword, e.g.'s'HOUR,'s'id_5) is unique to anonymizer misalignment. Filter on exactly's'<word>— a broader'<anything>'<word>regex misaligns on multi-string SQL ('foo','bar') and silently deletes hand-written sqlglot fixtures. -
Query-log truncation heuristics (
pipeline/process.py::_looks_truncated) that worked without false positives: trailing punctuation (,/(/=/operator), trailing clause keyword (SELECT/FROM/BY/AS/…), andCASEcount >ENDcount. Removed ~4k Redshift query-log fragments. -
Adding a real dispatch in
parse_createfor a previously-unsupportedCREATE <X>shape can flag new corpus failures: files that slipped through the generic skip-until-semicolon fallback are now actually parsed. Either extend support, accept on a case-by-case basis, or fall back gracefully — but expect the delta. -
This repo is PUBLIC (
getsynq/sqlparser-rs, a fork ofapache/datafusion-sqlparser-rs). Never put customer names, workspace IDs, or internal codenames into commit messages, branch names, PR titles, file names, or function names — even anonymized SQL content must be attributed generically. Every push is mirrored into GH Archive's permanent public dataset, which force-push cannot undo. -
Pulling real SQL from production Clickhouse for regression coverage:
SELECT sql FROM schema.latest_sql_definitions FINAL WHERE workspace='<name>' AND asset_type IN (...).asset_typecodes fromproto/core/types/v1/asset_type.protothat carry SQL bodies parseable by this library:- Snowflake: 508 view, 510 dynamic table, 511 task, 513 materialized view, 514 procedure, 515 function
- Redshift: 805 view, 806 procedure, 807 function
- BigQuery: 105 view
- Databricks: 1805 view · ClickHouse: 1305 view · Postgres: 1605 view · MySQL: 1705 view · MSSQL: 3405 view · Oracle: 3505 view · DuckDB: 2005 view · Trino: 2105 view
Anonymize schemas, tables, columns, function names, and string literals before landing anything as a test file; test file and function names must also be generic (no customer identifier).
Development workflow:
- Use corpus-runner for fast feedback (6.7s vs 2+ hours)
- No need to run via cargo test - it's a standalone binary
- CI uses the same binary (
target/release/corpus-runner tests/corpus) - Report format matches old test harness for compatibility
For profiling:
# Profile for 2 minutes with macOS sample
target/release/corpus-runner tests/corpus &
PID=$!
sample $PID 120 -file /tmp/profile.txt
kill $PID
# View hot spots
grep "Sort by top of stack" -A 30 /tmp/profile.txt- Lexical analysis - converts SQL strings into tokens
- Handles different quote styles, identifiers, keywords, operators
- Dialect-aware tokenization
- Design: Hand-written recursive descent parser
- Expression parsing: Uses Pratt Parser (TDOP - Top-Down Operator-Precedence) for expressions
- Recursion protection:
RecursionCounterprevents stack overflow on deeply nested queries - Main entry point:
Parser::parse_sql(&dialect, sql) - Most parsing logic is in the massive
src/parser/mod.rsfile (~412KB) - ALTER statement parsing is separated into
src/parser/alter.rs
- Complete Abstract Syntax Tree representation of SQL
- All AST nodes implement
Debug,Clone,PartialEq,Eq - Optional features:
serde: Serialize/deserialize AST nodesvisitor: Recursive AST walking via Visitor pattern
- Sub-modules:
data_type.rs- SQL data type definitionsddl.rs- DDL statement structures (CREATE, ALTER, DROP)dcl.rs- DCL statement structures (GRANT, REVOKE, ALTER ROLE)query.rs- Query structures (SELECT, joins, CTEs, window functions)value.rs- Literal values and constantsoperator.rs- Binary and unary operatorsvisitor.rs- AST visitor pattern (whenvisitorfeature enabled)
Each dialect module defines parsing behavior variations:
GenericDialect- Default baseline dialectAnsiDialect- Strict ANSI SQL:2011SnowflakeDialect,PostgreSqlDialect,BigQueryDialect,MySqlDialect, etc.- Note:
customer_*prefixed dialects (e.g.,customer_bigquery) map to their base dialect trinomaps toGenericDialect(parse_dialectinsrc/dialect/mod.rs), so anydialect_of!(self is GenericDialect | MySqlDialect)block also runs for Trino. Gate MySQL-only constructs (e.g. inlineKEY/INDEXtable constraints) so they don't shadow legal Trino identifiers/column names.
Dialects control:
- Quote character handling for identifiers
- Reserved vs. non-reserved keywords
- Custom syntax extensions
- Operator support
Use the dialect_of! macro to check parser's dialect:
if dialect_of!(parser is SnowflakeDialect | BigQueryDialect) {
// Parse Snowflake/BigQuery-specific syntax
}Expression parsing uses operator precedence climbing:
parse_expr()- Entry pointparse_prefix()- Handles prefix operators and primary expressionsparse_infix()- Handles binary operators based on precedence- Precedence levels defined in
get_precedence()
Lookahead and backtracking:
if self.peek_token().token == Token::Keyword(Keyword::FOO) {
self.next_token(); // consume
// ... parse FOO syntax
} else {
self.prev_token(); // backtrack if needed
}Negative lookahead (distinguish between similar patterns):
// Check for absence of keywords to detect non-keyword identifier
if !matches!(self.peek_token().token, Token::Word(w) if w.keyword == Keyword::PARTITION) {
// Parse as identifier, not as PARTITION keyword
}Speculative parse with backtrack: Parser::maybe_parse(|p| {...}) (pub(crate)) runs a closure and reverts self.index on Err (errors discarded). Use for "try the structured grammar, fall back to a general parse" — e.g. Snowflake COPY INTO (...) sources try the StageLoadSelectItem transformation grammar, then fall back to parse_query() for shapes it can't represent ($1[0], NULLIF($1, ...)).
Reserved keyword lists (src/keywords.rs):
RESERVED_FOR_COLUMN_ALIAS- Keywords that can't be column aliases in SELECTRESERVED_FOR_TABLE_ALIAS- Keywords that can't be table aliases in FROM/JOIN- Add clause-level keywords (FORMAT, SETTINGS, SAMPLE) to BOTH lists to prevent incorrect alias parsing
Dialect conflicts (same keyword, different syntax):
- Problem: Keyword parsed in multiple locations (e.g., SAMPLE as table factor vs SELECT clause)
- Solution: Use
dialect_of!to exclude conflicting dialects from one parsing location - Example: ClickHouse
SAMPLE n(clause) vs SnowflakeSAMPLE (n)(table factor) - exclude ClickHouse from table factor parsing
ParserError construction:
ParserError::ParserError wraps ParserErrorMessage (not a raw String). Use .into() to convert:
ParserError::ParserError(format!("msg {x}").into())
ParserError::ParserError("literal message".into())This applies to test assertions too. The parser_err! macro handles conversion automatically.
Balanced paren consumption (opaque clause parsing):
// Consume MATCH_RECOGNIZE(...), CODEC(...), BEFORE(...), etc. without deep AST support
self.expect_token(&Token::LParen)?;
let mut depth = 1i32;
while depth > 0 {
match self.next_token().token {
Token::LParen => depth += 1,
Token::RParen => depth -= 1,
Token::EOF => break,
_ => {}
}
}Use this pattern for dialect-specific clauses that don't need AST representation.
Unsupported / opaque statement nodes (prefer over masquerades): Unmodelled DDL returns honest nodes that preserve the raw body, NOT a fake Comment/SetVariable: CreateUnsupported/DropUnsupported/AlterUnsupported { object_type, body } and UnsupportedStatement { keyword, body } (SYSTEM/LOCK/…). Helpers in src/parser/mod.rs: skip_to_statement_end() (paren-aware skip to ;/EOF), consume_statement_body_text() (raw text capture — filters Token::Whitespace, else joined strings get stray spaces), parse_create_body_statement() (parse a body as a Statement, but revert + skip if it doesn't fully consume to ;/EOF — guards against partial parses like EXECUTE DBT PROJECT), and parse_function_body_definition() (shared $$…$$/'…' body capture with correct body_start, used by CREATE PROCEDURE and DO).
Reserved-keyword-as-alias carve-out (recurring fix shape):
- When a keyword (e.g. CLUSTER, SORT, FINAL, AT, BEFORE) is reserved only because of a specific clause (
CLUSTER BY,t AT(...)time-travel), accept it as an identifier alias inparse_optional_aliaswhen the lookahead doesn't match the clause shape (next-not-BY, next-not-(, etc.). - Existing instances in
parse_optional_aliasfor CLUSTER / SORT / FINAL / VIEW / OPTION / USE/IGNORE/FORCE inparse_table_factorfor AT / BEFORE serve as templates — copy the matching block rather than reinventing. - Match-arm ordering trap: the catch-all
Token::Word(w) if after_as || !reserved_kwds.contains(&w.keyword)arm fires first for any non-reserved keyword. New carve-outs for dialect-specific clauses (e.g.OPTION (,USE INDEX) must be placed before it or they silently never run.
Trailing-comma support lives in is_parse_comma_separated_end (src/parser/mod.rs:~4280). Extending the set of clauses where a trailing , is tolerated (FROM list, IN list, …) goes there, or in the per-clause loop's bespoke check (e.g. parse_from_clause_body's Snowflake terminator set).
Prefer WithSpan<Ident> / WithSpan<Expr> in new AST fields:
parse_identifier(in_table_clause)? returns WithSpan<Ident>. Keep that wrapping in new AST nodes so kernel-cll lineage can surface source positions. Reach for parse_identifier_no_span() only when there's a specific reason not to carry the span (e.g. matching an existing surrounding type that's still plain Ident). For new Expr fields, wrap with the standard idiom: let start_idx = self.index; let expr = self.parse_expr()?; expr.spanning(self.span_from_index(start_idx)) — see selection in parse_select for the template. span_from_index anchors its end at self.index - 1 (the last consumed token), so the trailing ) of an enclosing (...) is correctly excluded.
No peek_keyword:
For one-token keyword lookahead, use matches!(self.peek_token_kind(), Token::Word(w) if w.keyword == Keyword::FOO). For fixed-length sequences use peek_keywords::<N>() -> [Keyword; N]. There is no peek_keyword(Keyword::FOO) -> bool.
Match non-keyword words case-insensitively, don't add to keywords.rs:
Words that appear only in narrow constructs (e.g. SEMANTIC, EXCLUDING, ADDITIVE, SYNONYMS, CORTEX) shouldn't go in ALL_KEYWORDS — adding them risks breaking identifier usage and the reserved-alias lists. Match with Token::Word(w) if w.value.eq_ignore_ascii_case("FOO") (or the peek_word_ci / parse_word_ci helpers in the SEMANTIC VIEW parser).
Tokenizer Number("N.") quirk:
The tokenizer greedily folds a trailing . into the number token, so proj-NNN.dataset produces Number("NNN.") followed by Word("dataset"). To consume the digit prefix as part of a hyphenated identifier and keep the dot as a separator, mutate the just-consumed token in place: self.tokens[idx].token = Token::Period; then prev_token() so the surrounding parse_object_name loop continues. See the BigQuery hyphenated project-ID block in parse_identifier for the template.
Dialect struct names (for dialect_of! macro):
RedshiftSqlDialect(NOTRedshiftDialect),AnsiDialect,DuckDbDialectClickHouseDialect,SnowflakeDialect,BigQueryDialect,PostgreSqlDialectMySqlDialect,MsSqlDialect,SQLiteDialect
Dialect-specific AST node names match the dialect-struct casing: DuckDb (not Duckdb), ClickHouse, BigQuery, MsSql, MySql — e.g. DuckDbLoad, DatabricksMap, CopyIntoSnowflake. Prefix vs suffix is inconsistent across existing nodes; match the casing, pick whichever reads better.
This parser is syntax-only - it does NOT perform semantic validation. For example:
CREATE TABLE(x int, x int)parses successfully (duplicate column names)- Type checking is not performed
- Schema validation is not done
Semantic analysis varies drastically between SQL dialects and is left to consumers of this library.
- Update AST (
src/ast/*.rs): Add new AST node structures if needed - Update Parser (
src/parser/mod.rs): Add parsing logic - Add Tests: Write dialect-specific tests in appropriate test file
- Consider Dialect: Use
dialect_of!if syntax is dialect-specific
Keywords in src/keywords.rs MUST be in strict alphabetical order — ALL_KEYWORDS uses binary search.
If a keyword is out of order, the tokenizer silently fails to recognize it (maps to Keyword::NoKeyword).
Verify ordering carefully: e.g., EXCHANGE < EXCLUDE < EXEC (compare character by character).
When adding fields to AST structs, you must update ALL pattern matches:
- Add field to struct definition (e.g.,
src/ast/mod.rs,src/ast/query.rs) - Update Display implementation to output new field
- Update parser to initialize new field
- Fix all test files - add
new_field: _to pattern matches (Rust errors E0027, E0063 guide you) - Use
cargo checkto find all locations requiring updates
High-churn structs: Function struct is constructed in ~30+ places across parser and test files. Adding a field requires updating all of them — use replace_all or agent assistance.
Multi-line sed on macOS: sed -i '' operates line-by-line and silently skips multi-line patterns. For bulk edits that span newlines (e.g. appending a field after a params,\n }) block) use perl -i -0pe '…' instead.
Recursive AST types: New fields containing Expr inside Function/Expr cycle need Box<Expr> to break infinite size (e.g., HavingBound(Box<Expr>)).
ObjectName uses Vec<Ident> not Vec<WithSpan<Ident>> — don't wrap idents in WithSpan when constructing manually.
DECLARE has 5 dialect-specific branches in parse_declare (Snowflake block, Databricks variable+cursor, BigQuery variable-list, T-SQL @var [AS] type [= expr], Postgres-style cursor). Order matters: each branch returns early; a later branch never runs if an earlier one matched. When adding a new dialect, gate by dialect_of! and by a discriminator that won't false-match the others (e.g. T-SQL gates on name.value.starts_with('@'), Databricks falls through to the cursor path when peek == CURSOR).
Variable-length type lengths are split: parse_optional_character_length (used by VARCHAR / CHAR — accepts MAX and unit suffixes) vs parse_optional_precision (used by NVARCHAR / VARBINARY / TIMESTAMP / etc. — Option<u64>, accepts MAX as None). When a new type accepts MAX, pick the right helper or extend it; don't write a third one.
Since this is a fork of apache/datafusion-sqlparser-rs:
- Avoid creating new AST node types when possible
- Prefer parameterizing existing AST nodes for compatibility
- Document SYNQ-specific extensions clearly
- Consider if changes should be contributed upstream
- All PRs must include tests - PRs without tests will not be reviewed
- Test both success and error paths
- Use
pretty_assertionsfor readable diffs - Run
cargo fmt,cargo clippy,cargo nextest runbefore submitting - CI runs:
cargo check,cargo nextest run --all-features cargo nextest runwithoutRUST_MIN_STACK=8388608always SIGABRTs three tests (parse_deeply_nested_expr_hits_recursion_limits,..._parens_...,..._subquery_...). These are stack-size probes, not regressions — ignore if the rest of the run is green.- Avoid adding serde dependency to test code - use manual JSON building if needed
verified_stmt(s)requiressto be a full statement that round-trips (parse → display → equal). For fragment / coverage tests on a non-statement (a bare CAST, a SELECT-list snippet), wrap inSELECTor callparse_sql_statements(input).unwrap()directly —verified_stmtwill otherwise fail withExpected an SQL statement, found: ....- Spans are dummy (
Locationline/col 0) underParser::parse_sql/verified_stmt/ thecliexample — they callwith_tokenswhich installsSpan::default(). To assert realWithSpan/body_startlocations, build the parser explicitly:Parser::new(&dialect).with_tokens_with_locations(Tokenizer::new(&dialect, sql).tokenize_with_location().unwrap()). Seeparse_sql_with_locationsintests/redshift_customer_procedure_samples.rs. one_statement_parses_to(sql, canonical)re-parsescanonicaland asserts full AST equality. If your node'sDisplayreconstructs to text that re-parses through a different code path (e.g.FOREIGN TABLE→CREATE EXTERNAL TABLE, which setshive_formatsdifferently), it fails on a field mismatch — useparse_sql_statements+ manual field asserts instead.
Running corpus tests (now fast!):
# Run full corpus test suite (~7 seconds for 100k files)
target/release/corpus-runner tests/corpus
# Or use the benchmark suite for micro-benchmarking
cargo benchWithout profiling data, optimizations are guesswork. Benchmarks measure total time but don't show WHERE time is spent.
Profiling workflow (macOS):
# 1. Create a simple profiling binary (benches/simple_profile.rs):
# - Loop parse operations 10,000+ times
# - Add [[bin]] section to Cargo.toml pointing to it
# 2. Build in release mode
cargo build --release --bin profile_parse
# 3. Profile with samply (install: cargo install samply)
samply record target/release/profile_parse
# Opens Firefox Profiler UI at http://127.0.0.1:3000
# Shows call stacks, hot functions, time distribution
# Alternative: cargo-instruments (install: brew install cargo-instruments)
cargo instruments --bin profile_parse --release --template time
# Note: May fail with Xcode symbol errors on some macOS versions
# If it works, opens .trace file in Instruments.app
# Fallback: Use Instruments directly
xcrun xctrace record --template 'Time Profiler' \
--output profile.trace \
--launch -- target/release/profile_parse
# Open with: open profile.traceProfiling on Linux:
# Use perf (better than macOS tools)
cargo build --release --bin profile_parse
perf record -g target/release/profile_parse
perf report
# Or generate flamegraph:
cargo flamegraph --bin profile_parseBenchmarking with criterion:
# Add to Cargo.toml dev-dependencies: criterion = { version = "0.5", features = ["html_reports"] }
# Create benches/benchmark_name.rs with criterion_group! and criterion_main!
# Add [[bench]] section to Cargo.toml with harness = false
cargo bench --bench benchmark_name # Run benchmarks
cargo bench --bench benchmark_name -- --save-baseline name # Save baselineImportant caveats:
- Profile FIRST, optimize SECOND - Don't guess where bottlenecks are
- Rust compiler can inline and optimize stack allocations, but cannot eliminate heap allocations
Tokenenum containsStringfields - clones require heap allocation- Benchmarks show TOTAL time, profiling shows WHERE time is spent
- Small improvements (3-5%) may not be worth code complexity without profiling data
Bulk code transformations:
# Always backup before bulk sed operations
cp src/parser/mod.rs src/parser/mod.rs.backup
# Use sed carefully - test incrementally
sed -i '' 's/pattern/replacement/g' src/parser/mod.rs
cargo check # Verify after each transformation[features]
default = ["std"]
std = [] # Standard library support
serde = [...] # Serialize/deserialize AST
visitor = [...] # AST visitor pattern
json_example = [...] # JSON output in CLI exampleThe crate includes a CLI for parsing SQL and dumping JSON:
cargo run --features json_example --example cli queries/example.sql
cargo run --features json_example --example cli queries/example.sql --snowflake
# Dialect flag is the dialect name as a `--<name>` token: --bigquery, --snowflake,
# --redshift, --postgres, --clickhouse, --duckdb, --mssql, --mysql, --hive, --sqlite,
# --ansi, or --generic (default). Not `--dialect <name>`.This is a known Xcode Instruments issue on some macOS versions. Use samply instead.
- "duplicate key" error: Check for multiple
[dependencies]sections (merge them) - Ensure dev-dependencies use correct section name (not
[dev-dependencies]twice)
- samply may not generate output on timeout - use macOS
samplecommand instead - Test framework overhead dominates profiles - use corpus-runner for real workload
- Profile shows mutex locks: test harness coordination overhead, not parsing work
- Always check actual numbers: "25k files/sec" during warmup ≠ sustained 15k files/sec
- Measure complete runs, not just initial progress reports
- Use
timecommand to verify total execution time matches reported rate
- Exits cleanly after 5 minutes without a new commit (interpreted as "no viable tier-1 fix left", not a bug). Restart manually to resume.
- Commits land locally only — CI (
corpus.yml) and the PR corpus-report comment refresh only when someone pushes the branch. - Prompt lives in
.claude/fix-corpus-loop.md; tier-1 =unparsed_*+customer_*only, tier-2/3 patterns are explicitly out of scope.
- Custom SQL Parser Guide - How to write dialect extensions
- Fuzzing - Fuzz testing setup
- Benchmarking - Performance benchmarks
- Upstream Repository - Apache DataFusion SQL Parser