diff --git a/README.md b/README.md index cfbc1b9..3293075 100644 --- a/README.md +++ b/README.md @@ -161,7 +161,15 @@ which can be found in the `src/data` directory. ## Demo -See the [demo](https://marimo-team.github.io/codemirror-sql/) for a full example. +Run the interactive vNext playground locally: + +```bash +pnpm dev +``` + +The playground exercises relation, namespace, physical-column, CTE, correlated +scope, and `LATERAL` completion against an in-memory catalog. It also exposes +provider latency and catalog invalidation controls. ## Development diff --git a/demo/data.ts b/demo/data.ts index 79b3694..a4b271a 100644 --- a/demo/data.ts +++ b/demo/data.ts @@ -1,92 +1,75 @@ -import type { Schema } from "./custom-renderers.js"; +export interface DemoTable { + readonly columns: readonly { + readonly name: string; + readonly type: string; + }[]; + readonly description: string; + readonly name: string; + readonly schema: string; +} -// Default SQL content for the demo -export const defaultSqlDoc = `-- Welcome to the SQL Editor Demo! --- Try editing the queries below to see real-time validation +export const demoTables: readonly DemoTable[] = [ + { + columns: [ + { name: "id", type: "BIGINT" }, + { name: "name", type: "VARCHAR" }, + { name: "email", type: "VARCHAR" }, + { name: "active", type: "BOOLEAN" }, + { name: "created_at", type: "TIMESTAMP" }, + ], + description: "Application users", + name: "users", + schema: "main", + }, + { + columns: [ + { name: "id", type: "BIGINT" }, + { name: "user_id", type: "BIGINT" }, + { name: "title", type: "VARCHAR" }, + { name: "published", type: "BOOLEAN" }, + { name: "created_at", type: "TIMESTAMP" }, + ], + description: "Published and draft posts", + name: "posts", + schema: "main", + }, + { + columns: [ + { name: "id", type: "BIGINT" }, + { name: "customer_id", type: "BIGINT" }, + { name: "order_date", type: "DATE" }, + { name: "total_amount", type: "DECIMAL(18, 2)" }, + { name: "status", type: "VARCHAR" }, + ], + description: "Customer orders", + name: "orders", + schema: "sales", + }, + { + columns: [ + { name: "id", type: "BIGINT" }, + { name: "first_name", type: "VARCHAR" }, + { name: "last_name", type: "VARCHAR" }, + { name: "email", type: "VARCHAR" }, + { name: "country", type: "VARCHAR" }, + ], + description: "Customer directory", + name: "customers", + schema: "sales", + }, +] as const; --- Put the cursor on a CTE name or alias to highlight its references; --- Cmd/Ctrl-click (or F12) jumps to the definition, F2 renames. -WITH recent_orders AS ( - SELECT customer_id, total_amount FROM orders WHERE order_date >= '2024-01-01' -), -top_customers AS ( - SELECT customer_id, SUM(total_amount) AS total_spent - FROM recent_orders - GROUP BY customer_id -) -SELECT c.first_name, t.total_spent AS amount -FROM customers c -JOIN top_customers t ON t.customer_id = c.id -ORDER BY amount DESC; +export const defaultSqlDoc = `-- vNext SQL language-service playground +-- Press Ctrl-Space anywhere completion is expected. --- Valid queries (no errors): -SELECT id, name, email -FROM users -WHERE active = true -ORDER BY created_at DESC; +SELECT u. +FROM main.users AS u +WHERE EXISTS ( + SELECT 1 + FROM sales.orders AS o + WHERE o.customer_id = u.id +); -SELECT - u.name, - p.title, - p.created_at -FROM users u -JOIN posts p ON u.id = p.user_id -WHERE u.status = 'active' - AND p.published = true -LIMIT 10; - --- Try editing these to create syntax errors: --- Uncomment the lines below to see error highlighting - --- SELECT * FROM; -- Missing table name --- SELECT * FORM users; -- Typo in FROM keyword --- INSERT INTO VALUES (1, 2); -- Missing table name --- UPDATE SET name = 'test'; -- Missing table name - --- Complex example with subquery: -SELECT - customer_id, - order_date, - total_amount, - (SELECT AVG(total_amount) FROM orders) as avg_order_value -FROM orders -WHERE order_date >= '2024-01-01' - AND total_amount > ( - SELECT AVG(total_amount) * 0.8 - FROM orders - WHERE YEAR(order_date) = 2024 - ) -ORDER BY total_amount DESC; +SELECT * +FROM sales. `; - -export const schema: Record = { - // Users table - users: ["id", "name", "email", "active", "status", "created_at", "updated_at", "profile_id"], - // Posts table - posts: [ - "id", - "title", - "content", - "user_id", - "published", - "created_at", - "updated_at", - "category_id", - ], - // Orders table - orders: [ - "id", - "customer_id", - "order_date", - "total_amount", - "status", - "shipping_address", - "created_at", - ], - // Customers table (additional example) - customers: ["id", "first_name", "last_name", "email", "phone", "address", "city", "country"], - // Categories table - categories: ["id", "name", "description", "parent_id"], - // Users_Posts table - Users_Posts: ["user_id", "post_id"], -}; diff --git a/demo/index.html b/demo/index.html index 339f5b0..63077c2 100644 --- a/demo/index.html +++ b/demo/index.html @@ -3,134 +3,104 @@ - codemirror-sql demo - + codemirror-sql vNext playground + + - -
-

