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
10 changes: 9 additions & 1 deletion demo/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ <h2 class="text-2xl font-bold mb-4">SQL Editor with Diagnostics</h2>
<option value="SQLite">SQLite</option>
<option value="MariaDB">MariaDB</option>
<option value="Snowflake">Snowflake</option>
<option value="DuckDB">DuckDB</option>
</select>
</div>

Expand All @@ -63,6 +64,9 @@ <h3 class="font-semibold text-lg">Valid Queries</h3>
<button class="example-btn w-full p-3 text-left bg-green-50 hover:bg-green-100 border border-green-200 rounded">
<code class="text-sm">INSERT INTO products (name, price) VALUES ('Laptop', 999.99)</code>
</button>
<button class="example-btn w-full p-3 text-left bg-blue-50 hover:bg-blue-100 border border-blue-200 rounded">
<code class="text-sm">from nyc.rideshare select * limit 100</code>
</button>
</div>
<div class="space-y-2">
<h3 class="font-semibold text-lg">Invalid Queries (will show errors)</h3>
Expand Down Expand Up @@ -92,7 +96,11 @@ <h3 class="font-semibold mb-2">Error Highlighting</h3>
</div>
<div class="p-4 bg-blue-50 rounded-lg">
<h3 class="font-semibold mb-2">Multiple SQL Dialects</h3>
<p class="text-sm text-gray-700">Supports MySQL, PostgreSQL, MariaDB, and SQLite syntax validation.</p>
<p class="text-sm text-gray-700">Supports MySQL, PostgreSQL, MariaDB, SQLite, Snowflake, and DuckDB syntax validation.</p>
</div>
<div class="p-4 bg-blue-50 rounded-lg">
<h3 class="font-semibold mb-2">DuckDB Support</h3>
<p class="text-sm text-gray-700">Special support for DuckDB-specific syntax like "from table select * limit 100".</p>
</div>
<div class="p-4 bg-blue-50 rounded-lg">
<h3 class="font-semibold mb-2">TypeScript Support</h3>
Expand Down
4 changes: 2 additions & 2 deletions demo/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { acceptCompletion } from "@codemirror/autocomplete";
import { keywordCompletionSource, MariaSQL, PostgreSQL, SQLite, sql } from "@codemirror/lang-sql";
import { type EditorState, Facet, StateEffect, StateField } from "@codemirror/state";
import { PostgreSQL, sql } from "@codemirror/lang-sql";
import { type EditorState, StateEffect, StateField } from "@codemirror/state";
import { keymap } from "@codemirror/view";
import { basicSetup, EditorView } from "codemirror";
import { NodeSqlParser } from "../src/index.js";
Expand Down
76 changes: 76 additions & 0 deletions src/sql/__tests__/parser.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -110,4 +110,80 @@ describe("SqlParser", () => {
expect(errors[0]).toHaveProperty("severity");
});
});

describe("DuckDB dialect support", () => {
it("should accept DuckDB-specific syntax without parsing", async () => {
const duckdbParser = new NodeSqlParser({
getParserOptions: () => ({
database: "DuckDB",
}),
});

const state = EditorState.create({
doc: "from nyc.rideshare select * limit 100",
});

const result = await duckdbParser.parse("from nyc.rideshare select * limit 100", { state });

expect(result.success).toBe(true);
expect(result.errors).toHaveLength(0);
expect(result.ast).toBeNull();
});

it("should still parse standard SQL with DuckDB dialect", async () => {
const duckdbParser = new NodeSqlParser({
getParserOptions: () => ({
database: "DuckDB",
}),
});

const state = EditorState.create({
doc: "SELECT * FROM users WHERE id = 1",
});

const result = await duckdbParser.parse("SELECT * FROM users WHERE id = 1", { state });

expect(result.success).toBe(true);
expect(result.errors).toHaveLength(0);
});

it("should accept complex DuckDB queries without parsing", async () => {
const duckdbParser = new NodeSqlParser({
getParserOptions: () => ({
database: "DuckDB",
}),
});

const state = EditorState.create({
doc: "from nyc.rideshare select pickup_datetime, dropoff_datetime limit 50",
});

const result = await duckdbParser.parse(
"from nyc.rideshare select pickup_datetime, dropoff_datetime limit 50",
{ state },
);

expect(result.success).toBe(true);
expect(result.errors).toHaveLength(0);
expect(result.ast).toBeNull();
});

it("should accept DuckDB queries with semicolons without parsing", async () => {
const duckdbParser = new NodeSqlParser({
getParserOptions: () => ({
database: "DuckDB",
}),
});

const state = EditorState.create({
doc: "from posts select title, name;",
});

const result = await duckdbParser.parse("from posts select title, name;", { state });

expect(result.success).toBe(true);
expect(result.errors).toHaveLength(0);
expect(result.ast).toBeNull();
});
});
});
45 changes: 45 additions & 0 deletions src/sql/parser.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import type { EditorState } from "@codemirror/state";
import type { Option } from "node-sql-parser";
import { debug } from "../debug.js";
import { lazy } from "../utils.js";
import type { SqlParseError, SqlParseResult, SqlParser } from "./types.js";

Expand Down Expand Up @@ -44,6 +45,12 @@ export class NodeSqlParser implements SqlParser {
try {
const parserOptions = this.opts.getParserOptions?.(opts.state);
const parser = await this.getParser();

// Check if this is DuckDB dialect and apply custom processing
if (parserOptions?.database === "DuckDB") {
return this.parseWithDuckDBSupport(sql, parserOptions);
}

const ast = parser.astify(sql, parserOptions);

return {
Expand All @@ -60,6 +67,44 @@ export class NodeSqlParser implements SqlParser {
}
}

/**
* Parse SQL with DuckDB-specific syntax support
*/
private async parseWithDuckDBSupport(
sql: string,
parserOptions: Option,
): Promise<SqlParseResult> {
const parser = await this.getParser();

// If the query starts with "from", it's DuckDB-specific syntax
// Just return success without parsing to avoid errors
if (sql.trim().toLowerCase().startsWith("from")) {
debug("From syntax is not supported");
return {
success: true,
errors: [],
ast: null,
};
}

// Otherwise, try standard parsing with PostgreSQL dialect
try {
const postgresOptions = { ...parserOptions, database: "PostgresQL" };
const ast = parser.astify(sql, postgresOptions);
return {
success: true,
errors: [],
ast,
};
} catch (error) {
const parseError = this.extractErrorInfo(error, sql);
return {
success: false,
errors: [parseError],
};
}
}

private extractErrorInfo(error: unknown, _sql: string): SqlParseError {
let line = 1;
let column = 1;
Expand Down