An SQL parser for QuestDB syntax, built with Chevrotain. Parses SQL into a fully typed AST, converts AST back to SQL, and provides context-aware autocomplete — all in a single package.
- Complete QuestDB SQL coverage — 45 statement types including SELECT, INSERT, UPDATE, CREATE TABLE, ALTER TABLE, GRANT/REVOKE, COPY, PIVOT, and more
- QuestDB-specific syntax —
SAMPLE BY,LATEST ON,ASOF JOIN,LT JOIN,SPLICE JOIN,WINDOW JOIN, implicit SELECT, duration literals, geohash types - Lossless round-trip — parse SQL to AST and convert back with
toSql(). Verified against 1,726 real-world queries from QuestDB documentation with full round-trip coverage - Context-aware autocomplete — schema-driven suggestions for tables, columns, keywords, functions, and operators at any cursor position
- Fully typed — complete TypeScript type definitions for the entire AST
- Single dependency — only Chevrotain at runtime
import { parseToAst } from "@questdb/sql-parser";
const result = parseToAst("SELECT * FROM trades WHERE symbol = 'BTC-USD'");
if (result.errors.length === 0) {
console.log(result.ast[0]);
// {
// type: "select",
// columns: [{ type: "star" }],
// from: [{ type: "tableRef", table: { type: "qualifiedName", parts: ["trades"] } }],
// where: {
// type: "binary", operator: "=",
// left: { type: "column", name: { type: "qualifiedName", parts: ["symbol"] } },
// right: { type: "literal", value: "BTC-USD", literalType: "string" }
// }
// }
}import { parseToAst, toSql } from "@questdb/sql-parser";
const result = parseToAst("SELECT avg(price) FROM trades SAMPLE BY 1h FILL(PREV)");
const sql = toSql(result.ast);
// "SELECT avg(price) FROM trades SAMPLE BY 1h FILL(PREV)"import { parseOne } from "@questdb/sql-parser";
const stmt = parseOne("INSERT INTO trades VALUES (now(), 'BTC', 42000.50)");
console.log(stmt.type); // "insert"import { parseStatements } from "@questdb/sql-parser";
const statements = parseStatements(`
SELECT * FROM trades
INSERT INTO logs VALUES (1)
CREATE TABLE orders (id LONG, ts TIMESTAMP) TIMESTAMP(ts) PARTITION BY DAY WAL
`);
console.log(statements.length); // 3import { format } from "@questdb/sql-parser/formatter";
format("SELECT * FROM trades WHERE symbol = 'BTC-USD' LATEST ON ts PARTITION BY symbol");
// SELECT *
// FROM trades
// WHERE symbol = 'BTC-USD'
// LATEST ON ts PARTITION BY symbolThe formatter is token-based and never throws on SQL content. It preserves every token, comment, and unknown character, changes whitespace only, and leaves unterminated or unbalanced input verbatim from the point of the problem.
Options: indent (default two spaces), maxWidth (default 50), and capitalize (default false).
format(
"create table tab (s symbol index type bitmap, timestamp timestamp) timestamp(timestamp) partition by day wal",
{ capitalize: true },
);
// CREATE TABLE tab (s SYMBOL INDEX TYPE BITMAP, timestamp TIMESTAMP)
// TIMESTAMP(timestamp) PARTITION BY DAY WALWith capitalize, the formatter parses the statement and raises only the words the grammar read as syntax, so a keyword that names a table, view or column keeps its case, as timestamp does above. Most QuestDB keywords are non-reserved and double as names, which is why this needs the grammar. SQL the parser cannot read comes back with its case untouched, and the layout is the same either way.
import { createAutocompleteProvider } from "@questdb/sql-parser";
const provider = createAutocompleteProvider({
tables: [
{ name: "trades", designatedTimestamp: "ts" },
{ name: "orders" },
],
columns: {
trades: [
{ name: "ts", type: "TIMESTAMP" },
{ name: "symbol", type: "SYMBOL" },
{ name: "price", type: "DOUBLE" },
],
orders: [
{ name: "id", type: "LONG" },
{ name: "created_at", type: "TIMESTAMP" },
],
},
});
const suggestions = provider.getSuggestions("SELECT * FROM t", 16);
// [{ label: "trades", kind: "Table", insertText: "trades", ... }, ...]| Category | Statements |
|---|---|
| Query | SELECT, implicit SELECT, PIVOT, EXPLAIN |
| DML | INSERT, INSERT ATOMIC, INSERT BATCH, UPDATE |
| DDL | CREATE TABLE, ALTER TABLE, DROP TABLE, TRUNCATE TABLE, RENAME TABLE |
| Views | CREATE VIEW, ALTER VIEW, DROP VIEW, COMPILE VIEW |
| Materialized Views | CREATE MATERIALIZED VIEW, ALTER MATERIALIZED VIEW, DROP MATERIALIZED VIEW, REFRESH MATERIALIZED VIEW |
| Access Control | CREATE/ALTER/DROP USER, CREATE/ALTER/DROP GROUP, CREATE/ALTER/DROP SERVICE ACCOUNT, ADD/REMOVE USER, ASSUME/EXIT SERVICE ACCOUNT |
| Permissions | GRANT, REVOKE, GRANT/REVOKE ASSUME SERVICE ACCOUNT |
| Operations | COPY FROM/TO, BACKUP, VACUUM TABLE, REINDEX TABLE, CANCEL QUERY, CHECKPOINT, SNAPSHOT |
| WAL | RESUME WAL, SET TYPE WAL |
| Introspection | SHOW TABLES/COLUMNS/PARTITIONS/CREATE TABLE/PARAMETERS/... |
The parser handles QuestDB extensions that standard SQL parsers don't support:
-- Time-series sampling
SELECT avg(price) FROM trades SAMPLE BY 1h FILL(PREV) ALIGN TO CALENDAR
-- Latest value per partition
SELECT * FROM trades LATEST ON ts PARTITION BY symbol
-- Time-ordered joins
SELECT * FROM trades ASOF JOIN quotes ON (symbol)
SELECT * FROM trades LT JOIN quotes ON (symbol) TOLERANCE 5s
SELECT * FROM a WINDOW JOIN b ON a.ts = b.ts
RANGE BETWEEN 1h PRECEDING AND CURRENT ROW FOLLOWING
-- Implicit SELECT (table name without SELECT * FROM)
trades WHERE symbol = 'BTC-USD'
-- Duration literals
SELECT * FROM trades WHERE ts > now() - 2d
-- Geohash literals and types
CREATE TABLE geo (pos GEOHASH(8c), area GEOHASH(4b))
-- Array subscripts and slices
SELECT arr[2:], data[1:3] FROM t
-- WAL tables with deduplication and TTL
CREATE TABLE trades (ts TIMESTAMP, price DOUBLE)
TIMESTAMP(ts) PARTITION BY DAY WAL
DEDUP UPSERT KEYS(ts)
TTL 30 DAYSSQL String
|
v
Lexer (tokenize)
|
v
Tokens
|
v
Parser (Chevrotain CstParser)
|
v
Concrete Syntax Tree (CST)
|
v
Visitor (CST -> AST)
|
v
AST -----> toSql() -----> SQL String
The parser uses Chevrotain's CST pattern: the parser produces a lossless CST, then a visitor transforms it into a clean, typed AST. The toSql() function serializes any AST back to valid SQL.
| Function | Description |
|---|---|
parseToAst(sql) |
Parse SQL string into { ast: Statement[], errors: Error[] } |
parseOne(sql) |
Parse a single statement, throws on errors or multiple statements |
parseStatements(sql) |
Parse multiple statements, throws on errors |
toSql(ast) |
Convert Statement[] back to a SQL string |
format(sql, options) |
Format SQL text; tolerant of invalid input, whitespace-only edits |
| Function | Description |
|---|---|
tokenize(sql) |
Tokenize SQL into Chevrotain tokens |
parse(sql) |
Parse SQL to CST with lex/parse errors |
visitor |
Chevrotain CST visitor instance for CST-to-AST conversion |
| Function | Description |
|---|---|
createAutocompleteProvider(schema) |
Create a provider with getSuggestions(query, cursorOffset) |
getContentAssist(sql, offset) |
Low-level: get valid tokens at cursor position |
Arrays of keywords, functions, data types, operators, and constants for syntax highlighting integration:
import { keywords, functions, dataTypes, operators, constants } from "@questdb/sql-parser/grammar";yarn # Install dependencies
yarn build # Compile TypeScript (tsup + tsc)
yarn test # Run all tests (6,100+ tests)
yarn test:watch # Run tests in watch mode
yarn typecheck # Type-check without emitting
yarn lint # Run ESLint
yarn lint:fix # Auto-fix lint issues
yarn generate:cst # Regenerate CST type definitions from parser grammar
yarn clean # Remove dist/ and coverage/See DEVELOPMENT.md for the full development workflow guide — how to add keywords, statement types, modify autocomplete, and more.
Apache-2.0