Skip to content

Commit 99a76df

Browse files
authored
improvement: make it easy to override sql parse. get options via EditorState (#13)
* improvement: make it easy to override sql parse. get options via EditorState * tests
1 parent d953360 commit 99a76df

9 files changed

Lines changed: 98 additions & 44 deletions

File tree

src/__tests__/index.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ describe("index.ts exports", () => {
66
const sortedExports = Object.keys(exports).sort();
77
expect(sortedExports).toMatchInlineSnapshot(`
88
[
9-
"SqlParser",
9+
"NodeSqlParser",
1010
"SqlStructureAnalyzer",
1111
"cteCompletionSource",
1212
"sqlExtension",

src/index.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,9 @@ export type {
88
SqlKeywordInfo,
99
} from "./sql/hover.js";
1010
export { sqlHover, sqlHoverTheme } from "./sql/hover.js";
11-
export { SqlParser } from "./sql/parser.js";
11+
export { NodeSqlParser } from "./sql/parser.js";
1212
export type { SqlStatement } from "./sql/structure-analyzer.js";
1313
export { SqlStructureAnalyzer } from "./sql/structure-analyzer.js";
1414
export type { SqlGutterConfig } from "./sql/structure-extension.js";
1515
export { sqlStructureGutter } from "./sql/structure-extension.js";
16+
export type { SqlParseError, SqlParseResult, SqlParser } from "./sql/types.js";

src/sql/__tests__/parser.test.ts

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

44
describe("SqlParser", () => {
5-
const parser = new SqlParser();
5+
const parser = new NodeSqlParser();
66

77
describe("parse", () => {
88
it("should parse valid SQL successfully", async () => {

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

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,14 @@
11
import { EditorState } from "@codemirror/state";
22
import { beforeEach, describe, expect, it } from "vitest";
3+
import { NodeSqlParser } 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+
analyzer = new SqlStructureAnalyzer(new NodeSqlParser());
1112
});
1213

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

src/sql/diagnostics.ts

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
import { type Diagnostic, linter } from "@codemirror/lint";
2-
import type { Text } from "@codemirror/state";
2+
import type { Extension, Text } from "@codemirror/state";
33
import type { EditorView } from "@codemirror/view";
4-
import { type SqlParseError, SqlParser } from "./parser.js";
4+
import { NodeSqlParser } from "./parser.js";
5+
import type { SqlParseError, SqlParser } from "./types.js";
56

67
const DEFAULT_DELAY = 750;
78

@@ -48,8 +49,8 @@ function convertToCodeMirrorDiagnostic(error: SqlParseError, doc: Text): Diagnos
4849
* });
4950
* ```
5051
*/
51-
export function sqlLinter(config: SqlLinterConfig = {}) {
52-
const parser = config.parser || new SqlParser();
52+
export function sqlLinter(config: SqlLinterConfig = {}): Extension {
53+
const parser = config.parser || new NodeSqlParser();
5354

5455
return linter(
5556
async (view: EditorView): Promise<Diagnostic[]> => {
@@ -60,7 +61,7 @@ export function sqlLinter(config: SqlLinterConfig = {}) {
6061
return [];
6162
}
6263

63-
const errors = await parser.validateSql(sql);
64+
const errors = await parser.validateSql(sql, { state: view.state });
6465

6566
return errors.map((error) => convertToCodeMirrorDiagnostic(error, doc));
6667
},

src/sql/parser.ts

Lines changed: 31 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1,36 +1,37 @@
1+
import type { EditorState } from "@codemirror/state";
2+
import type { Option } from "node-sql-parser";
13
import { lazy } from "../utils.js";
4+
import type { SqlParseError, SqlParseResult, SqlParser } from "./types.js";
25

3-
/**
4-
* Represents a SQL parsing error with location information
5-
*/
6-
export interface SqlParseError {
7-
/** Error message describing the issue */
8-
message: string;
9-
/** Line number where the error occurred (1-indexed) */
10-
line: number;
11-
/** Column number where the error occurred (1-indexed) */
12-
column: number;
13-
/** Severity level of the error */
14-
severity: "error" | "warning";
15-
}
16-
17-
/**
18-
* Result of parsing a SQL statement
19-
*/
20-
export interface SqlParseResult {
21-
/** Whether parsing was successful */
22-
success: boolean;
23-
/** Array of parsing errors, if any */
24-
errors: SqlParseError[];
25-
/** The parsed AST if successful */
26-
ast?: unknown;
6+
interface NodeSqlParserOptions {
7+
getParserOptions?: (state: EditorState) => Option;
278
}
289

2910
/**
3011
* A SQL parser wrapper around node-sql-parser with enhanced error handling
3112
* and validation capabilities for CodeMirror integration.
13+
*
14+
* @example Custom dialect
15+
* ```ts
16+
* import { NodeSqlParser } from "@marimo-team/codemirror-sql";
17+
*
18+
* const myParser = new NodeSqlParser({
19+
* getParserOptions: (state) => ({
20+
* dialect: getDialect(state),
21+
* parseOptions: {
22+
* includeLocations: true,
23+
* },
24+
* }),
25+
* });
26+
* ```
3227
*/
33-
export class SqlParser {
28+
export class NodeSqlParser implements SqlParser {
29+
private opts: NodeSqlParserOptions;
30+
31+
constructor(opts: NodeSqlParserOptions = {}) {
32+
this.opts = opts;
33+
}
34+
3435
/**
3536
* Lazy import of the node-sql-parser package and create a new Parser instance.
3637
*/
@@ -39,10 +40,11 @@ export class SqlParser {
3940
return new Parser();
4041
});
4142

42-
async parse(sql: string): Promise<SqlParseResult> {
43+
async parse(sql: string, opts: { state: EditorState }): Promise<SqlParseResult> {
4344
try {
45+
const parserOptions = this.opts.getParserOptions?.(opts.state);
4446
const parser = await this.getParser();
45-
const ast = parser.astify(sql);
47+
const ast = parser.astify(sql, parserOptions);
4648

4749
return {
4850
success: true,
@@ -102,8 +104,8 @@ export class SqlParser {
102104
.trim();
103105
}
104106

105-
async validateSql(sql: string): Promise<SqlParseError[]> {
106-
const result = await this.parse(sql);
107+
async validateSql(sql: string, opts: { state: EditorState }): Promise<SqlParseError[]> {
108+
const result = await this.parse(sql, opts);
107109
return result.errors;
108110
}
109111
}

src/sql/structure-analyzer.ts

Lines changed: 4 additions & 4 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 "./types.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
/**
@@ -111,7 +111,7 @@ export class SqlStructureAnalyzer {
111111
}
112112

113113
// Parse the statement to determine validity and type (use stripped content)
114-
const parseResult = await this.parser.parse(strippedContent);
114+
const parseResult = await this.parser.parse(strippedContent, { state });
115115
const type = this.determineStatementType(strippedContent);
116116

117117
// Remove trailing semicolon from content for cleaner display

src/sql/structure-extension.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
import { type Extension, RangeSet, StateEffect, StateField } from "@codemirror/state";
22
import { EditorView, GutterMarker, gutter, type ViewUpdate } from "@codemirror/view";
3+
import { NodeSqlParser } from "./parser.js";
34
import { type SqlStatement, SqlStructureAnalyzer } from "./structure-analyzer.js";
5+
import type { SqlParser } from "./types.js";
46

57
export interface SqlGutterConfig {
68
/** Background color for the current statement indicator */
@@ -21,6 +23,8 @@ export interface SqlGutterConfig {
2123
hideWhenNotFocused?: boolean;
2224
/** Opacity when editor is not focused (overrides hideWhenNotFocused if set) */
2325
unfocusedOpacity?: number;
26+
/** Custom SQL parser instance to use for analysis */
27+
parser?: SqlParser;
2428
}
2529

2630
interface SqlGutterState {
@@ -230,7 +234,8 @@ function createSqlGutter(config: SqlGutterConfig): Extension {
230234
* indicators for other statements.
231235
*/
232236
export function sqlStructureGutter(config: SqlGutterConfig = {}): Extension[] {
233-
const analyzer = new SqlStructureAnalyzer();
237+
const parser = config.parser || new NodeSqlParser();
238+
const analyzer = new SqlStructureAnalyzer(parser);
234239

235240
return [
236241
sqlGutterStateField,

src/sql/types.ts

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
import type { EditorState } from "@codemirror/state";
2+
3+
/**
4+
* Represents a SQL parsing error with location information
5+
*/
6+
export interface SqlParseError {
7+
/** Error message describing the issue */
8+
message: string;
9+
/** Line number where the error occurred (1-indexed) */
10+
line: number;
11+
/** Column number where the error occurred (1-indexed) */
12+
column: number;
13+
/** Severity level of the error */
14+
severity: "error" | "warning";
15+
}
16+
/**
17+
* Result of parsing a SQL statement
18+
*/
19+
20+
export interface SqlParseResult {
21+
/** Whether parsing was successful */
22+
success: boolean;
23+
/** Array of parsing errors, if any */
24+
errors: SqlParseError[];
25+
/** The parsed AST if successful */
26+
ast?: unknown;
27+
}
28+
29+
export interface SqlParser {
30+
/**
31+
* Parse a SQL statement and return the AST
32+
* @param sql - The SQL statement to parse
33+
* @param opts - The options for the parser
34+
* @returns The parsed AST
35+
*/
36+
parse(sql: string, opts: { state: EditorState }): Promise<SqlParseResult>;
37+
/**
38+
* Validate a SQL statement and return any errors
39+
* @param sql - The SQL statement to validate
40+
* @param opts - The options for the parser
41+
* @returns An array of errors
42+
*/
43+
validateSql(sql: string, opts: { state: EditorState }): Promise<SqlParseError[]>;
44+
}

0 commit comments

Comments
 (0)