Skip to content

Commit e0efb40

Browse files
committed
docs(vnext): document and benchmark session completion
1 parent 002125d commit e0efb40

3 files changed

Lines changed: 224 additions & 4 deletions

File tree

docs/vnext/session-primitives.md

Lines changed: 71 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,10 @@
33
Status: experimental walking skeleton
44
Import: `@marimo-team/codemirror-sql/vnext`
55

6-
This entry point currently provides document ownership, atomic text/context
7-
updates, opaque revisions, dialect registration, and lifecycle management. It
8-
does not yet provide parsing, completion, diagnostics, hover, or navigation.
9-
Those methods will be added only with working vertical slices.
6+
This entry point provides document ownership, atomic text/context updates,
7+
opaque revisions, dialect registration, lifecycle management, and
8+
parser-independent relation completion. Diagnostics, hover, navigation, and
9+
general expression completion are not yet available.
1010

1111
See [source coordinates](./source-coordinates.md) for the shared UTF-16 range
1212
contract and the internal immutable source-snapshot model.
@@ -56,6 +56,73 @@ Use `{ kind: "replace", text }` for full replacement and
5656
also supplies the complete embedded-region set for its resulting text. A
5757
region-only transaction can replace or clear that set without a fake text edit.
5858

59+
## Relation completion
60+
61+
Completion works without a catalog for visible CTEs. A service can also own one
62+
shared asynchronous relation-catalog provider:
63+
64+
```ts
65+
const service = createSqlLanguageService<AppSqlContext>({
66+
catalog: {
67+
id: "app-catalog",
68+
search: async (request, signal) => {
69+
signal.throwIfAborted();
70+
return {
71+
coverage: { kind: "complete" },
72+
epoch: { generation: 0, token: "initial" },
73+
relations: [],
74+
status: "ready",
75+
};
76+
},
77+
},
78+
completion: { catalogResponseBudgetMs: 40 },
79+
dialects: [duckdbDialect()],
80+
});
81+
82+
const session = service.openDocument({
83+
context: {
84+
dialect: "duckdb",
85+
engine: "local",
86+
catalog: { scope: "connection-incarnation:1" },
87+
},
88+
text: "SELECT * FROM ",
89+
});
90+
91+
const subscription = session.onDidChange(({ reason }) => {
92+
if (reason === "catalog" || reason === "catalog-availability") {
93+
// Ask the editor adapter to request completion again.
94+
}
95+
});
96+
97+
const result = await session.complete({
98+
position: 14,
99+
trigger: { kind: "invoked" },
100+
});
101+
102+
if (result.status === "ready" && session.isCurrent(result.revision)) {
103+
for (const item of result.value.items) {
104+
// Apply item.edit in the original document's UTF-16 coordinates.
105+
}
106+
}
107+
108+
subscription.dispose();
109+
session.dispose();
110+
service.dispose();
111+
```
112+
113+
The catalog `scope` identifies a live connection incarnation, not a reusable
114+
display name. Search responses distinguish complete, partial, paginated,
115+
loading, and failed evidence. A ready result can therefore be incomplete; its
116+
closed `issues` explain why, while `sources` reports catalog outcomes without
117+
exposing provider errors or internal epochs.
118+
119+
The interactive catalog wait is bounded from the start of `complete()`. When
120+
the budget expires, the session returns local evidence with a
121+
`catalog-loading` issue and a checked remaining intent lease. Compatible
122+
readiness advances the session revision and emits `catalog-availability`;
123+
higher provider epochs emit `catalog`. Consumers must request completion again
124+
and apply only results whose revision remains current.
125+
59126
## Dialect registration
60127

