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
19 changes: 16 additions & 3 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,9 @@ jobs:
- name: ⚡ Performance Gates
run: pnpm run test:performance

- name: 🧬 Mutation Pilot
run: pnpm run test:mutation

- name: 📈 Changed-Code Coverage
if: github.event_name == 'pull_request' || github.event_name == 'push'
env:
Expand All @@ -77,6 +80,13 @@ jobs:

browser:
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
browser:
- chromium
- firefox
- webkit

permissions:
contents: read
Expand All @@ -96,13 +106,16 @@ jobs:
- name: 📥 Install dependencies
run: pnpm install --ignore-scripts --frozen-lockfile

- name: 🎭 Install Chromium
run: pnpm exec playwright install --with-deps chromium
- name: 🎭 Install ${{ matrix.browser }}
run: pnpm exec playwright install --with-deps "${{ matrix.browser }}"

- name: 🧪 Browser Tests
- name: 🧪 Browser Tests (${{ matrix.browser }})
env:
VITEST_BROWSER: ${{ matrix.browser }}
run: pnpm run test:browser

- name: 🧵 Worker Placement Evidence
if: matrix.browser == 'chromium'
run: pnpm run test:worker-placement

package:
Expand Down
5 changes: 5 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,9 @@ Published as [`@marimo-team/codemirror-sql`](https://www.npmjs.com/package/@mari
- **Schema-aware completion** — complete relations, namespaces, physical columns, inferred CTE/derived outputs, aliases, correlated scopes, set-operation arms, and DML targets
- **CodeMirror integration** — cancellation-safe completion, disposable detail panels, atomic context updates, and an optional virtualized statement gutter
- **Composable completion** — package results coexist with SQL keywords, functions, embedded-language sources, and host sources
- **Language intelligence** — bounded diagnostics, hover, navigation, rename,
symbols, folding, formatting, and code-action providers share one revision
and cancellation model
- **Isolated parsing** — optional browser-worker parser execution kept off the public API surface

## Installation
Expand Down Expand Up @@ -64,6 +67,8 @@ service.dispose();
Configure relation, column, and namespace providers on the shared service for
schema-aware completion. See the [CodeMirror adapter](./docs/codemirror-adapter.md)
and [session primitives](./docs/session-primitives.md) for the full contracts.
See [language feature providers](./docs/language-features.md) for native-engine,
LSP, formatter, and host-validation integration.

## Demo

Expand Down
46 changes: 46 additions & 0 deletions docs/adr/0007-language-feature-provider-composition.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
# ADR 0007: Bounded language-feature provider composition

Status: accepted
Date: 2026-07-27

## Context

Diagnostics, hover, navigation, rename, formatting, symbols, folding, and code
actions need the same revision, coordinate, cancellation, failure-isolation,
and result-validation rules. Leaving those rules to each host creates stale
results, leaked provider errors, unsafe edits, and incompatible native-engine
and language-server integrations.

## Decision

The framework-independent session owns one generic provider composition
runtime. Providers run concurrently under one absolute request deadline and receive
per-provider abort signals. The runtime validates, bounds, freezes, and
normalizes every public result. Collection features compose in configuration
order; scalar features use the first non-empty successful result. Provider ASTs,
errors, transports, and mutable editor objects never cross the boundary.

Statement symbols and folding have a parser-free local baseline. DOM rendering,
debouncing, lint panels, and tooltip presentation remain host policy.

This adds 3,695 gzip bytes (about 3.6 KiB) to the complete
framework-independent core. The measured
artifact is 57,307 gzip bytes and 215,639 raw bytes, so the enforced core
ceilings move from 54 KiB/200 KiB to 57 KiB/212 KiB. Optional parser chunks
remain unchanged and independently chunkable.
The worker-placement page includes the core, so its raw aggregate ceiling moves
from 720 KiB to 725 KiB; its parser chunks do not move, while the aggregate
gzip ceiling moves from 164 KiB to 165 KiB.
The increase is accepted because one shared validator is smaller and safer than
duplicating feature-specific boundaries in every consumer.

## Consequences

- Native engines, LSP clients, host validators, and formatters use one stable
plain-data contract.
- A slow or failed provider cannot suppress unrelated provider evidence.
- Cancellation and disposal settle without waiting for provider cooperation.
- Synchronous provider time counts against the deadline but cannot be
preempted; CPU-heavy integrations must run off-thread.
- Core size remains measured in CI with 1,061 gzip bytes and 1,449 raw bytes
of headroom at acceptance.
4 changes: 2 additions & 2 deletions docs/capability-charter.md
Original file line number Diff line number Diff line change
Expand Up @@ -238,8 +238,8 @@ count and estimated retained bytes.

Provisional compressed bundle budgets:

- Framework-independent core with relation, namespace, column, and local query
output completion: 54 KiB
- Framework-independent core with completion and the validated language-feature
provider runtime: 57 KiB gzip / 212 KiB raw
- Core plus CodeMirror adapter, excluding parser and dialect data: 75 KiB
- Each ordinary dialect module: 25 KiB
- Optional `node-sql-parser` chunk: no regression from its recorded baseline
Expand Down
85 changes: 85 additions & 0 deletions docs/language-features.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
# Language feature providers

The language service exposes diagnostics, hover, definition, references,
highlights, rename, document symbols, folding, formatting, and code actions
through the same document session used for completion.

```ts
const service = createSqlLanguageService({
dialects: [duckdbDialect()],
featureProviders: [{
id: "duckdb",
diagnostics: async ({ document, signal }) => {
const response = await validateSql(document.text, { signal });
return response.diagnostics;
},
hover: ({ document, request }) =>
describeSqlAt(document.text, request.position),
format: ({ document, request }) => ({
changes: [{
from: request.range?.from ?? 0,
to: request.range?.to ?? document.text.length,
insert: formatSql(document.text, request),
}],
}),
}],
});

const session = service.openDocument({
context: { dialect: "duckdb" },
text: "select * form users",
});

const task = session.diagnostics();
const result = await task.result;
if (result.status === "ready" && session.isCurrent(result.revision)) {
renderDiagnostics(result.value);
}
```

Every provider receives a frozen document snapshot, a normalized request, and
an `AbortSignal`. Public ranges are absolute, half-open UTF-16 ranges in the
original document. Results are copied, bounded, validated, and frozen before
publication. Provider exceptions, rejections, timeouts, malformed ranges, and
malformed edits do not escape the service boundary.

Providers run concurrently. `featureProviderBudgetMs` is one absolute
per-request deadline, not a budget multiplied by the provider count. Elapsed
synchronous invocation time counts against that deadline, but JavaScript
cannot preempt a provider that blocks the current thread. Expensive or
untrusted integrations must yield immediately and continue asynchronously in a
worker, process, or remote service. Cancellation, session updates, catalog
invalidation, and disposal stop publication promptly once control returns,
even when an underlying provider ignores its signal.

Collection features compose successful providers in configuration order.
Scalar features such as hover, rename, and format select the first successful
non-empty result. `isIncomplete` explicitly records bounded collection
truncation. Source reports remain available on ready and unavailable results,
preserving which providers were ready, failed, or timed out. Formatting is
explicit; typing never invokes a formatter.

The built-in local structure provider supplies statement-level document
symbols and multiline folding without loading an optional parser. Parser,
native-engine, language-server, host-policy, and formatter integrations remain
ordinary providers and do not expose their AST or transport types.

`sqlEditor` exposes the same tasks through its returned support object:

```ts
const support = sqlEditor({ initialContext, service });
const diagnostics = support.diagnostics(view);
const hover = support.hover(view, { position: view.state.selection.main.head });
```

The host owns presentation and scheduling. This keeps the core SSR-safe and
allows applications to use CodeMirror lint, tooltip, panel, or custom UI
extensions without the SQL package imposing DOM rendering or debounce policy.

Function, scalar parameter, and snippet completion use
`autocomplete.externalSources`, which accepts ordinary CodeMirror
`CompletionSource` values and therefore preserves snippet application and
parameter syntax without translating them through a lossy catalog shape.
Table-valued functions can also be returned by relation catalogs with
`relationKind: "table-function"`; the CodeMirror adapter presents them as
functions at relation sites.
5 changes: 4 additions & 1 deletion implementation.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# SQL editor completion plan

Status: active
Status: complete
Updated: 2026-07-27

The standard language-service overhaul is delivered by PR #204. Two additional
Expand Down Expand Up @@ -32,6 +32,9 @@ columns.

## PR 2: language intelligence and release hardening

Status: complete
Delivery: PR #206

The final PR adds the remaining feature methods and provider composition:

- syntax, semantic, and host diagnostics;
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
"test:worker-placement": "node ./scripts/worker-placement.mjs",
"test:integrity": "node ./scripts/check-test-integrity.mjs",
"test:performance": "vitest run --config vitest.performance.config.ts",
"test:mutation": "node ./scripts/mutation-pilot.mjs",
"bench:catalog-boundary": "vitest bench --run src/__tests__/relation-catalog-boundary.bench.ts",
"bench:catalog-coordinator": "vitest bench --run src/__tests__/relation-catalog-epoch-coordinator.bench.ts",
"bench:editor": "vitest bench --run src/codemirror/__tests__/sql-editor.bench.ts",
Expand Down
43 changes: 43 additions & 0 deletions scripts/mutation-pilot.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import { spawnSync } from "node:child_process";
import { readFileSync, writeFileSync } from "node:fs";
import { dirname, resolve } from "node:path";
import { fileURLToPath } from "node:url";

const repository = resolve(dirname(fileURLToPath(import.meta.url)), "..");
const target = resolve(repository, "src", "language-feature-runtime.ts");
const original = readFileSync(target, "utf8");
const before = "value.length > MAX_SQL_FEATURE_RESULTS";
const after = "value.length >= MAX_SQL_FEATURE_RESULTS";
if (original.split(before).length !== 2) {
throw new Error("Mutation pilot target is missing or ambiguous");
}

let outcome;
try {
writeFileSync(target, original.replace(before, after));
outcome = spawnSync(
process.execPath,
[
resolve(repository, "node_modules", "vitest", "vitest.mjs"),
"run",
"--config",
"vitest.config.ts",
"src/__tests__/language-feature-runtime.test.ts",
],
{
cwd: repository,
encoding: "utf8",
stdio: "pipe",
},
);
} finally {
writeFileSync(target, original);
}

if (outcome.status === 0) {
throw new Error(
"Mutation survived: the feature result limit boundary is not tested",
);
}
if (outcome.error) throw outcome.error;
process.stdout.write("Mutation killed: feature result limit boundary\n");
45 changes: 45 additions & 0 deletions scripts/package-smoke.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -239,6 +239,18 @@ const cleanup: SqlCatalogSubscriptionCleanup = () => undefined;

const service = createSqlLanguageService<HostContext>({
dialects: [duckdbDialect()],
featureProviders: [{
id: "marimo-duckdb",
diagnostics: ({ document }) => document.context.engine === "remote"
? [{
from: 0,
message: "Remote validation",
severity: "information",
source: "duckdb",
to: 6,
}]
: [],
}],
});
const editorSupport = sqlEditor({
initialContext: { dialect: "duckdb", engine: "local" },
Expand All @@ -262,12 +274,14 @@ const statement: SqlStatementBoundaryAtResult = session.statementBoundaryAt({
affinity: "left",
position: 0,
});
const diagnostics = session.diagnostics();

void editorSupport.extension;
void cleanup;
void range;
void session;
void statement;
void diagnostics;
`,
);

Expand Down Expand Up @@ -337,6 +351,37 @@ if (
throw new Error("The packaged statement boundary is invalid");
}
service.dispose();

const marimoService = api.createSqlLanguageService({
dialects: [api.duckdbDialect()],
featureProviders: [{
id: "marimo-duckdb",
diagnostics: ({ document }) => [{
from: 0,
message: \`Validated \${document.context.engine}\`,
severity: "information",
source: "duckdb",
to: 6,
}],
}],
});
const marimoSession = marimoService.openDocument({
context: { dialect: "duckdb", engine: "remote" },
embeddedRegions: [{ from: 14, language: "python", to: 18 }],
text: "SELECT * FROM {df}",
});
const diagnostics = await marimoSession.diagnostics().result;
const symbols = await marimoSession.documentSymbols().result;
if (
diagnostics.status !== "ready" ||
diagnostics.value[0]?.message !== "Validated remote" ||
symbols.status !== "ready" ||
symbols.value[0]?.name !== "SELECT statement"
) {
throw new Error("The packed marimo language-feature fixture failed");
}
marimoSession.dispose();
marimoService.dispose();
`,
);

Expand Down
8 changes: 4 additions & 4 deletions scripts/worker-placement.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -35,11 +35,11 @@ const PARSER_MARKERS = [
"tableList",
];
const BIGQUERY_GZIP_LIMIT = 50 * 1024;
const CORE_TOTAL_GZIP_LIMIT = 54 * 1024;
const CORE_TOTAL_RAW_LIMIT = 200 * 1024;
const CORE_TOTAL_GZIP_LIMIT = 57 * 1024;
const CORE_TOTAL_RAW_LIMIT = 212 * 1024;
const POSTGRESQL_GZIP_LIMIT = 68 * 1024;
const WORKER_TOTAL_GZIP_LIMIT = 164 * 1024;
const WORKER_TOTAL_RAW_LIMIT = 720 * 1024;
const WORKER_TOTAL_GZIP_LIMIT = 165 * 1024;
const WORKER_TOTAL_RAW_LIMIT = 725 * 1024;
const MIME_TYPES = new Map([
[".css", "text/css; charset=utf-8"],
[".html", "text/html; charset=utf-8"],
Expand Down
Loading
Loading