Skip to content

Commit 69cb33b

Browse files
committed
add dialect and refactor sql parser
1 parent f84b1fd commit 69cb33b

11 files changed

Lines changed: 99 additions & 40 deletions

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ const editor = new EditorView({
4444
autocomplete: cteCompletionSource,
4545
}),
4646
sqlExtension({
47+
sqlParser: new SqlParser({ dialect: "MySQL" }),
4748
linterConfig: {
4849
delay: 250 // Validation delay in ms
4950
},

demo/index.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { StandardSQL, sql } from "@codemirror/lang-sql";
22
import { basicSetup, EditorView } from "codemirror";
33
import { cteCompletionSource } from "../src/sql/cte-completion-source.js";
44
import { sqlExtension } from "../src/sql/extension.js";
5+
import { SqlParser } from "../src/sql/parser.js";
56

67
// Default SQL content for the demo
78
const defaultSqlDoc = `-- Welcome to the SQL Editor Demo!
@@ -109,6 +110,9 @@ function initializeEditor() {
109110
upperCaseKeywords: true,
110111
}),
111112
sqlExtension({
113+
// Parser definition
114+
sqlParser: new SqlParser({ dialect: "PostgresQL" }),
115+
112116
// Linter extension configuration
113117
linterConfig: {
114118
delay: 250, // Delay before running validation

src/sql/__tests__/diagnostics.test.ts

Lines changed: 8 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,9 @@ import { Text } from "@codemirror/state";
22
import type { EditorView } from "@codemirror/view";
33
import { describe, expect, it, vi } from "vitest";
44
import { sqlLinter } from "../diagnostics.js";
5-
import type { SqlParser } from "../parser.js";
5+
import { SqlParser } from "../parser.js";
6+
7+
const defaultParser = new SqlParser({ dialect: "PostgresQL" });
68

79
// Mock EditorView
810
const _createMockView = (content: string) => {
@@ -14,12 +16,12 @@ const _createMockView = (content: string) => {
1416

1517
describe("sqlLinter", () => {
1618
it("should create a linter extension", () => {
17-
const linter = sqlLinter();
19+
const linter = sqlLinter(defaultParser);
1820
expect(linter).toBeDefined();
1921
});
2022

2123
it("should accept configuration with custom delay", () => {
22-
const linter = sqlLinter({ delay: 1000 });
24+
const linter = sqlLinter(defaultParser, { delay: 1000 });
2325
expect(linter).toBeDefined();
2426
});
2527

@@ -29,22 +31,17 @@ describe("sqlLinter", () => {
2931
parseSql: vi.fn(() => ({ statements: [] })),
3032
} as unknown as SqlParser;
3133

32-
const linter = sqlLinter({ parser: mockParser });
34+
const linter = sqlLinter(mockParser);
3335
expect(linter).toBeDefined();
3436
});
3537

3638
it("should use default delay when no delay provided", () => {
37-
const linter = sqlLinter();
39+
const linter = sqlLinter(defaultParser);
3840
expect(linter).toBeDefined();
3941
});
4042

4143
it("should use custom delay when provided", () => {
42-
const linter = sqlLinter({ delay: 500 });
43-
expect(linter).toBeDefined();
44-
});
45-
46-
it("should use default parser when no parser provided", () => {
47-
const linter = sqlLinter();
44+
const linter = sqlLinter(defaultParser, { delay: 500 });
4845
expect(linter).toBeDefined();
4946
});
5047
});

src/sql/__tests__/parser.test.ts

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { describe, expect, it } from "vitest";
22
import { SqlParser } from "../parser.js";
33

44
describe("SqlParser", () => {
5-
const parser = new SqlParser();
5+
const parser = new SqlParser({ dialect: "PostgresQL" });
66

77
describe("parse", () => {
88
it("should parse valid SQL successfully", () => {
@@ -67,3 +67,21 @@ describe("SqlParser", () => {
6767
});
6868
});
6969
});
70+
71+
describe("Dialects", () => {
72+
it("should parse valid SQL based on dialect", () => {
73+
const sql = "SELECT * FROM table_function();";
74+
75+
const parser = new SqlParser({ dialect: "PostgresQL" });
76+
const result = parser.parse(sql);
77+
expect(result.success).toBe(true);
78+
expect(result.errors).toHaveLength(0);
79+
expect(result.ast).toBeDefined();
80+
81+
// MySQL does not support calling functions as tables
82+
const mysqlParser = new SqlParser({ dialect: "MySQL" });
83+
const mysqlResult = mysqlParser.parse(sql);
84+
expect(mysqlResult.success).toBe(false);
85+
expect(mysqlResult.errors).toHaveLength(1);
86+
});
87+
});

src/sql/__tests__/structure-analyzer.test.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,15 @@
11
import { EditorState } from "@codemirror/state";
22
import { beforeEach, describe, expect, it } from "vitest";
3+
import { SqlParser } from "../parser.js";
34
import { SqlStructureAnalyzer } from "../structure-analyzer.js";
45

56
describe("SqlStructureAnalyzer", () => {
67
let analyzer: SqlStructureAnalyzer;
78
let state: EditorState;
89

910
beforeEach(() => {
10-
analyzer = new SqlStructureAnalyzer();
11+
const parser = new SqlParser({ dialect: "PostgresQL" });
12+
analyzer = new SqlStructureAnalyzer(parser);
1113
});
1214

1315
const createState = (content: string) => {

src/sql/__tests__/structure-extension.test.ts

Lines changed: 11 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,17 @@
11
import { EditorState, Text } from "@codemirror/state";
22
import type { EditorView } from "@codemirror/view";
33
import { describe, expect, it } from "vitest";
4+
import { SqlParser } from "../parser.js";
45
import { sqlStructureGutter } from "../structure-extension.js";
56

7+
const defaultParser = new SqlParser({ dialect: "PostgresQL" });
8+
69
// Mock EditorView
710
const _createMockView = (content: string, hasFocus = true) => {
811
const doc = Text.of(content.split("\n"));
912
const state = EditorState.create({
1013
doc,
11-
extensions: [sqlStructureGutter()],
14+
extensions: [sqlStructureGutter(defaultParser)],
1215
});
1316

1417
return {
@@ -20,7 +23,7 @@ const _createMockView = (content: string, hasFocus = true) => {
2023

2124
describe("sqlStructureGutter", () => {
2225
it("should create a gutter extension with default config", () => {
23-
const extensions = sqlStructureGutter();
26+
const extensions = sqlStructureGutter(defaultParser);
2427
expect(Array.isArray(extensions)).toBe(true);
2528
expect(extensions.length).toBeGreaterThan(0);
2629
});
@@ -36,39 +39,39 @@ describe("sqlStructureGutter", () => {
3639
hideWhenNotFocused: true,
3740
};
3841

39-
const extensions = sqlStructureGutter(config);
42+
const extensions = sqlStructureGutter(defaultParser, config);
4043
expect(Array.isArray(extensions)).toBe(true);
4144
expect(extensions.length).toBeGreaterThan(0);
4245
});
4346

4447
it("should handle empty configuration", () => {
45-
const extensions = sqlStructureGutter({});
48+
const extensions = sqlStructureGutter(defaultParser);
4649
expect(Array.isArray(extensions)).toBe(true);
4750
});
4851

4952
it("should create extensions for all required parts", () => {
50-
const extensions = sqlStructureGutter();
53+
const extensions = sqlStructureGutter(defaultParser);
5154
// Should include state field, update listener, theme, and gutter
5255
expect(extensions.length).toBe(4);
5356
});
5457

5558
it("should handle unfocusedOpacity configuration", () => {
5659
const config = { unfocusedOpacity: 0.2 };
57-
const extensions = sqlStructureGutter(config);
60+
const extensions = sqlStructureGutter(defaultParser, config);
5861
expect(extensions.length).toBe(4);
5962
});
6063

6164
it("should handle whenHide configuration", () => {
6265
const config = {
6366
whenHide: (view: EditorView) => view.state.doc.length === 0,
6467
};
65-
const extensions = sqlStructureGutter(config);
68+
const extensions = sqlStructureGutter(defaultParser, config);
6669
expect(extensions.length).toBe(4);
6770
});
6871

6972
it("should work with minimal configuration", () => {
7073
const config = { width: 2 };
71-
const extensions = sqlStructureGutter(config);
74+
const extensions = sqlStructureGutter(defaultParser, config);
7275
expect(extensions.length).toBe(4);
7376
});
7477
});

src/sql/diagnostics.ts

Lines changed: 3 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { type Diagnostic, linter } from "@codemirror/lint";
22
import type { Text } from "@codemirror/state";
33
import type { EditorView } from "@codemirror/view";
4-
import { type SqlParseError, SqlParser } from "./parser.js";
4+
import type { SqlParseError, SqlParser } from "./parser.js";
55

66
const DEFAULT_DELAY = 750;
77

@@ -11,8 +11,6 @@ const DEFAULT_DELAY = 750;
1111
export interface SqlLinterConfig {
1212
/** Delay in milliseconds before running validation (default: 750) */
1313
delay?: number;
14-
/** Custom SQL parser instance to use for validation */
15-
parser?: SqlParser;
1614
}
1715

1816
/**
@@ -48,9 +46,7 @@ function convertToCodeMirrorDiagnostic(error: SqlParseError, doc: Text): Diagnos
4846
* });
4947
* ```
5048
*/
51-
export function sqlLinter(config: SqlLinterConfig = {}) {
52-
const parser = config.parser || new SqlParser();
53-
49+
export function sqlLinter(parser: SqlParser, config: SqlLinterConfig = {}) {
5450
return linter(
5551
(view: EditorView): Diagnostic[] => {
5652
const doc = view.state.doc;
@@ -65,7 +61,7 @@ export function sqlLinter(config: SqlLinterConfig = {}) {
6561
return errors.map((error) => convertToCodeMirrorDiagnostic(error, doc));
6662
},
6763
{
68-
delay: config.delay || DEFAULT_DELAY,
64+
delay: config.delay ?? DEFAULT_DELAY,
6965
},
7066
);
7167
}

src/sql/extension.ts

Lines changed: 18 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,20 @@
11
import type { Extension } from "@codemirror/state";
22
import { type SqlLinterConfig, sqlLinter } from "./diagnostics.js";
33
import { type SqlHoverConfig, sqlHover, sqlHoverTheme } from "./hover.js";
4+
import { SqlParser } from "./parser.js";
45
import { type SqlGutterConfig, sqlStructureGutter } from "./structure-extension.js";
56

67
/**
78
* Configuration options for the SQL extension
89
*/
910
export interface SqlExtensionConfig {
11+
/**
12+
* The SQL parser used for linting and gutter markers.
13+
* If not provided, a default PostgreSQL parser is created.
14+
* The parser instance is shared across the extension.
15+
*/
16+
sqlParser?: SqlParser;
17+
1018
/** Whether to enable SQL linting (default: true) */
1119
enableLinting?: boolean;
1220
/** Configuration for the SQL linter */
@@ -53,17 +61,22 @@ export function sqlExtension(config: SqlExtensionConfig = {}): Extension[] {
5361
enableLinting = true,
5462
enableGutterMarkers = true,
5563
enableHover = true,
64+
sqlParser,
5665
linterConfig,
5766
gutterConfig,
5867
hoverConfig,
5968
} = config;
6069

61-
if (enableLinting) {
62-
extensions.push(sqlLinter(linterConfig));
63-
}
70+
if (enableLinting || enableGutterMarkers) {
71+
const parser = sqlParser ?? new SqlParser({ dialect: "PostgresQL" });
72+
73+
if (enableLinting) {
74+
extensions.push(sqlLinter(parser, linterConfig));
75+
}
6476

65-
if (enableGutterMarkers) {
66-
extensions.push(sqlStructureGutter(gutterConfig));
77+
if (enableGutterMarkers) {
78+
extensions.push(sqlStructureGutter(parser, gutterConfig));
79+
}
6780
}
6881

6982
if (enableHover) {

src/sql/parser.ts

Lines changed: 26 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,20 +26,44 @@ export interface SqlParseResult {
2626
ast?: unknown;
2727
}
2828

29+
// https://github.com/taozhi8833998/node-sql-parser?tab=readme-ov-file#supported-database-sql-syntax
30+
export type SupportedDialects =
31+
| "Athena"
32+
| "BigQuery"
33+
| "DB2"
34+
| "Hive"
35+
| "MariaDB"
36+
| "MySQL"
37+
| "PostgresQL"
38+
| "Redshift"
39+
| "Sqlite"
40+
| "TransactSQL"
41+
| "FlinkSQL"
42+
| "Snowflake"
43+
| "Noql";
44+
45+
export interface SqlParserConfig {
46+
/** The database dialect to use for parsing */
47+
dialect: SupportedDialects;
48+
}
49+
2950
/**
3051
* A SQL parser wrapper around node-sql-parser with enhanced error handling
3152
* and validation capabilities for CodeMirror integration.
3253
*/
3354
export class SqlParser {
3455
private parser: Parser;
56+
private dialect: SupportedDialects;
3557

36-
constructor() {
58+
constructor(config: SqlParserConfig) {
3759
this.parser = new Parser();
60+
this.dialect = config.dialect;
3861
}
3962

4063
parse(sql: string): SqlParseResult {
4164
try {
42-
const ast = this.parser.astify(sql);
65+
const dialect = this.dialect;
66+
const ast = this.parser.astify(sql, { database: dialect });
4367

4468
return {
4569
success: true,

src/sql/structure-analyzer.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import type { EditorState } from "@codemirror/state";
2-
import { SqlParser } from "./parser.js";
2+
import type { SqlParser } from "./parser.js";
33

44
/**
55
* Represents a SQL statement with position information
@@ -29,8 +29,8 @@ export class SqlStructureAnalyzer {
2929
private parser: SqlParser;
3030
private cache = new Map<string, SqlStatement[]>();
3131

32-
constructor() {
33-
this.parser = new SqlParser();
32+
constructor(parser: SqlParser) {
33+
this.parser = parser;
3434
}
3535

3636
/**

0 commit comments

Comments
 (0)