- codemirror-sql -

-

by marimo

-

- A CodeMirror extension for SQL with real-time syntax validation and error diagnostics using - node-sql-parser. -

- -
-
-

SQL Editor with Diagnostics

-

- Try typing invalid SQL syntax to see real-time error highlighting and messages. - Valid tables are: users, posts, orders, customers, categories -

-

- Put the cursor on a CTE name or alias to highlight its references, - Cmd/Ctrl-click (or F12) to jump to its - definition, and press F2 to rename it everywhere in the statement. -

+ +
+
+
+

codemirror-sql vNext

+

The merged language service, running against a live in-memory catalog.

+
+ Experimental API +
-
- +
+
+
+

Interactive editor

+

Press Ctrl-Space for completion. The gutter marks statement boundaries.

- -
-
+
+ + +
-
+
+
+
+
+
+
-
-

Example Queries

-

Click any example to load it into the editor:

-
-
-

Valid Queries

- - - - - -
-
-

Invalid Queries (will show errors)

- - - -
-
-
+
+

Scenarios

+

The | marks where completion opens after loading an example.

+
+ + + + + + +
+
-
-

Features

-
-
-

Real-time Validation

-

SQL syntax is validated as you type with a configurable delay (750ms default).

-
-
-

Error Highlighting

-

Syntax errors are highlighted with red underlines and detailed error messages.

-
-
-

Multiple SQL Dialects

-

Supports MySQL, PostgreSQL, MariaDB, SQLite, Snowflake, and DuckDB syntax validation.

-
-
-

DuckDB Support

-

Special support for DuckDB-specific syntax like "from table select * limit 100".

-
-
-

CTE & Alias Navigation

-

- Go-to-definition (Cmd/Ctrl-click or F12), reference highlighting, and rename (F2) for CTE names, - table aliases, and select aliases. -

-
-
-

CTE Autocomplete

-

- Statement-scoped completion of CTE names and their output columns, including after - my_cte.. -

-
-
-

TypeScript Support

-

Full TypeScript support with comprehensive type definitions.

