diff --git a/README.md b/README.md index 3a38902..000c6ac 100644 --- a/README.md +++ b/README.md @@ -44,6 +44,7 @@ const editor = new EditorView({ autocomplete: cteCompletionSource, }), sqlExtension({ + sqlParser: new SqlParser({ dialect: "MySQL" }), linterConfig: { delay: 250 // Validation delay in ms }, diff --git a/demo/index.ts b/demo/index.ts index 3147b7d..f21b2ab 100644 --- a/demo/index.ts +++ b/demo/index.ts @@ -2,6 +2,7 @@ import { StandardSQL, sql } from "@codemirror/lang-sql"; import { basicSetup, EditorView } from "codemirror"; import { cteCompletionSource } from "../src/sql/cte-completion-source.js"; import { sqlExtension } from "../src/sql/extension.js"; +import { SqlParser } from "../src/sql/parser.js"; // Default SQL content for the demo const defaultSqlDoc = `-- Welcome to the SQL Editor Demo! @@ -109,6 +110,9 @@ function initializeEditor() { upperCaseKeywords: true, }), sqlExtension({ + // Parser definition + sqlParser: new SqlParser({ dialect: "PostgresQL" }), + // Linter extension configuration linterConfig: { delay: 250, // Delay before running validation diff --git a/src/sql/__tests__/diagnostics.test.ts b/src/sql/__tests__/diagnostics.test.ts index 6394edd..ac2e6e5 100644 --- a/src/sql/__tests__/diagnostics.test.ts +++ b/src/sql/__tests__/diagnostics.test.ts @@ -2,7 +2,9 @@ import { Text } from "@codemirror/state"; import type { EditorView } from "@codemirror/view"; import { describe, expect, it, vi } from "vitest"; import { sqlLinter } from "../diagnostics.js"; -import type { SqlParser } from "../parser.js"; +import { SqlParser } from "../parser.js"; + +const defaultParser = new SqlParser({ dialect: "PostgresQL" }); // Mock EditorView const _createMockView = (content: string) => { @@ -14,12 +16,12 @@ const _createMockView = (content: string) => { describe("sqlLinter", () => { it("should create a linter extension", () => { - const linter = sqlLinter(); + const linter = sqlLinter(defaultParser); expect(linter).toBeDefined(); }); it("should accept configuration with custom delay", () => { - const linter = sqlLinter({ delay: 1000 }); + const linter = sqlLinter(defaultParser, { delay: 1000 }); expect(linter).toBeDefined(); }); @@ -29,22 +31,17 @@ describe("sqlLinter", () => { parseSql: vi.fn(() => ({ statements: [] })), } as unknown as SqlParser; - const linter = sqlLinter({ parser: mockParser }); + const linter = sqlLinter(mockParser); expect(linter).toBeDefined(); }); it("should use default delay when no delay provided", () => { - const linter = sqlLinter(); + const linter = sqlLinter(defaultParser); expect(linter).toBeDefined(); }); it("should use custom delay when provided", () => { - const linter = sqlLinter({ delay: 500 }); - expect(linter).toBeDefined(); - }); - - it("should use default parser when no parser provided", () => { - const linter = sqlLinter(); + const linter = sqlLinter(defaultParser, { delay: 500 }); expect(linter).toBeDefined(); }); }); diff --git a/src/sql/__tests__/parser.test.ts b/src/sql/__tests__/parser.test.ts index 7afcd49..7820ed2 100644 --- a/src/sql/__tests__/parser.test.ts +++ b/src/sql/__tests__/parser.test.ts @@ -2,7 +2,7 @@ import { describe, expect, it } from "vitest"; import { SqlParser } from "../parser.js"; describe("SqlParser", () => { - const parser = new SqlParser(); + const parser = new SqlParser({ dialect: "PostgresQL" }); describe("parse", () => { it("should parse valid SQL successfully", () => { @@ -67,3 +67,21 @@ describe("SqlParser", () => { }); }); }); + +describe("Dialects", () => { + it("should parse valid SQL based on dialect", () => { + const sql = "SELECT * FROM table_function();"; + + const parser = new SqlParser({ dialect: "PostgresQL" }); + const result = parser.parse(sql); + expect(result.success).toBe(true); + expect(result.errors).toHaveLength(0); + expect(result.ast).toBeDefined(); + + // MySQL does not support calling functions as tables + const mysqlParser = new SqlParser({ dialect: "MySQL" }); + const mysqlResult = mysqlParser.parse(sql); + expect(mysqlResult.success).toBe(false); + expect(mysqlResult.errors).toHaveLength(1); + }); +}); diff --git a/src/sql/__tests__/structure-analyzer.test.ts b/src/sql/__tests__/structure-analyzer.test.ts index 9337bc5..1a77f47 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 { SqlParser } from "../parser.js"; import { SqlStructureAnalyzer } from "../structure-analyzer.js"; describe("SqlStructureAnalyzer", () => { @@ -7,7 +8,8 @@ describe("SqlStructureAnalyzer", () => { let state: EditorState; beforeEach(() => { - analyzer = new SqlStructureAnalyzer(); + const parser = new SqlParser({ dialect: "PostgresQL" }); + analyzer = new SqlStructureAnalyzer(parser); }); const createState = (content: string) => { diff --git a/src/sql/__tests__/structure-extension.test.ts b/src/sql/__tests__/structure-extension.test.ts index 2249d84..57f0fef 100644 --- a/src/sql/__tests__/structure-extension.test.ts +++ b/src/sql/__tests__/structure-extension.test.ts @@ -1,14 +1,17 @@ import { EditorState, Text } from "@codemirror/state"; import type { EditorView } from "@codemirror/view"; import { describe, expect, it } from "vitest"; +import { SqlParser } from "../parser.js"; import { sqlStructureGutter } from "../structure-extension.js"; +const defaultParser = new SqlParser({ dialect: "PostgresQL" }); + // Mock EditorView const _createMockView = (content: string, hasFocus = true) => { const doc = Text.of(content.split("\n")); const state = EditorState.create({ doc, - extensions: [sqlStructureGutter()], + extensions: [sqlStructureGutter(defaultParser)], }); return { @@ -20,7 +23,7 @@ const _createMockView = (content: string, hasFocus = true) => { describe("sqlStructureGutter", () => { it("should create a gutter extension with default config", () => { - const extensions = sqlStructureGutter(); + const extensions = sqlStructureGutter(defaultParser); expect(Array.isArray(extensions)).toBe(true); expect(extensions.length).toBeGreaterThan(0); }); @@ -36,25 +39,25 @@ describe("sqlStructureGutter", () => { hideWhenNotFocused: true, }; - const extensions = sqlStructureGutter(config); + const extensions = sqlStructureGutter(defaultParser, config); expect(Array.isArray(extensions)).toBe(true); expect(extensions.length).toBeGreaterThan(0); }); it("should handle empty configuration", () => { - const extensions = sqlStructureGutter({}); + const extensions = sqlStructureGutter(defaultParser); expect(Array.isArray(extensions)).toBe(true); }); it("should create extensions for all required parts", () => { - const extensions = sqlStructureGutter(); + const extensions = sqlStructureGutter(defaultParser); // Should include state field, update listener, theme, and gutter expect(extensions.length).toBe(4); }); it("should handle unfocusedOpacity configuration", () => { const config = { unfocusedOpacity: 0.2 }; - const extensions = sqlStructureGutter(config); + const extensions = sqlStructureGutter(defaultParser, config); expect(extensions.length).toBe(4); }); @@ -62,13 +65,13 @@ describe("sqlStructureGutter", () => { const config = { whenHide: (view: EditorView) => view.state.doc.length === 0, }; - const extensions = sqlStructureGutter(config); + const extensions = sqlStructureGutter(defaultParser, config); expect(extensions.length).toBe(4); }); it("should work with minimal configuration", () => { const config = { width: 2 }; - const extensions = sqlStructureGutter(config); + const extensions = sqlStructureGutter(defaultParser, config); expect(extensions.length).toBe(4); }); }); diff --git a/src/sql/diagnostics.ts b/src/sql/diagnostics.ts index 93c2465..5c7c361 100644 --- a/src/sql/diagnostics.ts +++ b/src/sql/diagnostics.ts @@ -1,7 +1,7 @@ import { type Diagnostic, linter } from "@codemirror/lint"; import type { Text } from "@codemirror/state"; import type { EditorView } from "@codemirror/view"; -import { type SqlParseError, SqlParser } from "./parser.js"; +import type { SqlParseError, SqlParser } from "./parser.js"; const DEFAULT_DELAY = 750; @@ -11,8 +11,6 @@ const DEFAULT_DELAY = 750; export interface SqlLinterConfig { /** Delay in milliseconds before running validation (default: 750) */ delay?: number; - /** Custom SQL parser instance to use for validation */ - parser?: SqlParser; } /** @@ -48,9 +46,7 @@ function convertToCodeMirrorDiagnostic(error: SqlParseError, doc: Text): Diagnos * }); * ``` */ -export function sqlLinter(config: SqlLinterConfig = {}) { - const parser = config.parser || new SqlParser(); - +export function sqlLinter(parser: SqlParser, config: SqlLinterConfig = {}) { return linter( (view: EditorView): Diagnostic[] => { const doc = view.state.doc; @@ -65,7 +61,7 @@ export function sqlLinter(config: SqlLinterConfig = {}) { return errors.map((error) => convertToCodeMirrorDiagnostic(error, doc)); }, { - delay: config.delay || DEFAULT_DELAY, + delay: config.delay ?? DEFAULT_DELAY, }, ); } diff --git a/src/sql/extension.ts b/src/sql/extension.ts index e04d9b6..e21793f 100644 --- a/src/sql/extension.ts +++ b/src/sql/extension.ts @@ -1,12 +1,20 @@ import type { Extension } from "@codemirror/state"; import { type SqlLinterConfig, sqlLinter } from "./diagnostics.js"; import { type SqlHoverConfig, sqlHover, sqlHoverTheme } from "./hover.js"; +import { SqlParser } from "./parser.js"; import { type SqlGutterConfig, sqlStructureGutter } from "./structure-extension.js"; /** * Configuration options for the SQL extension */ export interface SqlExtensionConfig { + /** + * The SQL parser used for linting and gutter markers. + * If not provided, a default PostgreSQL parser is created. + * The parser instance is shared across the extension. + */ + sqlParser?: SqlParser; + /** Whether to enable SQL linting (default: true) */ enableLinting?: boolean; /** Configuration for the SQL linter */ @@ -53,17 +61,22 @@ export function sqlExtension(config: SqlExtensionConfig = {}): Extension[] { enableLinting = true, enableGutterMarkers = true, enableHover = true, + sqlParser, linterConfig, gutterConfig, hoverConfig, } = config; - if (enableLinting) { - extensions.push(sqlLinter(linterConfig)); - } + if (enableLinting || enableGutterMarkers) { + const parser = sqlParser ?? new SqlParser({ dialect: "PostgresQL" }); + + if (enableLinting) { + extensions.push(sqlLinter(parser, linterConfig)); + } - if (enableGutterMarkers) { - extensions.push(sqlStructureGutter(gutterConfig)); + if (enableGutterMarkers) { + extensions.push(sqlStructureGutter(parser, gutterConfig)); + } } if (enableHover) { diff --git a/src/sql/parser.ts b/src/sql/parser.ts index d2f51d6..4acca60 100644 --- a/src/sql/parser.ts +++ b/src/sql/parser.ts @@ -26,20 +26,44 @@ export interface SqlParseResult { ast?: unknown; } +// https://github.com/taozhi8833998/node-sql-parser?tab=readme-ov-file#supported-database-sql-syntax +export type SupportedDialects = + | "Athena" + | "BigQuery" + | "DB2" + | "Hive" + | "MariaDB" + | "MySQL" + | "PostgresQL" + | "Redshift" + | "Sqlite" + | "TransactSQL" + | "FlinkSQL" + | "Snowflake" + | "Noql"; + +export interface SqlParserConfig { + /** The database dialect to use for parsing */ + dialect: SupportedDialects; +} + /** * A SQL parser wrapper around node-sql-parser with enhanced error handling * and validation capabilities for CodeMirror integration. */ export class SqlParser { private parser: Parser; + private dialect: SupportedDialects; - constructor() { + constructor(config: SqlParserConfig) { this.parser = new Parser(); + this.dialect = config.dialect; } parse(sql: string): SqlParseResult { try { - const ast = this.parser.astify(sql); + const dialect = this.dialect; + const ast = this.parser.astify(sql, { database: dialect }); return { success: true, diff --git a/src/sql/structure-analyzer.ts b/src/sql/structure-analyzer.ts index f99afba..2acae8d 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 "./parser.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; } /** diff --git a/src/sql/structure-extension.ts b/src/sql/structure-extension.ts index 1ab1458..0c5f8e4 100644 --- a/src/sql/structure-extension.ts +++ b/src/sql/structure-extension.ts @@ -1,5 +1,6 @@ import { type Extension, RangeSet, StateEffect, StateField } from "@codemirror/state"; import { EditorView, GutterMarker, gutter, type ViewUpdate } from "@codemirror/view"; +import type { SqlParser } from "./parser.js"; import { type SqlStatement, SqlStructureAnalyzer } from "./structure-analyzer.js"; export interface SqlGutterConfig { @@ -229,8 +230,8 @@ function createSqlGutter(config: SqlGutterConfig): Extension { * based on cursor position. Highlights the current statement and shows dimmed * indicators for other statements. */ -export function sqlStructureGutter(config: SqlGutterConfig = {}): Extension[] { - const analyzer = new SqlStructureAnalyzer(); +export function sqlStructureGutter(parser: SqlParser, config: SqlGutterConfig = {}): Extension[] { + const analyzer = new SqlStructureAnalyzer(parser); return [ sqlGutterStateField,