Skip to content

Commit 62f0dba

Browse files
committed
harden parser/autocomplete and add npm-ready build + CI setup
1 parent 8c5964d commit 62f0dba

29 files changed

Lines changed: 4766 additions & 3570 deletions

.github/workflows/ci.yml

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
name: CI
2+
3+
on:
4+
push:
5+
branches: [master]
6+
pull_request:
7+
branches: [master]
8+
9+
jobs:
10+
build-and-test:
11+
runs-on: ubuntu-latest
12+
steps:
13+
- uses: actions/checkout@v4
14+
15+
- uses: actions/setup-node@v4
16+
with:
17+
node-version: 20
18+
cache: yarn
19+
20+
- run: yarn
21+
22+
- run: yarn build
23+
24+
- run: yarn lint
25+
26+
- run: yarn test

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
# Changelog
2+
3+
## 0.1.0
4+
- Initial release

README.md

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
# questdb-sql-parser
1+
# @questdb/questdb-sql-parser
22

33
A production-ready SQL parser for [QuestDB](https://questdb.io/) syntax, built with [Chevrotain](https://chevrotain.io/). Parses SQL into a fully typed AST, converts AST back to SQL, and provides context-aware autocomplete — all in a single package.
44

@@ -17,7 +17,7 @@ A production-ready SQL parser for [QuestDB](https://questdb.io/) syntax, built w
1717
### Parse SQL to AST
1818

1919
```typescript
20-
import { parseToAst } from "questdb-sql-parser";
20+
import { parseToAst } from "@questdb/questdb-sql-parser";
2121

2222
const result = parseToAst("SELECT * FROM trades WHERE symbol = 'BTC-USD'");
2323

@@ -39,7 +39,7 @@ if (result.errors.length === 0) {
3939
### Convert AST back to SQL
4040

4141
```typescript
42-
import { parseToAst, toSql } from "questdb-sql-parser";
42+
import { parseToAst, toSql } from "@questdb/questdb-sql-parser";
4343

4444
const result = parseToAst("SELECT avg(price) FROM trades SAMPLE BY 1h FILL(PREV)");
4545
const sql = toSql(result.ast);
@@ -49,7 +49,7 @@ const sql = toSql(result.ast);
4949
### Parse a single statement
5050

5151
```typescript
52-
import { parseOne } from "questdb-sql-parser";
52+
import { parseOne } from "@questdb/questdb-sql-parser";
5353

5454
const stmt = parseOne("INSERT INTO trades VALUES (now(), 'BTC', 42000.50)");
5555
console.log(stmt.type); // "insert"
@@ -58,7 +58,7 @@ console.log(stmt.type); // "insert"
5858
### Parse multiple statements
5959

6060
```typescript
61-
import { parseStatements } from "questdb-sql-parser";
61+
import { parseStatements } from "@questdb/questdb-sql-parser";
6262

6363
const statements = parseStatements(`
6464
SELECT * FROM trades
@@ -72,7 +72,7 @@ console.log(statements.length); // 3
7272
### Autocomplete
7373

7474
```typescript
75-
import { createAutocompleteProvider } from "questdb-sql-parser";
75+
import { createAutocompleteProvider } from "@questdb/questdb-sql-parser";
7676

7777
const provider = createAutocompleteProvider({
7878
tables: [
@@ -204,7 +204,7 @@ The parser uses Chevrotain's [CST pattern](https://chevrotain.io/docs/guide/conc
204204
Arrays of keywords, functions, data types, operators, and constants for syntax highlighting integration:
205205

206206
```typescript
207-
import { keywords, functions, dataTypes, operators, constants } from "questdb-sql-parser";
207+
import { keywords, functions, dataTypes, operators, constants } from "@questdb/questdb-sql-parser/grammar";
208208
```
209209

210210
## Development

eslint.config.ts

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import { Linter } from "eslint"
77

88
export default [
99
{
10-
ignores: ["dist/", "node_modules/", "eslint.config.ts", "src/parser/cst-types.d.ts"],
10+
ignores: ["dist/", "node_modules/", "eslint.config.ts", "tsup.config.ts", "src/parser/cst-types.d.ts"],
1111
},
1212
{
1313
files: ["**/*.{js,ts}"],
@@ -27,11 +27,11 @@ export default [
2727
},
2828
},
2929
{
30-
files: ["src/**/*.ts"],
30+
files: ["src/**/*.ts", "tests/**/*.ts"],
3131
languageOptions: {
3232
parser: tseslint.parser,
3333
parserOptions: {
34-
project: "./tsconfig.json",
34+
projectService: true,
3535
tsconfigRootDir: import.meta.dirname,
3636
},
3737
},
@@ -81,6 +81,14 @@ export default [
8181
"no-shadow-restricted-names": "off",
8282
},
8383
},
84+
{
85+
files: ["tests/**/*.ts"],
86+
languageOptions: {
87+
globals: {
88+
...globals.vitest,
89+
},
90+
},
91+
},
8492

8593
prettierConfig,
8694
] satisfies Linter.Config[]

package.json

Lines changed: 34 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -3,23 +3,43 @@
33
"version": "0.1.0",
44
"description": "SQL parser for QuestDB syntax using Chevrotain",
55
"type": "module",
6-
"main": "dist/index.js",
6+
"main": "dist/index.cjs",
7+
"module": "dist/index.js",
78
"types": "dist/index.d.ts",
9+
"sideEffects": false,
810
"exports": {
911
".": {
10-
"import": "./dist/index.js",
11-
"types": "./dist/index.d.ts"
12+
"import": {
13+
"types": "./dist/index.d.ts",
14+
"default": "./dist/index.js"
15+
},
16+
"require": {
17+
"types": "./dist/index.d.ts",
18+
"default": "./dist/index.cjs"
19+
}
20+
},
21+
"./grammar": {
22+
"import": {
23+
"types": "./dist/grammar/index.d.ts",
24+
"default": "./dist/grammar/index.js"
25+
},
26+
"require": {
27+
"types": "./dist/grammar/index.d.ts",
28+
"default": "./dist/grammar/index.cjs"
29+
}
1230
}
1331
},
1432
"packageManager": "yarn@4.9.1",
1533
"scripts": {
16-
"build": "tsc",
34+
"build": "tsup && tsc -p tsconfig.build.json",
35+
"typecheck": "tsc --noEmit",
1736
"test": "vitest run",
1837
"test:watch": "vitest",
1938
"test:coverage": "vitest run --coverage",
2039
"clean": "rm -rf dist coverage",
21-
"lint": "eslint src/",
22-
"lint:fix": "eslint src/ --fix"
40+
"lint": "eslint src/ tests/",
41+
"lint:fix": "eslint src/ tests/ --fix",
42+
"prepublishOnly": "yarn clean && yarn build"
2343
},
2444
"keywords": [
2545
"questdb",
@@ -31,27 +51,34 @@
3151
"files": [
3252
"dist",
3353
"README.md",
54+
"CHANGELOG.md",
3455
"LICENSE"
3556
],
57+
"engines": {
58+
"node": ">=18.18.0"
59+
},
3660
"repository": {
3761
"type": "git",
3862
"url": "https://github.com/questdb/questdb-sql-parser.git"
3963
},
4064
"homepage": "https://questdb.com",
65+
"bugs": {
66+
"url": "https://github.com/questdb/questdb-sql-parser/issues"
67+
},
4168
"license": "Apache-2.0",
4269
"dependencies": {
4370
"chevrotain": "^11.1.1"
4471
},
4572
"devDependencies": {
4673
"@eslint/js": "^10.0.1",
47-
"@types/jest": "^30.0.0",
4874
"@types/node": "^25.2.0",
4975
"eslint": "^10.0.0",
5076
"eslint-config-prettier": "^10.1.8",
5177
"eslint-plugin-prettier": "^5.5.5",
5278
"globals": "^17.3.0",
5379
"jiti": "^2.6.1",
5480
"prettier": "^3.8.1",
81+
"tsup": "^8.5.1",
5582
"typescript": "^5.9.3",
5683
"typescript-eslint": "^8.55.0",
5784
"vitest": "^4.0.18"

src/autocomplete/content-assist.ts

Lines changed: 18 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
import { type ILexingError, IToken, TokenType } from "chevrotain"
2-
import { parser } from "../parser/parser"
2+
import { parser, parse as parseRaw } from "../parser/parser"
3+
import { visitor } from "../parser/visitor"
34
import { QuestDBLexer } from "../parser/lexer"
4-
import { parseToAst } from "../index"
5+
import type { Statement } from "../parser/ast"
56
import { IDENTIFIER_KEYWORD_TOKENS } from "./token-classification"
67

78
// =============================================================================
@@ -47,7 +48,11 @@ function normalizeTableName(value: unknown): string | undefined {
4748
if (typeof value === "object" && value !== null) {
4849
const obj = value as Record<string, unknown>
4950
if (Array.isArray(obj.parts)) {
50-
return (obj.parts as string[]).join(".")
51+
// Use only the last part (table name) for schema lookup.
52+
// Schema-qualified names like "public.trades" should resolve to "trades"
53+
// since the schema columns map is keyed by bare table name.
54+
const parts = obj.parts as string[]
55+
return parts[parts.length - 1]
5156
}
5257
if (typeof obj.name === "string") return obj.name
5358
}
@@ -66,7 +71,7 @@ function extractTablesFromAst(ast: unknown): TableRef[] {
6671
const n = node as Record<string, unknown>
6772

6873
// Handle table references in FROM clause
69-
if (n.type === "table_ref" || n.type === "tableRef") {
74+
if (n.type === "tableRef") {
7075
const tableName = normalizeTableName(n.table ?? n.name)
7176
if (tableName) {
7277
tables.push({
@@ -151,12 +156,13 @@ function extractTablesFromAst(ast: unknown): TableRef[] {
151156
function extractTables(fullSql: string, tokens: IToken[]): TableRef[] {
152157
// First, try to parse and extract from AST
153158
try {
154-
const result = parseToAst(fullSql)
155-
if (result.ast && result.ast.length > 0) {
156-
return extractTablesFromAst(result.ast)
159+
const { cst } = parseRaw(fullSql)
160+
const ast = visitor.visit(cst) as Statement[]
161+
if (ast && ast.length > 0) {
162+
return extractTablesFromAst(ast)
157163
}
158-
} catch (e) {
159-
// Parsing failed, fall through to token-based extraction
164+
} catch {
165+
// Parsing or visitor failed, fall through to token-based extraction
160166
}
161167

162168
// Fallback: extract from tokens by looking for table name patterns
@@ -194,7 +200,9 @@ function extractTables(fullSql: string, tokens: IToken[]): TableRef[] {
194200
i += 2
195201
}
196202

197-
return { name: parts.join("."), nextIndex: i }
203+
// Use only the last part (table name) for schema lookup.
204+
// Schema-qualified names like "metrics.trades" resolve to "trades".
205+
return { name: parts[parts.length - 1], nextIndex: i }
198206
}
199207

200208
for (let i = 0; i < tokens.length; i++) {

src/autocomplete/index.ts

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,4 @@ export {
3030
} from "./token-classification"
3131

3232
// Suggestion building (for advanced use cases)
33-
export {
34-
buildSuggestions,
35-
buildFallbackSuggestions,
36-
} from "./suggestion-builder"
33+
export { buildSuggestions } from "./suggestion-builder"

src/autocomplete/suggestion-builder.ts

Lines changed: 2 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -138,7 +138,7 @@ export function buildSuggestions(
138138
label: keyword,
139139
kind,
140140
insertText: keyword,
141-
filterText: name.toLowerCase(),
141+
filterText: keyword.toLowerCase(),
142142
priority,
143143
})
144144
}
@@ -223,43 +223,11 @@ export function buildSuggestions(
223223
label: display,
224224
kind: SuggestionKind.Keyword,
225225
insertText: display,
226-
filterText: name.toLowerCase(),
226+
filterText: display.toLowerCase(),
227227
priority: SuggestionPriority.Low,
228228
})
229229
}
230230
}
231231

232232
return suggestions
233233
}
234-
235-
/**
236-
* Build fallback suggestions when parser can't determine valid tokens.
237-
* Returns all columns and tables as a generic fallback.
238-
*/
239-
export function buildFallbackSuggestions(schema: SchemaInfo): Suggestion[] {
240-
const suggestions: Suggestion[] = []
241-
242-
// Add all columns
243-
const allColumns = getAllColumns(schema)
244-
for (const col of allColumns) {
245-
suggestions.push({
246-
label: col.name,
247-
kind: SuggestionKind.Column,
248-
insertText: col.name,
249-
detail: `${col.tableName}.${col.name} (${col.type})`,
250-
priority: SuggestionPriority.High,
251-
})
252-
}
253-
254-
// Add all tables
255-
for (const table of schema.tables) {
256-
suggestions.push({
257-
label: table.name,
258-
kind: SuggestionKind.Table,
259-
insertText: table.name,
260-
priority: SuggestionPriority.MediumLow,
261-
})
262-
}
263-
264-
return suggestions
265-
}

0 commit comments

Comments
 (0)