-
-
-
-
+
+ Known boundary: vNext resolves physical table columns, but does not yet infer output columns + produced by CTEs or derived tables. This playground deliberately exposes that next milestone. +
diff --git a/demo/index.ts b/demo/index.ts index 025fd77..503b6e4 100644 --- a/demo/index.ts +++ b/demo/index.ts @@ -1,326 +1,307 @@ -import { acceptCompletion } from "@codemirror/autocomplete"; -import { PostgreSQL, type SQLDialect, sql } from "@codemirror/lang-sql"; -import { Compartment, type EditorState, StateEffect, StateField } from "@codemirror/state"; -import { keymap } from "@codemirror/view"; +import { + acceptCompletion, + closeCompletion, + completionStatus, + startCompletion, +} from "@codemirror/autocomplete"; +import { PostgreSQL, sql, type SQLDialect } from "@codemirror/lang-sql"; +import { Compartment } from "@codemirror/state"; import { basicSetup, EditorView } from "codemirror"; import { - DefaultSqlTooltipRenders, - defaultSqlHoverTheme, - NodeSqlParser, - QueryContextAnalyzer, - type SqlKeywordInfo, - type SupportedDialects, - sqlCompletion, - sqlExtension, -} from "../src/index.js"; -import { tableTooltipRenderer } from "./custom-renderers.js"; -import { defaultSqlDoc, schema } from "./data.js"; -import { guessSqlDialect } from "./utils.js"; + bigQueryDialect, + createSqlLanguageService, + dremioDialect, + duckdbDialect, + postgresDialect, + type SqlCatalogEpoch, + type SqlCatalogRelation, + type SqlColumnCatalogProvider, + type SqlDocumentContext, + type SqlNamespaceCatalogProvider, + type SqlRelationCatalogProvider, +} from "../src/vnext/index.js"; +import { + sqlEditor, +} from "../src/vnext/codemirror/index.js"; +import { DuckDBDialect } from "../src/dialects/duckdb/duckdb.js"; +import { defaultSqlDoc, demoTables } from "./data.js"; -let editor: EditorView; +type DemoDialect = "bigquery" | "dremio" | "duckdb" | "postgresql"; + +interface DemoContext extends SqlDocumentContext { + readonly environment: "demo"; +} -const completionKindStyles = { - borderRadius: "4px", - padding: "2px 4px", - marginRight: "4px", - display: "inline-flex", - alignItems: "center", - justifyContent: "center", - width: "12px", - height: "12px", +const epoch: SqlCatalogEpoch = { + generation: 1, + token: "demo-catalog-v1", }; +let latencyMs = 0; +let catalogRequests = 0; +let columnRequests = 0; +let namespaceRequests = 0; + +function identifier(value: string) { + return { quoted: false as const, value }; +} + +async function delay(signal: AbortSignal): Promise { + if (latencyMs === 0) return; + await new Promise((resolve, reject) => { + const handle = setTimeout(resolve, latencyMs); + signal.addEventListener("abort", () => { + clearTimeout(handle); + reject(signal.reason); + }, { once: true }); + }); +} -const defaultDialect = PostgreSQL; +function updateStats(): void { + const node = document.querySelector("#provider-stats"); + if (node) { + node.textContent = + `${catalogRequests} relation · ${columnRequests} column · ` + + `${namespaceRequests} namespace requests`; + } +} -const defaultKeymap = [ - { - key: "Tab", - run: (view: EditorView) => { - // Try to accept completion first - if (acceptCompletion(view)) { - return true; - } - // In production, you can use @codemirror/commands.indentWithTab instead of custom logic - // If no completion to accept, insert a tab character - const { state } = view; - const { selection } = state; - if (selection.main.empty) { - // Insert tab at cursor position - view.dispatch({ - changes: { - from: selection.main.from, - insert: "\t", +const catalog: SqlRelationCatalogProvider = { + id: "vnext-demo-relations", + search: async (request, signal) => { + catalogRequests += 1; + updateStats(); + await delay(signal); + signal.throwIfAborted(); + const prefix = request.prefix.value.toLocaleLowerCase(); + const qualifier = request.qualifier.map((part) => + part.value.toLocaleLowerCase() + ); + const relations = demoTables + .filter((table) => + table.name.toLocaleLowerCase().startsWith(prefix) && + (qualifier.length === 0 || + qualifier.at(-1) === table.schema.toLocaleLowerCase()) + ) + .slice(0, request.limit) + .map((table): SqlCatalogRelation => ({ + canonicalPath: [ + { + ...identifier(table.schema), + role: "schema" as const, }, - selection: { - anchor: selection.main.from + 1, - head: selection.main.from + 1, + { + ...identifier(table.name), + role: "relation" as const, }, - }); - return true; - } - return false; - }, + ], + completionPathStart: qualifier.length === 0 ? 1 : 0, + detail: table.description, + entityId: `${table.schema}.${table.name}`, + matchQuality: "exact" as const, + relationKind: "table" as const, + })); + return { + coverage: { kind: "complete" }, + epoch, + relations, + status: "ready", + }; }, -]; +}; -// e.g. lazily load keyword docs -const getKeywordDocs = async (): Promise> => { - const keywords = await import("@marimo-team/codemirror-sql/data/common-keywords.json"); - const duckdbKeywords = await import("@marimo-team/codemirror-sql/data/duckdb-keywords.json"); - return { - ...keywords.default.keywords, - ...duckdbKeywords.default.keywords, - }; +const columns: SqlColumnCatalogProvider = { + id: "vnext-demo-columns", + loadColumns: async (request, signal) => { + columnRequests += 1; + updateStats(); + await delay(signal); + signal.throwIfAborted(); + return { + epoch, + relations: request.relations.map((relation) => { + const tableName = relation.path.at(-1)?.value.toLocaleLowerCase(); + const table = demoTables.find((candidate) => + candidate.name.toLocaleLowerCase() === tableName + ); + if (!table) { + return { + code: "unknown" as const, + requestKey: relation.requestKey, + retry: "after-invalidation" as const, + status: "failed" as const, + }; + } + return { + columns: table.columns.map((column, ordinal) => ({ + columnEntityId: `${table.schema}.${table.name}.${column.name}`, + dataType: column.type, + detail: `${column.type} · ${table.schema}.${table.name}`, + identifier: identifier(column.name), + insertText: column.name, + ordinal, + })), + coverage: "complete" as const, + relationEntityId: `${table.schema}.${table.name}`, + requestKey: relation.requestKey, + status: "ready" as const, + }; + }), + }; + }, }; -const setDatabase = StateEffect.define(); -const databaseField = StateField.define({ - create: () => "PostgreSQL", - update: (prevValue, transaction) => { - for (const effect of transaction.effects) { - if (effect.is(setDatabase)) { - return effect.value; - } - } - return prevValue; +const namespaces: SqlNamespaceCatalogProvider = { + id: "vnext-demo-namespaces", + search: async (request, signal) => { + namespaceRequests += 1; + updateStats(); + await delay(signal); + signal.throwIfAborted(); + const prefix = request.prefix.value.toLocaleLowerCase(); + return { + containers: ["main", "sales"] + .filter((name) => name.startsWith(prefix)) + .map((name) => ({ + canonicalPath: [{ + ...identifier(name), + role: "schema" as const, + }], + containerEntityId: `schema:${name}`, + detail: `Demo ${name} schema`, + insertText: name, + matchQuality: "exact" as const, + })), + coverage: "complete", + epoch, + status: "ready", + }; }, +}; + +const service = createSqlLanguageService({ + catalog, + columns, + completion: { catalogResponseBudgetMs: 40 }, + dialects: [ + bigQueryDialect(), + dremioDialect(), + duckdbDialect(), + postgresDialect(), + ], + namespaces, }); -// Allows us to reconfigure the base sql extension without reloading the editor -const baseSqlCompartment = new Compartment(); +let dialect: DemoDialect = "duckdb"; +const context = (): DemoContext => ({ + catalog: { + scope: `demo-connection:${dialect}`, + searchPath: [[identifier("main")]], + }, + dialect, + environment: "demo", +}); -const baseSqlExtension = (dialect: SQLDialect) => { - return sql({ - dialect: dialect, - // Example schema for autocomplete - schema: schema, - // Enable uppercase keywords for more traditional SQL style - upperCaseKeywords: true, - keywordCompletion: (label, _type) => { - return { - label, - keyword: label, - info: async () => { - const dom = document.createElement("div"); - const keywordDocs = await getKeywordDocs(); - const description = keywordDocs[label.toLocaleLowerCase()]; - if (!description) { - return null; - } - dom.innerHTML = DefaultSqlTooltipRenders.keyword({ - keyword: label, - info: description, - }); - return dom; - }, - }; +const support = sqlEditor({ + autocomplete: { + infoResolver: (item) => { + const dom = document.createElement("div"); + dom.className = "vnext-completion-info"; + const title = document.createElement("strong"); + title.textContent = item.label; + const detail = document.createElement("div"); + detail.textContent = item.detail ?? `${item.kind} completion`; + dom.append(title, detail); + return { destroy: () => undefined, dom }; }, - }); -}; + }, + initialContext: context(), + service, + statementGutter: { + hideWhenNotFocused: false, + showInactive: true, + }, +}); -// Initialize the SQL editor -function initializeEditor() { - // Use the same parser - const parser = new NodeSqlParser({ - getParserOptions: (state: EditorState) => { - return { - database: getDatabase(state), - }; - }, - }); - // Shared between the completion sources so each edit is analyzed once - const contextAnalyzer = new QueryContextAnalyzer(parser); +const syntax = new Compartment(); +function syntaxDialect(value: DemoDialect): SQLDialect { + return value === "duckdb" ? DuckDBDialect : PostgreSQL; +} - const extensions = [ +const editor = new EditorView({ + doc: defaultSqlDoc, + extensions: [ basicSetup, EditorView.lineWrapping, - keymap.of(defaultKeymap), - databaseField, - baseSqlCompartment.of(baseSqlExtension(defaultDialect)), - sqlExtension({ - // Shared schema for hover tooltips and schema-aware linting - schema: schema, - // Linter extension configuration - linterConfig: { - delay: 250, // Delay before running validation - parser, - }, - // Schema-aware linting (unknown tables/columns, ambiguous columns) - semanticLinterConfig: { - delay: 250, - parser, - }, - - // Gutter extension configuration - gutterConfig: { - backgroundColor: "#3b82f6", // Blue for current statement - errorBackgroundColor: "#ef4444", // Red for invalid statements - hideWhenNotFocused: true, // Hide gutter when editor loses focus - parser, - }, - // Hover extension configuration - enableHover: true, // Enable hover tooltips - hoverConfig: { - schema: schema, // Use the same schema as autocomplete - hoverTime: 300, // 300ms hover delay - enableKeywords: true, // Show keyword information - enableTables: true, // Show table information - enableColumns: true, // Show column information - keywords: async () => { - const keywords = await getKeywordDocs(); - return keywords; - }, - tooltipRenderers: { - // Custom renderer for tables - table: tableTooltipRenderer, - }, - theme: defaultSqlHoverTheme("light"), - parser, - }, - // Reference highlighting and go-to-definition for CTEs/aliases - enableNavigation: true, - navigationConfig: { - keymap: true, // F12/Mod-b go-to-definition, F2 rename - parser, - }, - }), - // Register all schema-aware completion sources at once: - // - CTE names and their output columns - // - `u.` -> columns of `users` in `SELECT ... FROM users u` - // - `SELECT e` -> `email` because `FROM users` is in the statement - sqlCompletion({ dialect: defaultDialect, schema, parser, contextAnalyzer }), - // Custom theme for better SQL editing + syntax.of(sql({ dialect: syntaxDialect(dialect) })), + support.extension, EditorView.theme({ "&": { - fontSize: "14px", fontFamily: '"JetBrains Mono", monospace', + fontSize: "14px", }, - ".cm-content": { - minHeight: "400px", - }, - ".cm-focused": { - outline: "none", - }, - ".cm-editor": { - borderRadius: "8px", - }, - ".cm-scroller": { - fontFamily: "inherit", - }, - // Style for diagnostic errors - ".cm-diagnostic-error": { - borderBottom: "2px wavy #dc2626", - }, - ".cm-diagnostic": { - padding: "4px 8px", - borderRadius: "4px", - backgroundColor: "#fef2f2", - border: "1px solid #fecaca", - color: "#dc2626", - fontSize: "13px", - }, - // Completion kind backgrounds - ".cm-completionIcon-keyword": { - backgroundColor: "#e0e7ff", // indigo-100 - ...completionKindStyles, - }, - ".cm-completionIcon-variable": { - backgroundColor: "#fef9c3", // yellow-100 - ...completionKindStyles, - }, - ".cm-completionIcon-property": { - backgroundColor: "#bbf7d0", // green-100 - ...completionKindStyles, - }, - ".cm-completionIcon-function": { - backgroundColor: "#bae6fd", // sky-100 - ...completionKindStyles, - }, - ".cm-completionIcon-class": { - backgroundColor: "#fbcfe8", // pink-100 - ...completionKindStyles, - }, - ".cm-completionIcon-constant": { - backgroundColor: "#fde68a", // amber-200 - ...completionKindStyles, - }, - ".cm-completionIcon-type": { - backgroundColor: "#ddd6fe", // violet-200 - ...completionKindStyles, - }, - ".cm-completionIcon-text": { - backgroundColor: "#f3f4f6", // gray-100 - ...completionKindStyles, + ".cm-content": { minHeight: "390px" }, + ".cm-focused": { outline: "none" }, + }), + EditorView.domEventHandlers({ + keydown: (event, view) => { + if (event.key !== "Tab") return false; + return acceptCompletion(view); }, }), - ]; + ], + parent: document.querySelector("#sql-editor") ?? undefined, +}); - editor = new EditorView({ - doc: defaultSqlDoc, - extensions, - parent: document.querySelector("#sql-editor") ?? undefined, +function loadExample(markedSql: string): void { + const cursor = markedSql.indexOf("|"); + const text = markedSql.replace("|", ""); + const position = cursor < 0 ? text.length : cursor; + closeCompletion(editor); + editor.dispatch({ + changes: { from: 0, insert: text, to: editor.state.doc.length }, + selection: { anchor: position }, }); - - return editor; + editor.focus(); + queueMicrotask(() => startCompletion(editor)); + setTimeout(() => { + if (completionStatus(editor.state) === null) { + startCompletion(editor); + } + }, 250); } -// Handle example button clicks -function setupExampleButtons() { - const buttons = document.querySelectorAll(".example-btn"); - - buttons.forEach((button) => { - button.addEventListener("click", () => { - const code = button.querySelector("code"); - if (code && editor) { - const sql = code.textContent || ""; - // Replace editor content with the example - editor.dispatch({ - changes: { - from: 0, - to: editor.state.doc.length, - insert: sql, - }, - }); - // Focus the editor - editor.focus(); - } - }); +for (const button of document.querySelectorAll(".example-btn")) { + button.addEventListener("click", () => { + loadExample(button.dataset.sql ?? ""); }); } -function getDatabase(state: EditorState): SupportedDialects { - return state.field(databaseField); -} - -function setupDatabaseSelect() { - const select = document.querySelector("#database-select"); - if (select) { - select.addEventListener("change", (e) => { - const value = (e.target as HTMLSelectElement).value as SupportedDialects; - updateSqlDialect(editor, guessSqlDialect(value)); - editor.dispatch({ - effects: [setDatabase.of(value)], - }); +document.querySelector("#database-select") + ?.addEventListener("change", (event) => { + dialect = (event.target as HTMLSelectElement).value as DemoDialect; + support.setContext(editor, context()); + editor.dispatch({ + effects: syntax.reconfigure(sql({ dialect: syntaxDialect(dialect) })), }); - } -} + editor.focus(); + }); -function updateSqlDialect(view: EditorView, dialect: SQLDialect) { - view.dispatch({ - effects: [baseSqlCompartment.reconfigure(baseSqlExtension(dialect))], +document.querySelector("#latency-select") + ?.addEventListener("change", (event) => { + latencyMs = Number((event.target as HTMLSelectElement).value); + support.invalidateCatalog(editor); }); -} -// Initialize everything when the page loads -document.addEventListener("DOMContentLoaded", () => { - initializeEditor(); - setupExampleButtons(); - setupDatabaseSelect(); +document.querySelector("#invalidate-catalog") + ?.addEventListener("click", () => { + support.invalidateCatalog(editor); + editor.focus(); + startCompletion(editor); + }); - console.log("SQL Editor Demo initialized!"); - console.log("Features:"); - console.log("- Real-time SQL syntax validation"); - console.log("- Error highlighting with detailed messages"); - console.log("- Support for multiple SQL dialects"); - console.log("- TypeScript support"); +window.addEventListener("beforeunload", () => { + editor.destroy(); + service.dispose(); }); + +updateStats(); diff --git a/src/sql/__tests__/demo-doc.test.ts b/src/sql/__tests__/demo-doc.test.ts index d27b5ef..d72a17b 100644 --- a/src/sql/__tests__/demo-doc.test.ts +++ b/src/sql/__tests__/demo-doc.test.ts @@ -1,15 +1,25 @@ import { EditorState } from "@codemirror/state"; import { expect, it } from "vitest"; -import { defaultSqlDoc } from "../../../demo/data.js"; import { NodeSqlParser } from "../parser.js"; import { findReferences } from "../references.js"; const parser = new NodeSqlParser({ getParserOptions: () => ({ database: "PostgreSQL" }) }); +const navigationSql = `WITH recent_orders AS ( + SELECT customer_id, total_amount FROM orders +), +top_customers AS ( + SELECT customer_id, SUM(total_amount) AS total_spent + FROM recent_orders + GROUP BY customer_id +) +SELECT c.first_name, t.total_spent AS amount +FROM customers c +JOIN top_customers t ON t.customer_id = c.id +ORDER BY amount DESC;`; -it("demo default doc: first statement parses and navigation resolves", async () => { - const state = EditorState.create({ doc: defaultSqlDoc }); - const firstStmt = defaultSqlDoc.slice(0, defaultSqlDoc.indexOf(";") + 1); - const result = await parser.parse(firstStmt, { state }); +it("resolves navigation in a compound CTE query", async () => { + const state = EditorState.create({ doc: navigationSql }); + const result = await parser.parse(navigationSql, { state }); expect(result.errors).toEqual([]); const expectations: Array<[string, string, number]> = [ @@ -19,7 +29,7 @@ it("demo default doc: first statement parses and navigation resolves", async () ["t.customer_id", "table-alias", 3], ]; for (const [marker, kind, count] of expectations) { - const refs = await findReferences(state, defaultSqlDoc.indexOf(marker), { parser }); + const refs = await findReferences(state, navigationSql.indexOf(marker), { parser }); expect(refs?.kind, marker).toBe(kind); expect(refs?.references, marker).toHaveLength(count); }