Skip to content

Commit d31de2c

Browse files
NicolappsConvex, Inc.
authored andcommitted
Improve how indexes are displayed in the CLI (#41926)
GitOrigin-RevId: 37cf170179055762f52ca9d4c67abfd8949bdb3e
1 parent ff2abf7 commit d31de2c

File tree

3 files changed

+264
-18
lines changed

3 files changed

+264
-18
lines changed

src/cli/lib/components.ts

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -49,17 +49,16 @@ import {
4949
deploymentSelectionWithinProjectFromOptions,
5050
loadSelectedDeploymentCredentials,
5151
} from "./api.js";
52-
import {
53-
FinishPushDiff,
54-
DeveloperIndexConfig,
55-
} from "./deployApi/finishPush.js";
52+
import { FinishPushDiff } from "./deployApi/finishPush.js";
5653
import { Reporter, Span } from "./tracing.js";
5754
import {
5855
DEFINITION_FILENAME_JS,
5956
DEFINITION_FILENAME_TS,
6057
} from "./components/constants.js";
6158
import { DeploymentSelection } from "./deploymentSelection.js";
6259
import { deploymentDashboardUrlPage } from "./dashboard.js";
60+
import { formatIndex } from "./indexes.js";
61+
6362
async function findComponentRootPath(ctx: Context, functionsDir: string) {
6463
// Default to `.ts` but fallback to `.js` if not present.
6564
let componentRootPath = path.resolve(
@@ -504,7 +503,7 @@ function printDiff(
504503
msg = msg.slice(0, -1); // strip last new line
505504
logFinishedStep(msg);
506505
}
507-
const addedStaged = rootDiff.added_indexes.filter((i) => i.staged);
506+
508507
const addedEnabled = rootDiff.added_indexes.filter((i) => !i.staged);
509508
if (addedEnabled.length > 0) {
510509
let msg = `${opts.dryRun ? "Would add" : "Added"} table indexes:\n`;
@@ -514,6 +513,8 @@ function printDiff(
514513
msg = msg.slice(0, -1); // strip last new line
515514
logFinishedStep(msg);
516515
}
516+
517+
const addedStaged = rootDiff.added_indexes.filter((i) => i.staged);
517518
if (addedStaged.length > 0) {
518519
let msg = `${opts.dryRun ? "Would add" : "Added"} staged table indexes:\n`;
519520
for (const index of addedStaged) {
@@ -522,11 +523,13 @@ function printDiff(
522523
opts.deploymentName,
523524
`/data?table=${table}&showIndexes=true`,
524525
);
525-
msg += ` [+] ${formatIndex(index)}, see progress: ${progressLink}\n`;
526+
msg += ` [+] ${formatIndex(index)}\n`;
527+
msg += ` See progress: ${progressLink}\n`;
526528
}
527529
msg = msg.slice(0, -1); // strip last new line
528530
logFinishedStep(msg);
529531
}
532+
530533
if (rootDiff.enabled_indexes && rootDiff.enabled_indexes.length > 0) {
531534
let msg = opts.dryRun
532535
? `These indexes would be enabled:\n`
@@ -537,6 +540,7 @@ function printDiff(
537540
msg = msg.slice(0, -1); // strip last new line
538541
logFinishedStep(msg);
539542
}
543+
540544
if (rootDiff.disabled_indexes && rootDiff.disabled_indexes.length > 0) {
541545
let msg = opts.dryRun
542546
? `These indexes would be staged:\n`
@@ -565,7 +569,3 @@ function printDiff(
565569
}
566570
}
567571
}
568-
569-
function formatIndex(index: DeveloperIndexConfig) {
570-
return `${index.name}`;
571-
}

src/cli/lib/indexes.test.ts

Lines changed: 156 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,156 @@
1+
import { describe, test, expect } from "vitest";
2+
import {
3+
IndexMetadata,
4+
toDeveloperIndexConfig,
5+
formatIndex,
6+
} from "./indexes.js";
7+
import { DeveloperIndexConfig } from "./deployApi/finishPush.js";
8+
import chalk from "chalk";
9+
10+
describe("toDeveloperIndexConfig", () => {
11+
test("converts database IndexMetadata to DeveloperIndexConfig", () => {
12+
const databaseIndex: IndexMetadata = {
13+
table: "messages",
14+
name: "by_user_and_timestamp",
15+
fields: ["userId", "timestamp"],
16+
backfill: {
17+
state: "done",
18+
},
19+
staged: false,
20+
};
21+
22+
const result = toDeveloperIndexConfig(databaseIndex);
23+
24+
expect(result).toEqual({
25+
type: "database",
26+
name: "messages.by_user_and_timestamp",
27+
fields: ["userId", "timestamp"],
28+
staged: false,
29+
});
30+
});
31+
32+
test("converts text search IndexMetadata to DeveloperIndexConfig", () => {
33+
const searchIndex: IndexMetadata = {
34+
table: "articles",
35+
name: "search_by_content",
36+
fields: {
37+
searchField: "content",
38+
filterFields: ["category", "status"],
39+
},
40+
backfill: {
41+
state: "done",
42+
},
43+
staged: false,
44+
};
45+
46+
const result = toDeveloperIndexConfig(searchIndex);
47+
48+
expect(result).toEqual({
49+
type: "search",
50+
name: "articles.search_by_content",
51+
searchField: "content",
52+
filterFields: ["category", "status"],
53+
staged: false,
54+
});
55+
});
56+
57+
test("converts vector search IndexMetadata to DeveloperIndexConfig", () => {
58+
const vectorIndex: IndexMetadata = {
59+
table: "documents",
60+
name: "embedding_index",
61+
fields: {
62+
dimensions: 1536,
63+
vectorField: "embedding",
64+
filterFields: ["userId", "type"],
65+
},
66+
backfill: {
67+
state: "done",
68+
},
69+
staged: true,
70+
};
71+
72+
const result = toDeveloperIndexConfig(vectorIndex);
73+
74+
expect(result).toEqual({
75+
type: "vector",
76+
name: "documents.embedding_index",
77+
dimensions: 1536,
78+
vectorField: "embedding",
79+
filterFields: ["userId", "type"],
80+
staged: true,
81+
});
82+
});
83+
});
84+
85+
describe("formatIndex", () => {
86+
test("formats database index with multiple fields", () => {
87+
const databaseIndex: DeveloperIndexConfig = {
88+
type: "database",
89+
name: "messages.by_user_and_timestamp",
90+
fields: ["userId", "timestamp"],
91+
};
92+
93+
const result = formatIndex(databaseIndex);
94+
95+
const expected = `messages.${chalk.bold("by_user_and_timestamp")} ${chalk.gray(`${chalk.underline("userId")}, ${chalk.underline("timestamp")}`)}`;
96+
expect(result).toEqual(expected);
97+
});
98+
99+
test("formats text index without filter fields", () => {
100+
const searchIndex: DeveloperIndexConfig = {
101+
type: "search",
102+
name: "articles.search_by_content",
103+
searchField: "content",
104+
filterFields: [],
105+
};
106+
107+
const result = formatIndex(searchIndex);
108+
109+
const expected = `articles.${chalk.bold("search_by_content")} ${chalk.gray(`${chalk.cyan("(text)")} ${chalk.underline("content")}`)}`;
110+
expect(result).toEqual(expected);
111+
});
112+
113+
test("formats text index with filter fields", () => {
114+
const searchIndex: DeveloperIndexConfig = {
115+
type: "search",
116+
name: "articles.search_by_content",
117+
searchField: "content",
118+
filterFields: ["category", "status"],
119+
};
120+
121+
const result = formatIndex(searchIndex);
122+
123+
const expected = `articles.${chalk.bold("search_by_content")} ${chalk.gray(`${chalk.cyan("(text)")} ${chalk.underline("content")}, filters on ${chalk.underline("category")}, ${chalk.underline("status")}`)}`;
124+
expect(result).toEqual(expected);
125+
});
126+
127+
test("formats vector index with single filter field", () => {
128+
const vectorIndex: DeveloperIndexConfig = {
129+
type: "vector",
130+
name: "documents.embedding_index",
131+
dimensions: 1536,
132+
vectorField: "embedding",
133+
filterFields: ["userId"],
134+
};
135+
136+
const result = formatIndex(vectorIndex);
137+
138+
const expected = `documents.${chalk.bold("embedding_index")} ${chalk.gray(`${chalk.cyan("(vector)")} ${chalk.underline("embedding")} (1536 dimensions), filter on ${chalk.underline("userId")}`)}`;
139+
expect(result).toEqual(expected);
140+
});
141+
142+
test("formats vector index with multiple filter fields", () => {
143+
const vectorIndex: DeveloperIndexConfig = {
144+
type: "vector",
145+
name: "documents.embedding_index",
146+
dimensions: 768,
147+
vectorField: "embedding",
148+
filterFields: ["userId", "type", "category"],
149+
};
150+
151+
const result = formatIndex(vectorIndex);
152+
153+
const expected = `documents.${chalk.bold("embedding_index")} ${chalk.gray(`${chalk.cyan("(vector)")} ${chalk.underline("embedding")} (768 dimensions), filters on ${chalk.underline("userId")}, ${chalk.underline("type")}, ${chalk.underline("category")}`)}`;
154+
expect(result).toEqual(expected);
155+
});
156+
});

src/cli/lib/indexes.ts

Lines changed: 98 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,9 @@ import {
1515
deprecationCheckWarning,
1616
} from "./utils/utils.js";
1717
import { deploymentDashboardUrlPage } from "./dashboard.js";
18+
import { DeveloperIndexConfig } from "./deployApi/finishPush.js";
1819

19-
type IndexMetadata = {
20+
export type IndexMetadata = {
2021
table: string;
2122
name: string;
2223
fields:
@@ -259,7 +260,7 @@ function logIndexChanges(
259260
if (indexes.dropped.length > 0) {
260261
let indexDiff = "";
261262
for (const index of indexes.dropped) {
262-
indexDiff += ` [-] ${stringifyIndex(index)}\n`;
263+
indexDiff += ` [-] ${formatIndex(toDeveloperIndexConfig(index))}\n`;
263264
}
264265
// strip last new line
265266
indexDiff = indexDiff.slice(0, -1);
@@ -272,7 +273,7 @@ function logIndexChanges(
272273
if (addedEnabled.length > 0) {
273274
let indexDiff = "";
274275
for (const index of addedEnabled) {
275-
indexDiff += ` [+] ${stringifyIndex(index)}\n`;
276+
indexDiff += ` [+] ${formatIndex(toDeveloperIndexConfig(index))}\n`;
276277
}
277278
// strip last new line
278279
indexDiff = indexDiff.slice(0, -1);
@@ -287,7 +288,7 @@ function logIndexChanges(
287288
deploymentName,
288289
`/data?table=${index.table}&showIndexes=true`,
289290
);
290-
indexDiff += ` [+] ${stringifyIndex(index)}, see progress: ${progressLink}\n`;
291+
indexDiff += ` [+] ${formatIndex(toDeveloperIndexConfig(index))}, see progress: ${progressLink}\n`;
291292
}
292293
// strip last new line
293294
indexDiff = indexDiff.slice(0, -1);
@@ -298,7 +299,7 @@ function logIndexChanges(
298299
if (indexes.enabled && indexes.enabled.length > 0) {
299300
let indexDiff = "";
300301
for (const index of indexes.enabled) {
301-
indexDiff += ` [*] ${stringifyIndex(index)}\n`;
302+
indexDiff += ` [*] ${formatIndex(toDeveloperIndexConfig(index))}\n`;
302303
}
303304
// strip last new line
304305
indexDiff = indexDiff.slice(0, -1);
@@ -310,7 +311,7 @@ function logIndexChanges(
310311
if (indexes.disabled && indexes.disabled.length > 0) {
311312
let indexDiff = "";
312313
for (const index of indexes.disabled) {
313-
indexDiff += ` [*] ${stringifyIndex(index)}\n`;
314+
indexDiff += ` [*] ${formatIndex(toDeveloperIndexConfig(index))}\n`;
314315
}
315316
// strip last new line
316317
indexDiff = indexDiff.slice(0, -1);
@@ -321,6 +322,95 @@ function logIndexChanges(
321322
}
322323
}
323324

324-
function stringifyIndex(index: IndexMetadata) {
325-
return `${index.table}.${index.name} ${JSON.stringify(index.fields)}`;
325+
export function toIndexMetadata(index: DeveloperIndexConfig): IndexMetadata {
326+
function extractFields(index: DeveloperIndexConfig): IndexMetadata["fields"] {
327+
if (index.type === "database") {
328+
return index.fields;
329+
} else if (index.type === "search") {
330+
return {
331+
searchField: index.searchField,
332+
filterFields: index.filterFields,
333+
};
334+
} else if (index.type === "vector") {
335+
return {
336+
dimensions: index.dimensions,
337+
vectorField: index.vectorField,
338+
filterFields: index.filterFields,
339+
};
340+
} else {
341+
index satisfies never;
342+
return [];
343+
}
344+
}
345+
346+
const [table, indexName] = index.name.split(".");
347+
return {
348+
table,
349+
name: indexName,
350+
fields: extractFields(index),
351+
backfill: {
352+
state: "done",
353+
},
354+
staged: index.staged ?? false,
355+
};
356+
}
357+
358+
export function toDeveloperIndexConfig(
359+
index: IndexMetadata,
360+
): DeveloperIndexConfig {
361+
const name = `${index.table}.${index.name}`;
362+
const commonProps = { name, staged: index.staged };
363+
364+
const { fields } = index;
365+
if (Array.isArray(fields)) {
366+
return {
367+
...commonProps,
368+
type: "database",
369+
fields: fields,
370+
};
371+
} else if ("searchField" in fields) {
372+
return {
373+
...commonProps,
374+
type: "search",
375+
searchField: fields.searchField,
376+
filterFields: fields.filterFields,
377+
};
378+
} else if ("vectorField" in fields) {
379+
return {
380+
...commonProps,
381+
type: "vector",
382+
vectorField: fields.vectorField,
383+
dimensions: fields.dimensions,
384+
filterFields: fields.filterFields,
385+
};
386+
} else {
387+
fields satisfies never;
388+
return { ...commonProps, type: "database", fields: [] };
389+
}
390+
}
391+
392+
export function formatIndex(index: DeveloperIndexConfig) {
393+
const [tableName, indexName] = index.name.split(".");
394+
return `${tableName}.${chalk.bold(indexName)} ${chalk.gray(formatIndexFields(index))}`;
395+
}
396+
397+
function formatIndexFields(index: DeveloperIndexConfig) {
398+
switch (index.type) {
399+
case "database":
400+
return " " + index.fields.map((f) => chalk.underline(f)).join(", ");
401+
case "search":
402+
return `${chalk.cyan("(text)")} ${chalk.underline(index.searchField)}${formatFilterFields(index.filterFields)}`;
403+
case "vector":
404+
return `${chalk.cyan("(vector)")} ${chalk.underline(index.vectorField)} (${index.dimensions} dimensions)${formatFilterFields(index.filterFields)}`;
405+
default:
406+
index satisfies never;
407+
return "";
408+
}
409+
}
410+
411+
function formatFilterFields(filterFields: string[]) {
412+
if (filterFields.length === 0) {
413+
return "";
414+
}
415+
return `, filter${filterFields.length === 1 ? "" : "s"} on ${filterFields.map((f) => chalk.underline(f)).join(", ")}`;
326416
}

0 commit comments

Comments
 (0)