Skip to content

Commit 2d5b402

Browse files
committed
fix(vnext): expose bounded catalog invalidation
1 parent 2b66021 commit 2d5b402

4 files changed

Lines changed: 50 additions & 1 deletion

File tree

docs/vnext/codemirror-adapter.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,17 @@ const visible = support.statementBoundariesIntersecting(view, {
8787
```
8888

8989
Both methods return `null` for foreign, destroyed, or unsynchronized views.
90+
Catalog-backed hosts can invalidate the adapter-owned session without exposing
91+
it:
92+
93+
```ts
94+
const revision = support.invalidateCatalog(view);
95+
```
96+
97+
This returns `null` for foreign or destroyed views. A live view advances its
98+
revision, cancels stale catalog work, and forces relation, column, and namespace
99+
providers to be consulted again.
100+
90101
An opt-in structural gutter marks only lines intersecting scanner-owned SQL
91102
`code` spans. It never parses or copies statement text:
92103

src/vnext/codemirror/__tests__/sql-editor.test.ts

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ interface FakeServiceHarness {
4848
readonly completeSignals: AbortSignal[];
4949
readonly emit: (event: SqlSessionChangeEvent) => void;
5050
readonly getLastToken: () => SqlCompletionRefreshToken | null;
51+
readonly invalidations: () => number;
5152
readonly service: SqlLanguageService<TestContext>;
5253
readonly sessionDisposals: () => number;
5354
readonly statementBoundaryCalls: () => number;
@@ -138,6 +139,7 @@ function fakeService(
138139
| ((event: SqlSessionChangeEvent) => void)
139140
| null = null;
140141
let lastToken: SqlCompletionRefreshToken | null = null;
142+
let invalidationCount = 0;
141143
let sessionDisposalCount = 0;
142144
let statementBoundaryCallCount = 0;
143145
let statementIntersectionCallCount = 0;
@@ -162,6 +164,7 @@ function fakeService(
162164
sessionDisposalCount += 1;
163165
},
164166
invalidateCatalog: () => {
167+
invalidationCount += 1;
165168
revision = createSqlRevisionToken();
166169
return revision;
167170
},
@@ -243,6 +246,7 @@ function fakeService(
243246
completeSignals,
244247
emit: (event) => listener?.(event),
245248
getLastToken: () => lastToken,
249+
invalidations: () => invalidationCount,
246250
service,
247251
sessionDisposals: () => sessionDisposalCount,
248252
statementBoundaryCalls: () => statementBoundaryCallCount,
@@ -360,7 +364,7 @@ async function resolveCompletionInfo(
360364
}
361365

362366
describe("sqlEditor", () => {
363-
it("exposes current statement boundaries only for owned views", () => {
367+
it("exposes session controls only for owned views", () => {
364368
const service = createSqlLanguageService<TestContext>({
365369
dialects: [duckdbDialect()],
366370
});
@@ -383,6 +387,8 @@ describe("sqlEditor", () => {
383387
from: 0,
384388
to: view.state.doc.length,
385389
})?.boundaries).toHaveLength(2);
390+
expect(support.invalidateCatalog(view)).not.toBeNull();
391+
expect(support.invalidateCatalog(foreign)).toBeNull();
386392
expect(support.statementBoundaryAt(foreign, {
387393
affinity: "left",
388394
position: 0,
@@ -397,6 +403,7 @@ describe("sqlEditor", () => {
397403
})).toBeNull();
398404

399405
view.destroy();
406+
expect(support.invalidateCatalog(view)).toBeNull();
400407
expect(support.statementBoundaryAt(view, {
401408
affinity: "left",
402409
position: 0,
@@ -408,6 +415,24 @@ describe("sqlEditor", () => {
408415
service.dispose();
409416
});
410417

418+
it("proxies catalog invalidation only to its owned live session", () => {
419+
const harness = fakeService((revision) => readyResult(revision, []));
420+
const support = sqlEditor({
421+
initialContext: context(),
422+
service: harness.service,
423+
});
424+
const view = createView(support.extension);
425+
const foreign = createView([]);
426+
427+
expect(support.invalidateCatalog(view)).not.toBeNull();
428+
expect(harness.invalidations()).toBe(1);
429+
expect(support.invalidateCatalog(foreign)).toBeNull();
430+
expect(harness.invalidations()).toBe(1);
431+
view.destroy();
432+
expect(support.invalidateCatalog(view)).toBeNull();
433+
expect(harness.invalidations()).toBe(1);
434+
});
435+
411436
it("renders an opt-in visible-line statement gutter", async () => {
412437
const service = createSqlLanguageService<TestContext>({
413438
dialects: [duckdbDialect()],

src/vnext/codemirror/sql-editor.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,7 @@ export interface SqlEditorSupport<
9595
readonly SqlEmbeddedRegion[]
9696
>;
9797
readonly extension: Extension;
98+
readonly invalidateCatalog: (view: EditorView) => SqlRevision | null;
9899
readonly statementBoundariesIntersecting: (
99100
view: EditorView,
100101
request: SqlStatementBoundariesIntersectingRequest,
@@ -671,6 +672,15 @@ export function createSqlEditorInternal<
671672
this.#clearCompletionState();
672673
};
673674

675+
readonly invalidateCatalog = (): SqlRevision | null => {
676+
if (this.#destroyed) return null;
677+
try {
678+
return this.#session.invalidateCatalog();
679+
} catch {
680+
return null;
681+
}
682+
};
683+
674684
readonly statementBoundariesIntersecting = (
675685
request: SqlStatementBoundariesIntersectingRequest,
676686
): SqlStatementBoundariesIntersectingResult | null => {
@@ -869,6 +879,8 @@ export function createSqlEditorInternal<
869879
override: [completionSource, ...externalSources],
870880
}),
871881
],
882+
invalidateCatalog: (view: EditorView): SqlRevision | null =>
883+
view.plugin(plugin)?.invalidateCatalog() ?? null,
872884
statementBoundariesIntersecting: (
873885
view: EditorView,
874886
request: SqlStatementBoundariesIntersectingRequest,

src/vnext/session.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2695,6 +2695,7 @@ export class DefaultSqlDocumentSession<Context extends SqlDocumentContext>
26952695
}
26962696
this.#activeCompletion = null;
26972697
this.#columnLoadingRetry = null;
2698+
this.#namespaceLoadingRetry = null;
26982699
this.#refreshIntent = null;
26992700
this.#clearSoftRefreshIntentTimer();
27002701
this.#clearTerminalIntent();

0 commit comments

Comments
 (0)