Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ const editor = new EditorView({
autocomplete: cteCompletionSource,
}),
sqlExtension({
sqlParser: new SqlParser({ dialect: "MySQL" }),
linterConfig: {
delay: 250 // Validation delay in ms
},
Expand Down
4 changes: 4 additions & 0 deletions demo/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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!
Expand Down Expand Up @@ -109,6 +110,9 @@ function initializeEditor() {
upperCaseKeywords: true,
}),
sqlExtension({
// Parser definition
sqlParser: new SqlParser({ dialect: "PostgresQL" }),

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i don't think we need to separate this from each config.

you can still do:

const sqlParser = new SqlParser({ dialect: "PostgresQL" }),

linterConfig: {sqlParser},
gutterConfig: {sqlParser},

and you still get great composition


// Linter extension configuration
linterConfig: {
delay: 250, // Delay before running validation
Expand Down
19 changes: 8 additions & 11 deletions src/sql/__tests__/diagnostics.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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) => {
Expand All @@ -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();
});

Expand All @@ -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();
});
});
20 changes: 19 additions & 1 deletion src/sql/__tests__/parser.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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", () => {
Expand Down Expand Up @@ -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);
});
});
4 changes: 3 additions & 1 deletion src/sql/__tests__/structure-analyzer.test.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,15 @@
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", () => {
let analyzer: SqlStructureAnalyzer;
let state: EditorState;

beforeEach(() => {
analyzer = new SqlStructureAnalyzer();
const parser = new SqlParser({ dialect: "PostgresQL" });
analyzer = new SqlStructureAnalyzer(parser);
});

const createState = (content: string) => {
Expand Down
19 changes: 11 additions & 8 deletions src/sql/__tests__/structure-extension.test.ts
Original file line number Diff line number Diff line change
@@ -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 {
Expand All @@ -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);
});
Expand All @@ -36,39 +39,39 @@ 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);
});

it("should handle whenHide configuration", () => {
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);
});
});
10 changes: 3 additions & 7 deletions src/sql/diagnostics.ts
Original file line number Diff line number Diff line change
@@ -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;

Expand All @@ -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;
}

/**
Expand Down Expand Up @@ -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;
Expand All @@ -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,
},
);
}
23 changes: 18 additions & 5 deletions src/sql/extension.ts
Original file line number Diff line number Diff line change
@@ -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 */
Expand Down Expand Up @@ -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) {
Expand Down
28 changes: 26 additions & 2 deletions src/sql/parser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
6 changes: 3 additions & 3 deletions src/sql/structure-analyzer.ts
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -29,8 +29,8 @@ export class SqlStructureAnalyzer {
private parser: SqlParser;
private cache = new Map<string, SqlStatement[]>();

constructor() {
this.parser = new SqlParser();
constructor(parser: SqlParser) {
this.parser = parser;
}

/**
Expand Down
5 changes: 3 additions & 2 deletions src/sql/structure-extension.ts
Original file line number Diff line number Diff line change
@@ -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 {
Expand Down Expand Up @@ -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,
Expand Down