61128
`duckdbDialect()`, `postgresDialect()`, `bigQueryDialect()`, and

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@
3131
"bench:local-relation-site": "vitest bench --run src/vnext/__tests__/local-relation-site.bench.ts",
3232
"bench:parser-adapter": "vitest bench --run src/vnext/__tests__/node-sql-parser-adapter.bench.ts",
3333
"bench:query-site": "vitest bench --run src/vnext/__tests__/query-site.bench.ts",
34+
"bench:session-completion": "vitest bench --run src/vnext/__tests__/session-completion.bench.ts",
3435
"bench:statement-index": "vitest bench --run src/vnext/__tests__/statement-index.bench.ts",
3536
"test:package": "node ./scripts/clean.mjs && tsc && node ./scripts/package-smoke.mjs",
3637
"demo": "vite build",
Lines changed: 152 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,152 @@
1+
import { bench, describe } from "vitest";
2+
import {
3+
createSqlLanguageService,
4+
duckdbDialect,
5+
} from "../index.js";
6+
import type {
7+
SqlCatalogRelation,
8+
} from "../relation-completion-types.js";
9+
import type {
10+
SqlDocumentContext,
11+
SqlDocumentSession,
12+
SqlLanguageService,
13+
} from "../types.js";
14+
15+
interface BenchmarkContext extends SqlDocumentContext {
16+
readonly engine: "benchmark";
17+
}
18+
19+
const dialect = duckdbDialect();
20+
const tenKibibytes = 10 * 1_024;
21+
const prefix = "WITH local_cte AS (SELECT 1) SELECT ";
22+
const suffix = " FROM local";
23+
const projectionLength =
24+
tenKibibytes - prefix.length - suffix.length;
25+
const projection = `${"x,".repeat(
26+
Math.floor((projectionLength - 1) / 2),
27+
)}x`;
28+
const tenKibibyteText = `${prefix}${projection}${" ".repeat(
29+
projectionLength - projection.length,
30+
)}${suffix}`;
31+
if (tenKibibyteText.length !== tenKibibytes) {
32+
throw new Error("Session completion benchmark must be exactly 10 KiB");
33+
}
34+
35+
function openLocalSession(
36+
service: SqlLanguageService<BenchmarkContext>,
37+
): SqlDocumentSession<BenchmarkContext> {
38+
return service.openDocument({
39+
context: { dialect: "duckdb", engine: "benchmark" },
40+
text: tenKibibyteText,
41+
});
42+
}
43+
44+
const localService =
45+
createSqlLanguageService<BenchmarkContext>({
46+
dialects: [dialect],
47+
});
48+
const warmLocalSession = openLocalSession(localService);
49+
const warmPreflight = await warmLocalSession.complete({
50+
position: tenKibibyteText.length,
51+
trigger: { kind: "invoked" },
52+
});
53+
if (
54+
warmPreflight.status !== "ready" ||
55+
warmPreflight.value.items.length !== 1
56+
) {
57+
throw new Error("Warm session benchmark preflight failed");
58+
}
59+
60+
const catalogRelations = Array.from(
61+
{ length: 100 },
62+
(_, index): SqlCatalogRelation => ({
63+
canonicalPath: [
64+
{
65+
quoted: false,
66+
role: "relation" as const,
67+
value: `relation_${index}`,
68+
},
69+
],
70+
completionPathStart: 0,
71+
entityId: `relation-${index}`,
72+
matchQuality: "exact" as const,
73+
relationKind: "table" as const,
74+
}),
75+
);
76+
77+
function createCatalogService(): SqlLanguageService<BenchmarkContext> {
78+
return createSqlLanguageService<BenchmarkContext>({
79+
catalog: {
80+
id: "benchmark-catalog",
81+
search: async () => ({
82+
coverage: { kind: "complete" },
83+
epoch: { generation: 0, token: "initial" },
84+
relations: catalogRelations,
85+
status: "ready",
86+
}),
87+
},
88+
dialects: [dialect],
89+
});
90+
}
91+
92+
const cachedCatalogService = createCatalogService();
93+
const cachedCatalogSession = cachedCatalogService.openDocument({
94+
context: {
95+
catalog: { scope: "benchmark:cached" },
96+
dialect: "duckdb",
97+
engine: "benchmark",
98+
},
99+
text: "SELECT * FROM relation_",
100+
});
101+
const cachedPreflight = await cachedCatalogSession.complete({
102+
position: 23,
103+
trigger: { kind: "invoked" },
104+
});
105+
if (
106+
cachedPreflight.status !== "ready" ||
107+
cachedPreflight.value.items.length !== 100
108+
) {
109+
throw new Error("Cached catalog benchmark preflight failed");
110+
}
111+
112+
describe("session completion", () => {
113+
bench("cold 10 KiB local completion", async () => {
114+
const session = openLocalSession(localService);
115+
await session.complete({
116+
position: tenKibibyteText.length,
117+
trigger: { kind: "invoked" },
118+
});
119+
session.dispose();
120+
});
121+
122+
bench("warm 10 KiB local completion", async () => {
123+
await warmLocalSession.complete({
124+
position: tenKibibyteText.length,
125+
trigger: { kind: "invoked" },
126+
});
127+
});
128+
129+
bench("cached 100-candidate catalog completion", async () => {
130+
await cachedCatalogSession.complete({
131+
position: 23,
132+
trigger: { kind: "invoked" },
133+
});
134+
});
135+
136+
bench("cold 100-candidate catalog completion", async () => {
137+
const service = createCatalogService();
138+
const session = service.openDocument({
139+
context: {
140+
catalog: { scope: "benchmark:cold" },
141+
dialect: "duckdb",
142+
engine: "benchmark",
143+
},
144+
text: "SELECT * FROM relation_",
145+
});
146+
await session.complete({
147+
position: 23,
148+
trigger: { kind: "invoked" },
149+
});
150+
service.dispose();
151+
});
152+
});

0 commit comments

Comments
 (0)