Skip to content
Merged
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
2 changes: 1 addition & 1 deletion src/__tests__/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ describe("index.ts exports", () => {
const sortedExports = Object.keys(exports).sort();
expect(sortedExports).toMatchInlineSnapshot(`
[
"SqlParser",
"NodeSqlParser",
"SqlStructureAnalyzer",
"cteCompletionSource",
"sqlExtension",
Expand Down
3 changes: 2 additions & 1 deletion src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
4 changes: 2 additions & 2 deletions src/sql/__tests__/parser.test.ts
Original file line number Diff line number Diff line change
@@ -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 () => {
Expand Down
3 changes: 2 additions & 1 deletion src/sql/__tests__/structure-analyzer.test.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
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", () => {
let analyzer: SqlStructureAnalyzer;
let state: EditorState;

beforeEach(() => {
analyzer = new SqlStructureAnalyzer();
analyzer = new SqlStructureAnalyzer(new NodeSqlParser());
});

const createState = (content: string) => {
Expand Down
11 changes: 6 additions & 5 deletions src/sql/diagnostics.ts
Original file line number Diff line number Diff line change
@@ -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;

Expand Down Expand Up @@ -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<Diagnostic[]> => {
Expand All @@ -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));
},
Expand Down
60 changes: 31 additions & 29 deletions src/sql/parser.ts
Original file line number Diff line number Diff line change
@@ -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.
*/
Expand All @@ -39,10 +40,11 @@ export class SqlParser {
return new Parser();
});

async parse(sql: string): Promise<SqlParseResult> {
async parse(sql: string, opts: { state: EditorState }): Promise<SqlParseResult> {
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,
Expand Down Expand Up @@ -102,8 +104,8 @@ export class SqlParser {
.trim();
}

async validateSql(sql: string): Promise<SqlParseError[]> {
const result = await this.parse(sql);
async validateSql(sql: string, opts: { state: EditorState }): Promise<SqlParseError[]> {
const result = await this.parse(sql, opts);
return result.errors;
}
}
8 changes: 4 additions & 4 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 "./types.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 Expand Up @@ -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
Expand Down
7 changes: 6 additions & 1 deletion src/sql/structure-extension.ts
Original file line number Diff line number Diff line change
@@ -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 */
Expand All @@ -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 {
Expand Down Expand Up @@ -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,
Expand Down
44 changes: 44 additions & 0 deletions src/sql/types.ts
Original file line number Diff line number Diff line change
@@ -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<SqlParseResult>;
/**
* 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<SqlParseError[]>;
}