diff --git a/src/__tests__/index.test.ts b/src/__tests__/index.test.ts index 9252db0..5146987 100644 --- a/src/__tests__/index.test.ts +++ b/src/__tests__/index.test.ts @@ -6,7 +6,7 @@ describe("index.ts exports", () => { const sortedExports = Object.keys(exports).sort(); expect(sortedExports).toMatchInlineSnapshot(` [ - "SqlParser", + "NodeSqlParser", "SqlStructureAnalyzer", "cteCompletionSource", "sqlExtension", diff --git a/src/index.ts b/src/index.ts index 7149a0c..168e6c3 100644 --- a/src/index.ts +++ b/src/index.ts @@ -8,8 +8,9 @@ export type { SqlKeywordInfo, } from "./sql/hover.js"; export { sqlHover, sqlHoverTheme } from "./sql/hover.js"; -export { SqlParser } from "./sql/parser.js"; +export { NodeSqlParser } from "./sql/parser.js"; export type { SqlStatement } from "./sql/structure-analyzer.js"; export { SqlStructureAnalyzer } from "./sql/structure-analyzer.js"; export type { SqlGutterConfig } from "./sql/structure-extension.js"; export { sqlStructureGutter } from "./sql/structure-extension.js"; +export type { SqlParseError, SqlParseResult, SqlParser } from "./sql/types.js"; diff --git a/src/sql/__tests__/parser.test.ts b/src/sql/__tests__/parser.test.ts index 98f5b9c..5e18323 100644 --- a/src/sql/__tests__/parser.test.ts +++ b/src/sql/__tests__/parser.test.ts @@ -1,8 +1,8 @@ import { describe, expect, it } from "vitest"; -import { SqlParser } from "../parser.js"; +import { NodeSqlParser } from "../parser.js"; describe("SqlParser", () => { - const parser = new SqlParser(); + const parser = new NodeSqlParser(); describe("parse", () => { it("should parse valid SQL successfully", async () => { diff --git a/src/sql/__tests__/structure-analyzer.test.ts b/src/sql/__tests__/structure-analyzer.test.ts index d08942e..495f906 100644 --- a/src/sql/__tests__/structure-analyzer.test.ts +++ b/src/sql/__tests__/structure-analyzer.test.ts @@ -1,5 +1,6 @@ import { EditorState } from "@codemirror/state"; import { beforeEach, describe, expect, it } from "vitest"; +import { NodeSqlParser } from "../parser.js"; import { SqlStructureAnalyzer } from "../structure-analyzer.js"; describe("SqlStructureAnalyzer", () => { @@ -7,7 +8,7 @@ describe("SqlStructureAnalyzer", () => { let state: EditorState; beforeEach(() => { - analyzer = new SqlStructureAnalyzer(); + analyzer = new SqlStructureAnalyzer(new NodeSqlParser()); }); const createState = (content: string) => { diff --git a/src/sql/diagnostics.ts b/src/sql/diagnostics.ts index 2174fdb..7653665 100644 --- a/src/sql/diagnostics.ts +++ b/src/sql/diagnostics.ts @@ -1,7 +1,8 @@ import { type Diagnostic, linter } from "@codemirror/lint"; -import type { Text } from "@codemirror/state"; +import type { Extension, Text } from "@codemirror/state"; import type { EditorView } from "@codemirror/view"; -import { type SqlParseError, SqlParser } from "./parser.js"; +import { NodeSqlParser } from "./parser.js"; +import type { SqlParseError, SqlParser } from "./types.js"; const DEFAULT_DELAY = 750; @@ -48,8 +49,8 @@ function convertToCodeMirrorDiagnostic(error: SqlParseError, doc: Text): Diagnos * }); * ``` */ -export function sqlLinter(config: SqlLinterConfig = {}) { - const parser = config.parser || new SqlParser(); +export function sqlLinter(config: SqlLinterConfig = {}): Extension { + const parser = config.parser || new NodeSqlParser(); return linter( async (view: EditorView): Promise => { @@ -60,7 +61,7 @@ export function sqlLinter(config: SqlLinterConfig = {}) { return []; } - const errors = await parser.validateSql(sql); + const errors = await parser.validateSql(sql, { state: view.state }); return errors.map((error) => convertToCodeMirrorDiagnostic(error, doc)); }, diff --git a/src/sql/parser.ts b/src/sql/parser.ts index 7645453..9337a13 100644 --- a/src/sql/parser.ts +++ b/src/sql/parser.ts @@ -1,36 +1,37 @@ +import type { EditorState } from "@codemirror/state"; +import type { Option } from "node-sql-parser"; import { lazy } from "../utils.js"; +import type { SqlParseError, SqlParseResult, SqlParser } from "./types.js"; -/** - * Represents a SQL parsing error with location information - */ -export interface SqlParseError { - /** Error message describing the issue */ - message: string; - /** Line number where the error occurred (1-indexed) */ - line: number; - /** Column number where the error occurred (1-indexed) */ - column: number; - /** Severity level of the error */ - severity: "error" | "warning"; -} - -/** - * Result of parsing a SQL statement - */ -export interface SqlParseResult { - /** Whether parsing was successful */ - success: boolean; - /** Array of parsing errors, if any */ - errors: SqlParseError[]; - /** The parsed AST if successful */ - ast?: unknown; +interface NodeSqlParserOptions { + getParserOptions?: (state: EditorState) => Option; } /** * A SQL parser wrapper around node-sql-parser with enhanced error handling * and validation capabilities for CodeMirror integration. + * + * @example Custom dialect + * ```ts + * import { NodeSqlParser } from "@marimo-team/codemirror-sql"; + * + * const myParser = new NodeSqlParser({ + * getParserOptions: (state) => ({ + * dialect: getDialect(state), + * parseOptions: { + * includeLocations: true, + * }, + * }), + * }); + * ``` */ -export class SqlParser { +export class NodeSqlParser implements SqlParser { + private opts: NodeSqlParserOptions; + + constructor(opts: NodeSqlParserOptions = {}) { + this.opts = opts; + } + /** * Lazy import of the node-sql-parser package and create a new Parser instance. */ @@ -39,10 +40,11 @@ export class SqlParser { return new Parser(); }); - async parse(sql: string): Promise { + async parse(sql: string, opts: { state: EditorState }): Promise { try { + const parserOptions = this.opts.getParserOptions?.(opts.state); const parser = await this.getParser(); - const ast = parser.astify(sql); + const ast = parser.astify(sql, parserOptions); return { success: true, @@ -102,8 +104,8 @@ export class SqlParser { .trim(); } - async validateSql(sql: string): Promise { - const result = await this.parse(sql); + async validateSql(sql: string, opts: { state: EditorState }): Promise { + const result = await this.parse(sql, opts); return result.errors; } } diff --git a/src/sql/structure-analyzer.ts b/src/sql/structure-analyzer.ts index d46bfb3..7f96ef6 100644 --- a/src/sql/structure-analyzer.ts +++ b/src/sql/structure-analyzer.ts @@ -1,5 +1,5 @@ import type { EditorState } from "@codemirror/state"; -import { SqlParser } from "./parser.js"; +import type { SqlParser } from "./types.js"; /** * Represents a SQL statement with position information @@ -29,8 +29,8 @@ export class SqlStructureAnalyzer { private parser: SqlParser; private cache = new Map(); - constructor() { - this.parser = new SqlParser(); + constructor(parser: SqlParser) { + this.parser = parser; } /** @@ -111,7 +111,7 @@ export class SqlStructureAnalyzer { } // Parse the statement to determine validity and type (use stripped content) - const parseResult = await this.parser.parse(strippedContent); + const parseResult = await this.parser.parse(strippedContent, { state }); const type = this.determineStatementType(strippedContent); // Remove trailing semicolon from content for cleaner display diff --git a/src/sql/structure-extension.ts b/src/sql/structure-extension.ts index ae43444..78ff84d 100644 --- a/src/sql/structure-extension.ts +++ b/src/sql/structure-extension.ts @@ -1,6 +1,8 @@ import { type Extension, RangeSet, StateEffect, StateField } from "@codemirror/state"; import { EditorView, GutterMarker, gutter, type ViewUpdate } from "@codemirror/view"; +import { NodeSqlParser } from "./parser.js"; import { type SqlStatement, SqlStructureAnalyzer } from "./structure-analyzer.js"; +import type { SqlParser } from "./types.js"; export interface SqlGutterConfig { /** Background color for the current statement indicator */ @@ -21,6 +23,8 @@ export interface SqlGutterConfig { hideWhenNotFocused?: boolean; /** Opacity when editor is not focused (overrides hideWhenNotFocused if set) */ unfocusedOpacity?: number; + /** Custom SQL parser instance to use for analysis */ + parser?: SqlParser; } interface SqlGutterState { @@ -230,7 +234,8 @@ function createSqlGutter(config: SqlGutterConfig): Extension { * indicators for other statements. */ export function sqlStructureGutter(config: SqlGutterConfig = {}): Extension[] { - const analyzer = new SqlStructureAnalyzer(); + const parser = config.parser || new NodeSqlParser(); + const analyzer = new SqlStructureAnalyzer(parser); return [ sqlGutterStateField, diff --git a/src/sql/types.ts b/src/sql/types.ts new file mode 100644 index 0000000..a4dc5a4 --- /dev/null +++ b/src/sql/types.ts @@ -0,0 +1,44 @@ +import type { EditorState } from "@codemirror/state"; + +/** + * Represents a SQL parsing error with location information + */ +export interface SqlParseError { + /** Error message describing the issue */ + message: string; + /** Line number where the error occurred (1-indexed) */ + line: number; + /** Column number where the error occurred (1-indexed) */ + column: number; + /** Severity level of the error */ + severity: "error" | "warning"; +} +/** + * Result of parsing a SQL statement + */ + +export interface SqlParseResult { + /** Whether parsing was successful */ + success: boolean; + /** Array of parsing errors, if any */ + errors: SqlParseError[]; + /** The parsed AST if successful */ + ast?: unknown; +} + +export interface SqlParser { + /** + * Parse a SQL statement and return the AST + * @param sql - The SQL statement to parse + * @param opts - The options for the parser + * @returns The parsed AST + */ + parse(sql: string, opts: { state: EditorState }): Promise; + /** + * Validate a SQL statement and return any errors + * @param sql - The SQL statement to validate + * @param opts - The options for the parser + * @returns An array of errors + */ + validateSql(sql: string, opts: { state: EditorState }): Promise; +}