Skip to content

Commit b1eb97a

Browse files
committed
feat(vnext): compose relation completion in sessions
1 parent f0f4d5e commit b1eb97a

8 files changed

Lines changed: 2117 additions & 17 deletions

File tree

Lines changed: 387 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,387 @@
1+
import { describe, expect, it } from "vitest";
2+
import type {
3+
SqlValidatedCatalogRelation,
4+
SqlValidatedCatalogSearchResponse,
5+
} from "../relation-catalog-boundary.js";
6+
import {
7+
composeSqlRelationCompletion,
8+
MAX_RELATION_COMPLETION_RESULTS,
9+
type SqlComposableCatalogOutcome,
10+
} from "../relation-completion.js";
11+
import {
12+
analyzeSqlLocalRelationSite,
13+
prepareSqlLocalRelationStatement,
14+
type SqlLocalRelationSiteResult,
15+
} from "../local-relation-site.js";
16+
import {
17+
POSTGRESQL_SQL_RELATION_DIALECT,
18+
} from "../relation-dialect.js";
19+
import {
20+
createIdentitySqlSource,
21+
} from "../source.js";
22+
import {
23+
buildSqlStatementIndex,
24+
findSqlStatementSlot,
25+
} from "../statement-index.js";
26+
import type {
27+
SqlCanonicalRelationPath,
28+
SqlCatalogRelationKind,
29+
SqlCatalogReadyCoverage,
30+
SqlRelationCompletionList,
31+
} from "../relation-completion-types.js";
32+
import type {
33+
SqlTextRange,
34+
} from "../types.js";
35+
36+
interface MarkedLocalSite {
37+
readonly localSite: Extract<
38+
SqlLocalRelationSiteResult,
39+
{ readonly status: "ready" }
40+
>;
41+
readonly replacementRange: SqlTextRange;
42+
readonly statementOffset: number;
43+
}
44+
45+
function markedLocalSite(marked: string): MarkedLocalSite {
46+
const position = marked.indexOf("|");
47+
if (
48+
position < 0 ||
49+
marked.indexOf("|", position + 1) >= 0
50+
) {
51+
throw new Error("Expected exactly one cursor marker");
52+
}
53+
const text =
54+
marked.slice(0, position) + marked.slice(position + 1);
55+
const source = createIdentitySqlSource(text);
56+
const index = buildSqlStatementIndex(
57+
source.analysisText,
58+
POSTGRESQL_SQL_RELATION_DIALECT.querySite.lexicalProfile,
59+
);
60+
const slot = findSqlStatementSlot(index, position, "left");
61+
const prepared = prepareSqlLocalRelationStatement(
62+
source,
63+
index,
64+
slot,
65+
POSTGRESQL_SQL_RELATION_DIALECT,
66+
);
67+
if (prepared.status !== "ready") {
68+
throw new Error(`Local statement unavailable: ${prepared.reason}`);
69+
}
70+
const localSite = analyzeSqlLocalRelationSite(
71+
prepared.statement,
72+
position,
73+
);
74+
if (localSite.status !== "ready") {
75+
throw new Error(`Local site unavailable: ${localSite.status}`);
76+
}
77+
if (slot.boundaryQuality !== "exact") {
78+
throw new Error("Ready local relation site requires an exact slot");
79+
}
80+
return {
81+
localSite,
82+
replacementRange: Object.freeze({
83+
from:
84+
slot.source.from + localSite.querySite.typedPathRange.from,
85+
to:
86+
slot.source.from + localSite.querySite.typedPathRange.to,
87+
}),
88+
statementOffset: slot.source.from,
89+
};
90+
}
91+
92+
function relationPath(
93+
containers: readonly string[],
94+
name: string,
95+
): SqlCanonicalRelationPath {
96+
return Object.freeze([
97+
...containers.map((value) =>
98+
Object.freeze({
99+
quoted: false,
100+
role: "schema" as const,
101+
value,
102+
}),
103+
),
104+
Object.freeze({
105+
quoted: false,
106+
role: "relation" as const,
107+
value: name,
108+
}),
109+
]);
110+
}
111+
112+
function catalogRelation(input: {
113+
readonly completionPathStart?: number;
114+
readonly containers?: readonly string[];
115+
readonly entityId: string;
116+
readonly kind?: SqlCatalogRelationKind;
117+
readonly match?: "equivalent" | "exact";
118+
readonly name: string;
119+
}): SqlValidatedCatalogRelation {
120+
const path = relationPath(
121+
input.containers ?? [],
122+
input.name,
123+
);
124+
const completionPathStart =
125+
input.completionPathStart ?? 0;
126+
if (
127+
completionPathStart !== 0 &&
128+
completionPathStart !== path.length - 1
129+
) {
130+
throw new Error("Test helper supports full or relation-only completion");
131+
}
132+
const completionPath =
133+
completionPathStart === 0
134+
? path
135+
: relationPath([], input.name);
136+
return Object.freeze({
137+
canonicalPath: path,
138+
completionPath,
139+
completionPathStart,
140+
completionText: completionPath
141+
.map((component) => component.value)
142+
.join("."),
143+
entityId: input.entityId,
144+
matchQuality: input.match ?? "exact",
145+
relationKind: input.kind ?? "table",
146+
});
147+
}
148+
149+
function readyResponse(
150+
relations: readonly SqlValidatedCatalogRelation[],
151+
coverage: SqlCatalogReadyCoverage = Object.freeze({
152+
kind: "complete",
153+
}),
154+
): Extract<
155+
SqlValidatedCatalogSearchResponse,
156+
{ readonly status: "ready" }
157+
> {
158+
return Object.freeze({
159+
coverage,
160+
epoch: Object.freeze({ generation: 1, token: "one" }),
161+
relations: Object.freeze([...relations]),
162+
status: "ready",
163+
});
164+
}
165+
166+
function usable(
167+
response: SqlValidatedCatalogSearchResponse,
168+
): SqlComposableCatalogOutcome {
169+
return Object.freeze({
170+
observation: "equal",
171+
response,
172+
status: "usable",
173+
});
174+
}
175+
176+
function compose(
177+
marked: string,
178+
catalogOutcome: SqlComposableCatalogOutcome,
179+
): SqlRelationCompletionList {
180+
const local = markedLocalSite(marked);
181+
return composeSqlRelationCompletion({
182+
catalogOutcome,
183+
dialect: POSTGRESQL_SQL_RELATION_DIALECT,
184+
localSite: local.localSite,
185+
providerId:
186+
catalogOutcome === null ? null : "catalog",
187+
remainingIntentLeaseMs: 25,
188+
replacementRange: local.replacementRange,
189+
statementOffset: local.statementOffset,
190+
}).value;
191+
}
192+
193+
describe("relation completion composition", () => {
194+
it("ranks CTEs before deterministic catalog tiers and shadows only unqualified insertions", () => {
195+
const relations = [
196+
catalogRelation({
197+
entityId: "equivalent",
198+
match: "equivalent",
199+
name: "aardvark",
200+
}),
201+
catalogRelation({
202+
completionPathStart: 0,
203+
containers: ["public"],
204+
entityId: "qualified-users",
205+
name: "users",
206+
}),
207+
catalogRelation({
208+
entityId: "view",
209+
kind: "view",
210+
name: "beta",
211+
}),
212+
catalogRelation({
213+
entityId: "shadowed-users",
214+
name: "users",
215+
}),
216+
catalogRelation({
217+
entityId: "table",
218+
name: "alpha",
219+
}),
220+
];
221+
const value = compose(
222+
"WITH zed AS (SELECT 1), users AS (SELECT 1) SELECT * FROM |",
223+
usable(readyResponse(relations)),
224+
);
225+
226+
expect(
227+
value.items.map((item) => [
228+
item.relationKind,
229+
item.label,
230+
item.edit.insert,
231+
]),
232+
).toEqual([
233+
["cte", "users", "users"],
234+
["cte", "zed", "zed"],
235+
["table", "alpha", "alpha"],
236+
["view", "beta", "beta"],
237+
["table", "users", "public.users"],
238+
["table", "aardvark", "aardvark"],
239+
]);
240+
expect(
241+
value.items.some(
242+
(item) =>
243+
item.provenance.kind === "catalog" &&
244+
item.provenance.entityId === "shadowed-users",
245+
),
246+
).toBe(false);
247+
expect(value.isIncomplete).toBe(false);
248+
});
249+
250+
it("uses the mapped whole-path replacement range and preserves qualified catalog candidates", () => {
251+
const local = markedLocalSite(
252+
"WITH users AS (SELECT 1) SELECT * FROM public.us|",
253+
);
254+
const result = composeSqlRelationCompletion({
255+
catalogOutcome: usable(
256+
readyResponse([
257+
catalogRelation({
258+
completionPathStart: 1,
259+
containers: ["public"],
260+
entityId: "users",
261+
name: "users",
262+
}),
263+
]),
264+
),
265+
dialect: POSTGRESQL_SQL_RELATION_DIALECT,
266+
localSite: local.localSite,
267+
providerId: "catalog",
268+
remainingIntentLeaseMs: 0,
269+
replacementRange: local.replacementRange,
270+
statementOffset: local.statementOffset,
271+
});
272+
273+
expect(result.value.items).toHaveLength(1);
274+
expect(result.value.items[0]?.edit).toEqual({
275+
from: local.replacementRange.from,
276+
insert: "users",
277+
to: local.replacementRange.to,
278+
});
279+
});
280+
281+
it("reports soft loading, terminal catalog evidence, and coverage in a stable issue order", () => {
282+
const loading = compose(
283+
"SELECT * FROM |",
284+
Object.freeze({ status: "loading" }),
285+
);
286+
expect(loading).toMatchObject({
287+
isIncomplete: true,
288+
issues: [
289+
{
290+
reason: "catalog-loading",
291+
remainingIntentLeaseMs: 25,
292+
},
293+
],
294+
});
295+
296+
const partial = compose(
297+
'SELECT * FROM "unterminated|',
298+
usable(
299+
readyResponse(
300+
[],
301+
Object.freeze({ kind: "partial" }),
302+
),
303+
),
304+
);
305+
expect(partial.issues.map((issue) => issue.reason)).toEqual([
306+
"query-site-recovery",
307+
"catalog-partial",
308+
]);
309+
310+
const paginated = compose(
311+
"SELECT * FROM |",
312+
usable(
313+
readyResponse(
314+
[],
315+
Object.freeze({
316+
continuationToken: "secret",
317+
kind: "paginated",
318+
}),
319+
),
320+
),
321+
);
322+
expect(paginated.issues).toEqual([
323+
{ reason: "catalog-paginated" },
324+
]);
325+
expect(JSON.stringify(paginated)).not.toContain("secret");
326+
});
327+
328+
it.each([
329+
["execution-timeout", "catalog-timeout", "execution-timeout"],
330+
["malformed-response", "catalog-malformed", "malformed-response"],
331+
["overloaded", "catalog-overloaded", "queue-overloaded"],
332+
["provider-failed", "catalog-failed", "provider-rejected"],
333+
["queue-timeout", "catalog-queue-timeout", "queue-timeout"],
334+
] as const)(
335+
"maps %s without discarding local evidence",
336+
(reason, issue, reportReason) => {
337+
const local = markedLocalSite(
338+
"WITH local_table AS (SELECT 1) SELECT * FROM |",
339+
);
340+
const result = composeSqlRelationCompletion({
341+
catalogOutcome: Object.freeze({
342+
reason,
343+
status: "unavailable",
344+
}),
345+
dialect: POSTGRESQL_SQL_RELATION_DIALECT,
346+
localSite: local.localSite,
347+
providerId: "catalog",
348+
remainingIntentLeaseMs: 0,
349+
replacementRange: local.replacementRange,
350+
statementOffset: local.statementOffset,
351+
});
352+
353+
expect(result.value.items[0]?.label).toBe("local_table");
354+
expect(result.value.issues).toContainEqual({ reason: issue });
355+
expect(result.sources).toEqual([
356+
{
357+
feature: "relation-catalog",
358+
outcome: "unavailable",
359+
providerId: "catalog",
360+
reason: reportReason,
361+
},
362+
]);
363+
},
364+
);
365+
366+
it("applies the result cap after ranking and returns deeply frozen output", () => {
367+
const declarations = Array.from(
368+
{ length: MAX_RELATION_COMPLETION_RESULTS + 1 },
369+
(_, index) => `cte_${String(index).padStart(3, "0")} AS (SELECT 1)`,
370+
).join(", ");
371+
const value = compose(
372+
`WITH ${declarations} SELECT * FROM |`,
373+
null,
374+
);
375+
376+
expect(value.items).toHaveLength(
377+
MAX_RELATION_COMPLETION_RESULTS,
378+
);
379+
expect(value.items[0]?.label).toBe("cte_000");
380+
expect(value.items.at(-1)?.label).toBe("cte_099");
381+
expect(value.issues).toContainEqual({ reason: "result-limit" });
382+
expect(Object.isFrozen(value)).toBe(true);
383+
expect(Object.isFrozen(value.items)).toBe(true);
384+
expect(Object.isFrozen(value.items[0]?.edit)).toBe(true);
385+
expect(Object.isFrozen(value.items[0]?.provenance)).toBe(true);
386+
});
387+
});

0 commit comments

Comments
 (0)