diff --git a/apps/desktop/src/components/layout/EditorToolbar.vue b/apps/desktop/src/components/layout/EditorToolbar.vue
index d95225a1ae..848e98015d 100644
--- a/apps/desktop/src/components/layout/EditorToolbar.vue
+++ b/apps/desktop/src/components/layout/EditorToolbar.vue
@@ -61,7 +61,7 @@ const activeConnectionValue = computed(() => props.activeConnection?.id || "");
const activeSchemaValue = computed(() => props.activeTab.schema || "");
const supportsExplain = computed(() => {
const dbType = props.activeConnection?.db_type;
- return dbType !== "redis" && dbType !== "mongodb" && dbType !== "elasticsearch" && dbType !== "etcd";
+ return dbType !== "redis" && dbType !== "mongodb" && dbType !== "elasticsearch" && dbType !== "qdrant" && dbType !== "milvus" && dbType !== "etcd";
});
const isSingleDb = computed(() => isSingleDatabase(props.activeConnection?.db_type));
const hasDefaultDatabaseOption = computed(() => activeDatabaseOptions.value.includes(""));
@@ -250,7 +250,7 @@ function connectionById(connectionId: string): ConnectionConfig | undefined {
-
+
= {
connection: ["connection"],
database: ["database", "redis-db", "mq-tenant", "mongo-db"],
schema: ["schema"],
- table: ["table", "mongo-collection", "elasticsearch-index"],
+ table: ["table", "mongo-collection", "vector-collection", "elasticsearch-index"],
view: ["view"],
};
@@ -336,6 +336,8 @@ async function ensureTreeLoadedForTarget(target: ActiveTabSidebarTarget, opts?:
await store.loadMongoDatabases(connId);
} else if (config.db_type === "elasticsearch") {
await store.loadElasticsearchIndices(connId);
+ } else if (config.db_type === "qdrant" || config.db_type === "milvus") {
+ await store.loadVectorCollections(connId);
} else if (config.db_type === "mq") {
await store.loadMqTenants(connId, loadOptions);
} else {
diff --git a/apps/desktop/src/components/sidebar/TreeItem.vue b/apps/desktop/src/components/sidebar/TreeItem.vue
index 45964cc876..dc7433e8a7 100644
--- a/apps/desktop/src/components/sidebar/TreeItem.vue
+++ b/apps/desktop/src/components/sidebar/TreeItem.vue
@@ -247,6 +247,8 @@ function getIconInfo(node: TreeNode): { icon: any; colorClass: string } | null {
return { icon: Database, colorClass: "text-yellow-500" };
case "mongo-collection":
return { icon: Table, colorClass: "text-green-400" };
+ case "vector-collection":
+ return { icon: TableProperties, colorClass: "text-cyan-400" };
case "elasticsearch-index":
return { icon: Table, colorClass: "text-emerald-400" };
case "procedure":
@@ -283,7 +285,7 @@ function getIconInfo(node: TreeNode): { icon: any; colorClass: string } | null {
}
const groupTypes: Set = new Set(["group-columns", "group-indexes", "group-fkeys", "group-triggers", "group-tables", "group-views", "group-materialized-views", "group-procedures", "group-functions", "group-sequences", "group-packages", "group-partitions"]);
-const pinnableTypes: Set = new Set(["connection-group", "database", "linked-server", "linked-server-catalog", "linked-server-schema", "schema", "table", "view", "materialized_view", "redis-db", "mongo-db", "mongo-collection", "elasticsearch-index"]);
+const pinnableTypes: Set = new Set(["connection-group", "database", "linked-server", "linked-server-catalog", "linked-server-schema", "schema", "table", "view", "materialized_view", "redis-db", "mongo-db", "mongo-collection", "vector-collection", "elasticsearch-index"]);
function isGroupLabel(node: TreeNode): boolean {
return groupTypes.has(node.type);
@@ -299,7 +301,7 @@ function displayLabel(node: TreeNode): string {
}
function visibleLabel(node: TreeNode): string {
- if (node.type === "table" || node.type === "view" || node.type === "materialized_view" || node.type === "mongo-collection" || node.type === "elasticsearch-index") {
+ if (node.type === "table" || node.type === "view" || node.type === "materialized_view" || node.type === "mongo-collection" || node.type === "vector-collection" || node.type === "elasticsearch-index") {
return sidebarDisplayTableName(node.label, settingsStore.editorSettings.sidebarHiddenTablePrefixes);
}
return displayLabel(node);
@@ -335,6 +337,8 @@ function connectionTooltipScheme(config: Pick {
return t === "connection" || t === "database" || t === "schema" || t === "table" || t === "view" || t === "column" || t === "procedure" || t === "function" || t === "package" || t === "package-body" || isGroupLabel(props.node);
});
const columnComment = computed(() => (!settingsStore.editorSettings.sidebarHideTableComments && props.node.type === "column" && props.node.meta && "comment" in props.node.meta ? (props.node.meta as any).comment : null));
-const tableComment = computed(() => (!settingsStore.editorSettings.sidebarHideTableComments && (props.node.type === "table" || props.node.type === "view" || props.node.type === "mongo-collection" || props.node.type === "elasticsearch-index") && props.node.comment ? props.node.comment : null));
+const tableComment = computed(() =>
+ !settingsStore.editorSettings.sidebarHideTableComments && (props.node.type === "table" || props.node.type === "view" || props.node.type === "mongo-collection" || props.node.type === "vector-collection" || props.node.type === "elasticsearch-index") && props.node.comment ? props.node.comment : null,
+);
const paddingLeft = computed(() => treeItemPaddingLeft(props.depth));
const isConnected = computed(() => props.node.type === "connection" && !!props.node.connectionId && connectionStore.connectedIds.has(props.node.connectionId));
const isConnectionReadonly = computed(() => props.node.type === "connection" && !!props.node.connectionId && (connectionStore.getConfig(props.node.connectionId)?.read_only ?? false));
@@ -2735,7 +2746,7 @@ const nodeIconClass = computed(() => {
const canConfigureVisibleDatabases = computed(() => {
if (props.node.type !== "connection" || !props.node.connectionId) return false;
const dbType = connectionStore.getConfig(props.node.connectionId)?.db_type;
- return dbType !== "elasticsearch" && dbType !== "etcd" && dbType !== "mq";
+ return dbType !== "elasticsearch" && dbType !== "qdrant" && dbType !== "milvus" && dbType !== "etcd" && dbType !== "mq";
});
const canCopyFinalProxyPort = computed(() => {
if (props.node.type !== "connection" || !props.node.connectionId) return false;
@@ -3325,7 +3336,7 @@ function treeItemMenuItems(): ContextMenuItem[] {
return items;
}
- if (node.type === "elasticsearch-index") {
+ if (node.type === "elasticsearch-index" || node.type === "vector-collection") {
items.push({ label: t("contextMenu.copyName"), action: copyName, icon: Copy, shortcut: shortcutCopyName.value });
items.push({ label: "", separator: true });
items.push({ label: t("contextMenu.viewData"), action: toggle, icon: TableProperties });
diff --git a/apps/desktop/src/components/sql-file/SqlFileExecutionDialog.vue b/apps/desktop/src/components/sql-file/SqlFileExecutionDialog.vue
index f60e1d0181..a515027d50 100644
--- a/apps/desktop/src/components/sql-file/SqlFileExecutionDialog.vue
+++ b/apps/desktop/src/components/sql-file/SqlFileExecutionDialog.vue
@@ -49,7 +49,7 @@ const terminalStatus = ref("idle");
const terminalError = ref("");
const refreshedTarget = ref(false);
-const sqlConnections = computed(() => store.connections.filter((c) => !["redis", "mongodb", "elasticsearch", "etcd"].includes(c.db_type)));
+const sqlConnections = computed(() => store.connections.filter((c) => !["redis", "mongodb", "elasticsearch", "qdrant", "milvus", "etcd"].includes(c.db_type)));
const selectedConnection = computed(() => sqlConnections.value.find((c) => c.id === connectionId.value));
diff --git a/apps/desktop/src/components/vector/VectorBrowser.vue b/apps/desktop/src/components/vector/VectorBrowser.vue
new file mode 100644
index 0000000000..8a8a6daa81
--- /dev/null
+++ b/apps/desktop/src/components/vector/VectorBrowser.vue
@@ -0,0 +1,256 @@
+
+
+
+
+
+
+
{{ collectionLabel }}
+
{{ t("vector.productCollection", { product: productLabel }) }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
{{ statusMessage }}
+
+
+
+
+
+
diff --git a/apps/desktop/src/composables/useNavigationTargets.ts b/apps/desktop/src/composables/useNavigationTargets.ts
index 0d49e7e0a5..f23fc60ad7 100644
--- a/apps/desktop/src/composables/useNavigationTargets.ts
+++ b/apps/desktop/src/composables/useNavigationTargets.ts
@@ -25,6 +25,12 @@ async function openTableTarget(target: NavigationTarget, options: { tableInfoTab
connectionStore.activeConnectionId = target.connectionId;
const config = connectionStore.getConfig(target.connectionId);
const tabTitle = target.schema ? `${target.schema}.${target.tableName}` : target.tableName;
+ if (config?.db_type === "qdrant" || config?.db_type === "milvus") {
+ await connectionStore.ensureConnected(target.connectionId);
+ const tabId = queryStore.createTab(target.connectionId, target.database || "default", tabTitle, "vector");
+ queryStore.updateSql(tabId, target.tableName);
+ return;
+ }
const tabId = (() => {
if (settingsStore.editorSettings.reuseDataTab) {
const existing = queryStore.tabs.find((tab) => tab.mode === "data" && tab.connectionId === target.connectionId && tab.database === target.database);
diff --git a/apps/desktop/src/i18n/locales/en.ts b/apps/desktop/src/i18n/locales/en.ts
index 882a76e8d3..16948a1875 100644
--- a/apps/desktop/src/i18n/locales/en.ts
+++ b/apps/desktop/src/i18n/locales/en.ts
@@ -442,6 +442,7 @@ export default {
redis: "Redis",
etcd: "etcd",
mongo: "Mongo",
+ vector: "Vector",
objects: "Objects",
users: "Users & Privileges",
executionSummary: "Execution Summary",
@@ -1592,6 +1593,19 @@ export default {
filterPlaceholder: "Filter...",
sortPlaceholder: "Sort...",
},
+ vector: {
+ collectionFallback: "collection",
+ productCollection: "{product} collection",
+ browse: "Browse",
+ upsert: "Upsert",
+ delete: "Delete",
+ reset: "Reset",
+ refresh: "Refresh",
+ run: "Run",
+ apply: "Apply",
+ requestEditor: "Vector request editor",
+ operationSuccess: "Operation completed. The collection preview has been refreshed.",
+ },
history: {
title: "History",
search: "Search history...",
diff --git a/apps/desktop/src/i18n/locales/zh-CN.ts b/apps/desktop/src/i18n/locales/zh-CN.ts
index cb44b1b191..b768a380c2 100644
--- a/apps/desktop/src/i18n/locales/zh-CN.ts
+++ b/apps/desktop/src/i18n/locales/zh-CN.ts
@@ -443,6 +443,7 @@ export default {
redis: "Redis",
etcd: "etcd",
mongo: "Mongo",
+ vector: "向量",
objects: "对象",
users: "用户与权限",
executionSummary: "执行摘要",
@@ -1591,6 +1592,19 @@ export default {
filterPlaceholder: "过滤条件...",
sortPlaceholder: "排序条件...",
},
+ vector: {
+ collectionFallback: "集合",
+ productCollection: "{product} 集合",
+ browse: "浏览",
+ upsert: "写入",
+ delete: "删除",
+ reset: "重置",
+ refresh: "刷新",
+ run: "运行",
+ apply: "应用",
+ requestEditor: "向量请求编辑器",
+ operationSuccess: "操作已完成,集合预览已刷新。",
+ },
history: {
title: "历史",
search: "搜索历史...",
diff --git a/apps/desktop/src/i18n/locales/zh-TW.ts b/apps/desktop/src/i18n/locales/zh-TW.ts
index 2a34404864..5c76c6e081 100644
--- a/apps/desktop/src/i18n/locales/zh-TW.ts
+++ b/apps/desktop/src/i18n/locales/zh-TW.ts
@@ -420,6 +420,7 @@ export default {
tableData: "資料表",
redis: "Redis",
mongo: "Mongo",
+ vector: "向量",
objects: "物件",
tooltipTitle: "標題:",
tooltipConnection: "連線:",
@@ -1434,6 +1435,19 @@ export default {
filterPlaceholder: "過濾條件……",
sortPlaceholder: "排序條件……",
},
+ vector: {
+ collectionFallback: "集合",
+ productCollection: "{product} 集合",
+ browse: "瀏覽",
+ upsert: "寫入",
+ delete: "刪除",
+ reset: "重設",
+ refresh: "重新整理",
+ run: "執行",
+ apply: "套用",
+ requestEditor: "向量請求編輯器",
+ operationSuccess: "操作已完成,集合預覽已重新整理。",
+ },
history: {
title: "歷史",
search: "搜尋歷史……",
diff --git a/apps/desktop/src/lib/api.ts b/apps/desktop/src/lib/api.ts
index 3c708ee398..dc37f376d9 100644
--- a/apps/desktop/src/lib/api.ts
+++ b/apps/desktop/src/lib/api.ts
@@ -338,6 +338,7 @@ export const mongoDeleteDocuments = forward("mongoDeleteDocuments");
// Elasticsearch
export const elasticsearchListIndices = forward("elasticsearchListIndices");
+export const vectorListCollections = forward("vectorListCollections");
// History
export const saveHistory = forward("saveHistory");
diff --git a/apps/desktop/src/lib/connectionPresentation.ts b/apps/desktop/src/lib/connectionPresentation.ts
index c876878e65..068251fe3a 100644
--- a/apps/desktop/src/lib/connectionPresentation.ts
+++ b/apps/desktop/src/lib/connectionPresentation.ts
@@ -127,6 +127,8 @@ export function connectionUrlPlaceholder(dbType: DatabaseType): string {
return "oracle://user:password@host:port/service_name";
case "elasticsearch":
+ case "qdrant":
+ case "milvus":
return "http://user:password@host:port";
case "dameng":
diff --git a/apps/desktop/src/lib/connectionUrl.ts b/apps/desktop/src/lib/connectionUrl.ts
index 7c4454317b..f76b5d4376 100644
--- a/apps/desktop/src/lib/connectionUrl.ts
+++ b/apps/desktop/src/lib/connectionUrl.ts
@@ -39,6 +39,8 @@ const SCHEME_PROFILES: Record = {
mssql: { type: "sqlserver", profile: "sqlserver", label: "SQL Server", defaultPort: 1433 },
oracle: { type: "oracle", profile: "oracle", label: "Oracle", defaultPort: 1521 },
elasticsearch: { type: "elasticsearch", profile: "elasticsearch", label: "Elasticsearch", defaultPort: 9200 },
+ qdrant: { type: "qdrant", profile: "qdrant", label: "Qdrant", defaultPort: 6333 },
+ milvus: { type: "milvus", profile: "milvus", label: "Milvus", defaultPort: 19530 },
dm: { type: "dameng", profile: "dm", label: "DM (Dameng)", defaultPort: 5236 },
dameng: { type: "dameng", profile: "dm", label: "DM (Dameng)", defaultPort: 5236 },
gaussdb: { type: "gaussdb", profile: "gaussdb", label: "GaussDB", defaultPort: 5432 },
@@ -59,6 +61,8 @@ const SCHEME_PROFILES: Record = {
const HTTP_SELECTED_PROFILES: Record = {
clickhouse: SCHEME_PROFILES.clickhouse,
elasticsearch: SCHEME_PROFILES.elasticsearch,
+ qdrant: SCHEME_PROFILES.qdrant,
+ milvus: SCHEME_PROFILES.milvus,
};
function decodeUrlPart(value: string): string {
diff --git a/apps/desktop/src/lib/connectionVisibleDatabases.ts b/apps/desktop/src/lib/connectionVisibleDatabases.ts
index 2c4f6dedfd..da7a6d6841 100644
--- a/apps/desktop/src/lib/connectionVisibleDatabases.ts
+++ b/apps/desktop/src/lib/connectionVisibleDatabases.ts
@@ -3,7 +3,7 @@ import { filterDatabaseNamesForConnection, normalizeVisibleDatabaseSelection } f
const DRAFT_VISIBLE_DATABASES_PREFIX = "__visible_draft_";
-const UNSUPPORTED_VISIBLE_DATABASE_TYPES = new Set(["elasticsearch", "etcd"]);
+const UNSUPPORTED_VISIBLE_DATABASE_TYPES = new Set(["elasticsearch", "qdrant", "milvus", "etcd"]);
type VisibleDatabaseConnectionFields = Pick<
ConnectionConfig,
diff --git a/apps/desktop/src/lib/http.ts b/apps/desktop/src/lib/http.ts
index bddd6bc449..dce983178d 100644
--- a/apps/desktop/src/lib/http.ts
+++ b/apps/desktop/src/lib/http.ts
@@ -1460,6 +1460,10 @@ export async function elasticsearchListIndices(connectionId: string): Promise {
+ return mongoListCollections(connectionId, "default");
+}
+
export async function mongoFindDocuments(connectionId: string, database: string, collection: string, skip: number, limit: number, filter?: string, sort?: string, executionId?: string): Promise {
return post("/api/mongo/find-documents", { connectionId, database, collection, skip, limit, filter, sort, executionId });
}
diff --git a/apps/desktop/src/lib/sidebarActiveTabTarget.ts b/apps/desktop/src/lib/sidebarActiveTabTarget.ts
index c12a83c179..eafe9530b9 100644
--- a/apps/desktop/src/lib/sidebarActiveTabTarget.ts
+++ b/apps/desktop/src/lib/sidebarActiveTabTarget.ts
@@ -15,6 +15,11 @@ export type ActiveTabSidebarTarget =
database: string;
collectionName: string;
}
+ | {
+ type: "vector-collection";
+ connectionId: string;
+ collectionName: string;
+ }
| {
type: "etcd-root";
connectionId: string;
@@ -61,6 +66,16 @@ export function activeTabSidebarTarget(tab: QueryTab | undefined | null): Active
};
}
+ if (tab.mode === "vector") {
+ const collectionName = tab.sql || tab.title;
+ if (!collectionName) return null;
+ return {
+ type: "vector-collection",
+ connectionId: tab.connectionId,
+ collectionName,
+ };
+ }
+
if (tab.mode === "etcd") {
return { type: "etcd-root", connectionId: tab.connectionId };
}
@@ -99,6 +114,10 @@ export function matchesTarget(node: TreeNode, target: ActiveTabSidebarTarget): b
return node.type === "mongo-collection" && node.connectionId === target.connectionId && node.database === target.database && node.label === target.collectionName;
}
+ if (target.type === "vector-collection") {
+ return node.type === "vector-collection" && node.connectionId === target.connectionId && node.label === target.collectionName;
+ }
+
if (target.type === "query-context") {
if (target.schema) {
return node.type === "schema" && node.connectionId === target.connectionId && node.database === target.database && node.label === target.schema;
diff --git a/apps/desktop/src/lib/sidebarNodeOrdering.ts b/apps/desktop/src/lib/sidebarNodeOrdering.ts
index 33f570d854..8cb54425b0 100644
--- a/apps/desktop/src/lib/sidebarNodeOrdering.ts
+++ b/apps/desktop/src/lib/sidebarNodeOrdering.ts
@@ -30,7 +30,7 @@ export function sortSidebarTreeChildrenForParent(parent: Pick,
const regularChildren = normalized.filter((child) => child.type !== "user-admin" && child.type !== "saved-sql-root");
const withConnectionUtilityOrder = (children: TreeNode[]) => [...savedSqlNodes, ...children, ...userAdminNodes];
- if (databaseType === "mongodb" || databaseType === "elasticsearch") {
+ if (databaseType === "mongodb" || databaseType === "elasticsearch" || databaseType === "qdrant" || databaseType === "milvus") {
return withConnectionUtilityOrder(sortByLabel(regularChildren));
}
diff --git a/apps/desktop/src/lib/sidebarTreeItemLayout.ts b/apps/desktop/src/lib/sidebarTreeItemLayout.ts
index cf9ee9e887..9cf688fdd6 100644
--- a/apps/desktop/src/lib/sidebarTreeItemLayout.ts
+++ b/apps/desktop/src/lib/sidebarTreeItemLayout.ts
@@ -1,8 +1,8 @@
import type { TreeNodeType } from "@/types/database";
-const leafTypes: Set = new Set(["column", "index", "fkey", "trigger", "procedure", "function", "package", "package-body", "object-browser", "redis-db", "mq-tenant", "mongo-collection", "elasticsearch-index", "user-admin", "saved-sql-file", "load-more"]);
+const leafTypes: Set = new Set(["column", "index", "fkey", "trigger", "procedure", "function", "package", "package-body", "object-browser", "redis-db", "mq-tenant", "mongo-collection", "vector-collection", "elasticsearch-index", "user-admin", "saved-sql-file", "load-more"]);
-const fullWidthLabelTypes: Set = new Set(["table", "view", "materialized_view", "mongo-collection", "elasticsearch-index"]);
+const fullWidthLabelTypes: Set = new Set(["table", "view", "materialized_view", "mongo-collection", "vector-collection", "elasticsearch-index"]);
const emptyContainerTypes: Set = new Set(["saved-sql-root", "saved-sql-folder"]);
diff --git a/apps/desktop/src/lib/sqlSemanticDiagnostics.ts b/apps/desktop/src/lib/sqlSemanticDiagnostics.ts
index 407b31c7d5..8ce38b1cad 100644
--- a/apps/desktop/src/lib/sqlSemanticDiagnostics.ts
+++ b/apps/desktop/src/lib/sqlSemanticDiagnostics.ts
@@ -80,7 +80,7 @@ export function areSqlSemanticDiagnosticsEqual(left: readonly SqlSemanticDiagnos
}
export function shouldRunSqlSemanticDiagnostics(sql: string, cursor: number, options: { databaseType?: DatabaseType } = {}): boolean {
- if (options.databaseType === "mongodb" || options.databaseType === "elasticsearch" || options.databaseType === "redis") return false;
+ if (options.databaseType === "mongodb" || options.databaseType === "elasticsearch" || options.databaseType === "qdrant" || options.databaseType === "milvus" || options.databaseType === "redis") return false;
const context = getSqlCompletionContext(sql, cursor);
if (context.suggestTables || context.exclusiveTableSuggestions || context.exclusiveColumnSuggestions) return false;
if (context.qualifier) return false;
diff --git a/apps/desktop/src/lib/tabPresentation.ts b/apps/desktop/src/lib/tabPresentation.ts
index 4fccaacf97..d1736aea22 100644
--- a/apps/desktop/src/lib/tabPresentation.ts
+++ b/apps/desktop/src/lib/tabPresentation.ts
@@ -70,6 +70,10 @@ export function tabDisplayTitle(tab: QueryTab, t: Translate): string {
if (compact) return tab.sql;
return `${tab.sql}@${database}`;
}
+ if (tab.mode === "vector" && tab.sql) {
+ if (compact) return tab.sql;
+ return `${tab.sql}@${database}`;
+ }
if (tab.mode === "redis") {
if (compact) return connectionDisplayName(tab.connectionId);
return `${connectionDisplayName(tab.connectionId)}@${database}`;
@@ -106,6 +110,9 @@ export function tabTooltipLines(tab: QueryTab, t: Translate): { label: string; v
if (tab.mode === "mongo" && tab.sql) {
lines.push({ label: t("tabs.tooltipCollection"), value: tab.sql });
}
+ if (tab.mode === "vector" && tab.sql) {
+ lines.push({ label: t("tabs.tooltipCollection"), value: tab.sql });
+ }
if (tab.mode === "objects" && tab.objectBrowser?.schema) {
lines.push({ label: t("tabs.tooltipSchema"), value: tab.objectBrowser.schema });
}
@@ -171,6 +178,7 @@ export function tabModeLabel(tab: QueryTab, t: Translate): string {
if (tab.mode === "data") return t("tabs.table");
if (tab.mode === "query") return t("tabs.sql");
if (tab.mode === "mongo") return t("tabs.mongo");
+ if (tab.mode === "vector") return t("tabs.vector");
if (tab.mode === "redis") return t("tabs.redis");
if (tab.mode === "etcd") return t("tabs.etcd");
if (tab.mode === "objects") return t("tabs.objects");
diff --git a/apps/desktop/src/lib/tableMetadataCapabilities.ts b/apps/desktop/src/lib/tableMetadataCapabilities.ts
index be51a38039..b51e32471a 100644
--- a/apps/desktop/src/lib/tableMetadataCapabilities.ts
+++ b/apps/desktop/src/lib/tableMetadataCapabilities.ts
@@ -31,6 +31,18 @@ const capabilityByType: Partial {
+ return mongoListCollections(connectionId, "default");
+}
+
export async function mongoFindDocuments(connectionId: string, database: string, collection: string, skip: number, limit: number, filter?: string, sort?: string, executionId?: string): Promise {
return invoke("mongo_find_documents", { connectionId, database, collection, skip, limit, filter, sort, executionId });
}
diff --git a/apps/desktop/src/lib/treeNodeClick.ts b/apps/desktop/src/lib/treeNodeClick.ts
index 4e4639cafb..66489d2777 100644
--- a/apps/desktop/src/lib/treeNodeClick.ts
+++ b/apps/desktop/src/lib/treeNodeClick.ts
@@ -7,7 +7,7 @@ export type SidebarSelectionCopyAction = "copy-name" | "none";
export type SidebarActivation = "single" | "double";
const dataNodeTypes = new Set(["table", "view", "materialized_view"]);
-const toggleLeafNodeTypes = new Set(["redis-db", "mq-tenant", "mongo-collection", "elasticsearch-index", "user-admin"]);
+const toggleLeafNodeTypes = new Set(["redis-db", "mq-tenant", "mongo-collection", "vector-collection", "elasticsearch-index", "user-admin"]);
const objectBrowserNodeTypes = new Set(["database", "schema", "object-browser"]);
const sourceNodeTypes = new Set(["materialized_view", "procedure", "function", "sequence", "package", "package-body"]);
const savedSqlNodeTypes = new Set(["saved-sql-file"]);
diff --git a/apps/desktop/src/stores/connectionStore.ts b/apps/desktop/src/stores/connectionStore.ts
index 2cde76187c..715ff706c6 100644
--- a/apps/desktop/src/stores/connectionStore.ts
+++ b/apps/desktop/src/stores/connectionStore.ts
@@ -293,6 +293,8 @@ export const useConnectionStore = defineStore("connection", () => {
mongodb: "MongoDB",
oracle: "Oracle",
elasticsearch: "Elasticsearch",
+ qdrant: "Qdrant",
+ milvus: "Milvus",
doris: "Doris",
starrocks: "StarRocks",
manticoresearch: "Manticore Search",
@@ -788,6 +790,8 @@ export const useConnectionStore = defineStore("connection", () => {
await loadMongoDatabases(connectionId);
} else if (config.db_type === "elasticsearch") {
await loadElasticsearchIndices(connectionId);
+ } else if (config.db_type === "qdrant" || config.db_type === "milvus") {
+ await loadVectorCollections(connectionId);
} else if (config.db_type === "mq") {
await loadMqTenants(connectionId, { force: true });
} else {
@@ -1198,7 +1202,7 @@ export const useConnectionStore = defineStore("connection", () => {
withSavedSqlRoot(
connectionId,
sortSidebarNames(indices).map((index) => ({
- id: `${connectionId}:__es_index:${index}`,
+ id: `${connectionId}:__collection:${index}`,
label: index,
type: "elasticsearch-index" as const,
connectionId,
@@ -1217,6 +1221,38 @@ export const useConnectionStore = defineStore("connection", () => {
}
}
+ async function loadVectorCollections(connectionId: string) {
+ const node = findNode(treeNodes.value, connectionId);
+ if (!node) return;
+
+ node.isLoading = true;
+ try {
+ await ensureConnected(connectionId);
+ const collections = await api.vectorListCollections(connectionId);
+ setChildren(
+ node,
+ withSavedSqlRoot(
+ connectionId,
+ sortSidebarNames(collections).map((collection) => ({
+ id: `${connectionId}:__vector_collection:${collection}`,
+ label: collection,
+ type: "vector-collection" as const,
+ connectionId,
+ database: "default",
+ isExpanded: false,
+ })),
+ node,
+ ),
+ );
+ node.isExpanded = true;
+ } catch (e) {
+ recordMetadataLoadError(connectionId, e);
+ throw e;
+ } finally {
+ node.isLoading = false;
+ }
+ }
+
async function loadMongoCollections(connectionId: string, database: string) {
const nodeId = `${connectionId}:${database}`;
const node = findNode(treeNodes.value, nodeId);
@@ -1822,6 +1858,8 @@ export const useConnectionStore = defineStore("connection", () => {
await loadMongoDatabases(node.connectionId);
} else if (config?.db_type === "elasticsearch") {
await loadElasticsearchIndices(node.connectionId);
+ } else if (config?.db_type === "qdrant" || config?.db_type === "milvus") {
+ await loadVectorCollections(node.connectionId);
} else if (config?.db_type === "mq") {
await loadMqTenants(node.connectionId, options);
} else {
@@ -2967,6 +3005,7 @@ export const useConnectionStore = defineStore("connection", () => {
updateRedisDbKeyStats,
loadMongoDatabases,
loadElasticsearchIndices,
+ loadVectorCollections,
loadMongoCollections,
loadSchemas,
loadSqlServerDatabaseObjects,
diff --git a/apps/desktop/src/types/database.ts b/apps/desktop/src/types/database.ts
index 72bc088923..a17b0ece0f 100644
--- a/apps/desktop/src/types/database.ts
+++ b/apps/desktop/src/types/database.ts
@@ -11,6 +11,8 @@ export type DatabaseType =
| "mongodb"
| "oracle"
| "elasticsearch"
+ | "qdrant"
+ | "milvus"
| "doris"
| "starrocks"
| "manticoresearch"
@@ -438,6 +440,7 @@ export type TreeNodeType =
| "etcd-root"
| "mongo-db"
| "mongo-collection"
+ | "vector-collection"
| "elasticsearch-index";
export interface ConnectionGroup {
@@ -540,7 +543,7 @@ export interface QueryTab {
executionId?: string;
isExplaining?: boolean;
explainExecutionId?: string;
- mode: "data" | "query" | "redis" | "mongo" | "etcd" | "mq" | "objects" | "structure" | "users";
+ mode: "data" | "query" | "redis" | "mongo" | "vector" | "etcd" | "mq" | "objects" | "structure" | "users";
mqTenant?: string;
structureTableName?: string;
objectBrowser?: {
diff --git a/crates/dbx-core/assets/database-drivers.manifest.json b/crates/dbx-core/assets/database-drivers.manifest.json
index 3fd753d820..5029466a1e 100644
--- a/crates/dbx-core/assets/database-drivers.manifest.json
+++ b/crates/dbx-core/assets/database-drivers.manifest.json
@@ -320,6 +320,64 @@
"driverManagement": false
}
},
+ {
+ "dbType": "qdrant",
+ "label": "Qdrant",
+ "runtimeMode": "native",
+ "mcpMode": "bridge",
+ "singleConnectionPool": false,
+ "metadataConnectionScoped": false,
+ "skipTcpProbe": false,
+ "defaultPort": 6333,
+ "supportLevel": "browse",
+ "capabilities": {
+ "queryExecution": true,
+ "metadataBrowse": true,
+ "objectBrowser": false,
+ "objectSource": false,
+ "schemaSearch": false,
+ "diagram": false,
+ "tableDataEdit": false,
+ "tableStructureEdit": false,
+ "tableImport": false,
+ "dataTransfer": false,
+ "sqlFileExecution": false,
+ "databaseCreate": false,
+ "fieldLineage": false,
+ "sqlExplain": false,
+ "userAdmin": false,
+ "driverManagement": false
+ }
+ },
+ {
+ "dbType": "milvus",
+ "label": "Milvus",
+ "runtimeMode": "native",
+ "mcpMode": "bridge",
+ "singleConnectionPool": false,
+ "metadataConnectionScoped": false,
+ "skipTcpProbe": false,
+ "defaultPort": 19530,
+ "supportLevel": "browse",
+ "capabilities": {
+ "queryExecution": true,
+ "metadataBrowse": true,
+ "objectBrowser": false,
+ "objectSource": false,
+ "schemaSearch": false,
+ "diagram": false,
+ "tableDataEdit": false,
+ "tableStructureEdit": false,
+ "tableImport": false,
+ "dataTransfer": false,
+ "sqlFileExecution": false,
+ "databaseCreate": false,
+ "fieldLineage": false,
+ "sqlExplain": false,
+ "userAdmin": false,
+ "driverManagement": false
+ }
+ },
{
"dbType": "doris",
"label": "Doris",
diff --git a/crates/dbx-core/src/connection.rs b/crates/dbx-core/src/connection.rs
index 100046b760..b09b91034d 100644
--- a/crates/dbx-core/src/connection.rs
+++ b/crates/dbx-core/src/connection.rs
@@ -67,6 +67,7 @@ pub enum PoolKind {
ClickHouse(db::clickhouse_driver::ChClient),
SqlServer(Arc>),
Elasticsearch(db::elasticsearch_driver::EsClient),
+ VectorDb(db::vector_driver::VectorClient),
InfluxDb(db::influxdb_driver::InfluxdbClient),
Agent(Arc>),
ExternalTabular(ExternalTabularHandle),
@@ -601,6 +602,23 @@ impl AppState {
db::elasticsearch_driver::test_connection(&mut client, connect_timeout).await?;
PoolKind::Elasticsearch(client)
}
+ DatabaseType::Qdrant | DatabaseType::Milvus => {
+ let kind = match db_config.db_type {
+ DatabaseType::Qdrant => db::vector_driver::VectorDbKind::Qdrant,
+ DatabaseType::Milvus => db::vector_driver::VectorDbKind::Milvus,
+ _ => unreachable!(),
+ };
+ let client = db::vector_driver::VectorClient::new(
+ kind,
+ &url,
+ Some(&db_config.username),
+ Some(&db_config.password),
+ db_config.ssl,
+ connect_timeout,
+ );
+ db::vector_driver::test_connection(&client, connect_timeout).await?;
+ PoolKind::VectorDb(client)
+ }
DatabaseType::InfluxDb => {
let username = if db_config.username.is_empty() { None } else { Some(db_config.username.clone()) };
let password = if db_config.password.is_empty() { None } else { Some(db_config.password.clone()) };
@@ -1292,6 +1310,7 @@ enum KeepaliveTarget {
ClickHouse(db::clickhouse_driver::ChClient),
SqlServer(Arc>),
Elasticsearch(db::elasticsearch_driver::EsClient),
+ VectorDb(db::vector_driver::VectorClient),
InfluxDb(db::influxdb_driver::InfluxdbClient),
}
@@ -1308,6 +1327,7 @@ fn keepalive_target_from_pool(pool: &PoolKind, config: &ConnectionConfig) -> Opt
PoolKind::ClickHouse(client) => Some(KeepaliveTarget::ClickHouse(client.clone())),
PoolKind::SqlServer(client) => Some(KeepaliveTarget::SqlServer(client.clone())),
PoolKind::Elasticsearch(client) => Some(KeepaliveTarget::Elasticsearch(client.clone())),
+ PoolKind::VectorDb(client) => Some(KeepaliveTarget::VectorDb(client.clone())),
PoolKind::InfluxDb(client) => Some(KeepaliveTarget::InfluxDb(client.clone())),
_ => None,
}
@@ -1334,6 +1354,7 @@ async fn ping_keepalive_target(target: &mut KeepaliveTarget, timeout: Duration)
db::sqlserver::test_connection(&mut client).await
}
KeepaliveTarget::Elasticsearch(client) => db::elasticsearch_driver::test_connection(client, timeout).await,
+ KeepaliveTarget::VectorDb(client) => db::vector_driver::test_connection(client, timeout).await,
KeepaliveTarget::InfluxDb(client) => db::influxdb_driver::test_connection(client, timeout).await,
}
}
@@ -1452,6 +1473,7 @@ pub async fn close_pool_kind(pool: PoolKind) {
PoolKind::ClickHouse(_) => {}
PoolKind::SqlServer(_) => {}
PoolKind::Elasticsearch(_) => {}
+ PoolKind::VectorDb(_) => {}
PoolKind::InfluxDb(_) => {}
PoolKind::Agent(client) => {
let mut client = client.lock().await;
@@ -1510,7 +1532,8 @@ fn base_pool_key_for(
) -> String {
let is_single_connection_pool = db_type.as_ref().is_some_and(|db_type| {
let is_single = database_capabilities::is_single_connection_pool(db_type)
- || (include_elasticsearch_single_pool && *db_type == DatabaseType::Elasticsearch);
+ || (include_elasticsearch_single_pool
+ && matches!(db_type, DatabaseType::Elasticsearch | DatabaseType::Qdrant | DatabaseType::Milvus));
is_single && (!database_capabilities::is_agent_type(db_type) || shares_database_pool_with_connection(db_type))
});
diff --git a/crates/dbx-core/src/db/mod.rs b/crates/dbx-core/src/db/mod.rs
index c15b36f7a0..2654a4b198 100644
--- a/crates/dbx-core/src/db/mod.rs
+++ b/crates/dbx-core/src/db/mod.rs
@@ -19,6 +19,7 @@ pub mod sqlserver;
pub mod ssh_tunnel;
pub mod transport_layer_tunnel;
pub mod turso_driver;
+pub mod vector_driver;
pub mod wkb;
use reqwest::ClientBuilder;
diff --git a/crates/dbx-core/src/db/vector_driver.rs b/crates/dbx-core/src/db/vector_driver.rs
new file mode 100644
index 0000000000..8565f0a760
--- /dev/null
+++ b/crates/dbx-core/src/db/vector_driver.rs
@@ -0,0 +1,408 @@
+use percent_encoding::{utf8_percent_encode, AsciiSet, CONTROLS};
+use reqwest::Client as HttpClient;
+use serde_json::{Map, Value};
+use std::error::Error;
+use std::time::{Duration, Instant};
+
+use super::{http_client_builder, with_connection_timeout};
+use crate::types::QueryResult;
+
+const PATH_SEGMENT_ENCODE_SET: &AsciiSet = &CONTROLS
+ .add(b' ')
+ .add(b'"')
+ .add(b'#')
+ .add(b'%')
+ .add(b'/')
+ .add(b'<')
+ .add(b'>')
+ .add(b'?')
+ .add(b'[')
+ .add(b'\\')
+ .add(b']')
+ .add(b'^')
+ .add(b'`')
+ .add(b'{')
+ .add(b'|')
+ .add(b'}');
+
+#[derive(Debug, Clone, Copy, PartialEq, Eq)]
+pub enum VectorDbKind {
+ Qdrant,
+ Milvus,
+}
+
+impl VectorDbKind {
+ fn label(self) -> &'static str {
+ match self {
+ VectorDbKind::Qdrant => "Qdrant",
+ VectorDbKind::Milvus => "Milvus",
+ }
+ }
+}
+
+#[derive(Clone)]
+pub struct VectorClient {
+ kind: VectorDbKind,
+ http: HttpClient,
+ base_url: String,
+ auth: Option,
+}
+
+#[derive(Clone)]
+enum VectorAuth {
+ Basic(String, String),
+ Bearer(String),
+ ApiKey(String),
+}
+
+impl VectorClient {
+ pub fn new(
+ kind: VectorDbKind,
+ url: &str,
+ username: Option<&str>,
+ password: Option<&str>,
+ accept_invalid_certs: bool,
+ timeout: Duration,
+ ) -> Self {
+ let base_url = url.trim_end_matches('/').to_string();
+ let auth = vector_auth(kind, username, password);
+ let builder = http_client_builder(timeout).danger_accept_invalid_certs(accept_invalid_certs);
+ let http = builder.build().unwrap_or_else(|_| HttpClient::new());
+ Self { kind, http, base_url, auth }
+ }
+
+ fn get(&self, path: &str) -> reqwest::RequestBuilder {
+ self.with_auth(self.http.get(format!("{}{}", self.base_url, path)))
+ }
+
+ fn post(&self, path: &str) -> reqwest::RequestBuilder {
+ self.with_auth(self.http.post(format!("{}{}", self.base_url, path)))
+ }
+
+ fn put(&self, path: &str) -> reqwest::RequestBuilder {
+ self.with_auth(self.http.put(format!("{}{}", self.base_url, path)))
+ }
+
+ fn delete(&self, path: &str) -> reqwest::RequestBuilder {
+ self.with_auth(self.http.delete(format!("{}{}", self.base_url, path)))
+ }
+
+ fn with_auth(&self, req: reqwest::RequestBuilder) -> reqwest::RequestBuilder {
+ match &self.auth {
+ Some(VectorAuth::Basic(user, pass)) => req.basic_auth(user, Some(pass)),
+ Some(VectorAuth::Bearer(token)) => req.bearer_auth(token),
+ Some(VectorAuth::ApiKey(token)) => req.header("api-key", token),
+ None => req,
+ }
+ }
+}
+
+fn vector_auth(kind: VectorDbKind, username: Option<&str>, password: Option<&str>) -> Option {
+ let username = username.unwrap_or("").trim();
+ let password = password.unwrap_or("");
+ match kind {
+ VectorDbKind::Qdrant if !username.is_empty() => {
+ Some(VectorAuth::Basic(username.to_string(), password.to_string()))
+ }
+ VectorDbKind::Qdrant if !password.is_empty() => Some(VectorAuth::ApiKey(password.to_string())),
+ VectorDbKind::Qdrant => None,
+ VectorDbKind::Milvus if !username.is_empty() => Some(VectorAuth::Bearer(format!("{username}:{password}"))),
+ VectorDbKind::Milvus => None,
+ }
+}
+
+pub async fn test_connection(client: &VectorClient, timeout: Duration) -> Result<(), String> {
+ let label = client.kind.label();
+ let path = match client.kind {
+ VectorDbKind::Qdrant => "/collections",
+ VectorDbKind::Milvus => "/v2/vectordb/collections/list",
+ };
+ let request = match client.kind {
+ VectorDbKind::Qdrant => client.get(path),
+ VectorDbKind::Milvus => client.post(path).json(&serde_json::json!({ "dbName": "default" })),
+ };
+ let resp = with_connection_timeout(label, timeout, async {
+ request.send().await.map_err(|e| format!("{label} connection failed: {}", format_reqwest_error(&e)))
+ })
+ .await?;
+ ensure_success(label, resp).await.map(|_| ())
+}
+
+pub async fn list_collections(client: &VectorClient) -> Result, String> {
+ match client.kind {
+ VectorDbKind::Qdrant => list_qdrant_collections(client).await,
+ VectorDbKind::Milvus => list_milvus_collections(client).await,
+ }
+}
+
+async fn list_qdrant_collections(client: &VectorClient) -> Result, String> {
+ let body = send_json(client.get("/collections"), "Qdrant").await?;
+ let mut names: Vec = body
+ .pointer("/result/collections")
+ .and_then(Value::as_array)
+ .into_iter()
+ .flatten()
+ .filter_map(|item| item.get("name").and_then(Value::as_str).map(str::to_string))
+ .collect();
+ names.sort();
+ Ok(names)
+}
+
+async fn list_milvus_collections(client: &VectorClient) -> Result, String> {
+ let body = send_json(
+ client.post("/v2/vectordb/collections/list").json(&serde_json::json!({ "dbName": "default" })),
+ "Milvus",
+ )
+ .await?;
+ let mut names: Vec = match body.get("data") {
+ Some(Value::Array(items)) => items.iter().filter_map(collection_name_from_milvus_item).collect(),
+ _ => Vec::new(),
+ };
+ names.sort();
+ Ok(names)
+}
+
+fn collection_name_from_milvus_item(item: &Value) -> Option {
+ item.as_str()
+ .map(str::to_string)
+ .or_else(|| item.get("collectionName").and_then(Value::as_str).map(str::to_string))
+ .or_else(|| item.get("name").and_then(Value::as_str).map(str::to_string))
+}
+
+pub async fn find_documents(
+ client: &VectorClient,
+ collection: &str,
+ skip: u64,
+ limit: i64,
+) -> Result {
+ let query = match client.kind {
+ VectorDbKind::Qdrant => format!(
+ "POST /collections/{}/points/scroll\n{}",
+ path_segment(collection),
+ serde_json::json!({
+ "limit": limit.max(1) as u64,
+ "offset": if skip == 0 { Value::Null } else { Value::from(skip) },
+ "with_payload": true,
+ "with_vector": false,
+ })
+ ),
+ VectorDbKind::Milvus => format!(
+ "POST /v2/vectordb/entities/query\n{}",
+ serde_json::json!({
+ "dbName": "default",
+ "collectionName": collection,
+ "filter": "",
+ "limit": limit.max(1) as u64,
+ "offset": skip,
+ "outputFields": ["*"],
+ })
+ ),
+ };
+ let result = execute_rest_query(client, &query).await?;
+ let documents = result
+ .rows
+ .into_iter()
+ .map(|row| {
+ let mut map = Map::new();
+ for (idx, column) in result.columns.iter().enumerate() {
+ map.insert(column.clone(), row.get(idx).cloned().unwrap_or(Value::Null));
+ }
+ Value::Object(map)
+ })
+ .collect();
+ Ok(crate::db::mongo_driver::MongoDocumentResult { documents, total: result.affected_rows })
+}
+
+pub async fn execute_rest_query(client: &VectorClient, input: &str) -> Result {
+ let start = Instant::now();
+ let request = parse_rest_query(client, input)?;
+ let resp = request.send().await.map_err(|e| format!("{} request failed: {e}", client.kind.label()))?;
+ let status = resp.status().as_u16();
+ let body = resp.json::().await.unwrap_or(Value::Null);
+ if !(200..300).contains(&status) {
+ let detail = serde_json::to_string_pretty(&body).unwrap_or_else(|_| body.to_string());
+ return Err(format!("{} error ({status}): {detail}", client.kind.label()));
+ }
+ Ok(json_to_query_result(status, body, start))
+}
+
+fn parse_rest_query(client: &VectorClient, input: &str) -> Result {
+ let trimmed = input.trim();
+ if trimmed.is_empty() {
+ return Err(format!("{} query cannot be empty", client.kind.label()));
+ }
+
+ if !starts_with_http_method(trimmed) {
+ return default_collection_query(client, trimmed);
+ }
+
+ let (head, body) = trimmed.split_once('\n').map_or((trimmed, ""), |(head, body)| (head.trim(), body.trim()));
+ let mut parts = head.split_whitespace();
+ let method = parts.next().unwrap_or("").to_ascii_uppercase();
+ let path = parts.next().ok_or_else(|| "Vector query path is required".to_string())?;
+ let path = if path.starts_with('/') { path.to_string() } else { format!("/{path}") };
+ let req = match method.as_str() {
+ "GET" => client.get(&path),
+ "POST" => client.post(&path),
+ "PUT" => client.put(&path),
+ "DELETE" => client.delete(&path),
+ other => return Err(format!("Unsupported vector REST method: {other}")),
+ };
+ if body.is_empty() {
+ Ok(req)
+ } else {
+ let json: Value = serde_json::from_str(body).map_err(|e| format!("Vector query body must be JSON: {e}"))?;
+ Ok(req.json(&json))
+ }
+}
+
+fn default_collection_query(client: &VectorClient, collection: &str) -> Result {
+ let collection = collection.trim();
+ if collection.is_empty() {
+ return Err("Vector collection name cannot be empty".to_string());
+ }
+ match client.kind {
+ VectorDbKind::Qdrant => Ok(client
+ .post(&format!("/collections/{}/points/scroll", path_segment(collection)))
+ .json(&serde_json::json!({ "limit": 100, "with_payload": true, "with_vector": false }))),
+ VectorDbKind::Milvus => Ok(client.post("/v2/vectordb/entities/query").json(&serde_json::json!({
+ "dbName": "default",
+ "collectionName": collection,
+ "filter": "",
+ "limit": 100,
+ "outputFields": ["*"],
+ }))),
+ }
+}
+
+fn starts_with_http_method(input: &str) -> bool {
+ ["GET ", "POST ", "PUT ", "DELETE "].iter().any(|prefix| input.to_ascii_uppercase().starts_with(prefix))
+}
+
+fn path_segment(value: &str) -> String {
+ utf8_percent_encode(value, PATH_SEGMENT_ENCODE_SET).to_string()
+}
+
+async fn send_json(req: reqwest::RequestBuilder, label: &str) -> Result {
+ let resp = req.send().await.map_err(|e| format!("{label} request failed: {e}"))?;
+ let resp = ensure_success(label, resp).await?;
+ resp.json().await.map_err(|e| format!("{label} parse error: {e}"))
+}
+
+async fn ensure_success(label: &str, resp: reqwest::Response) -> Result {
+ if resp.status().is_success() {
+ return Ok(resp);
+ }
+ let status = resp.status();
+ let body = resp.text().await.unwrap_or_default();
+ Err(format!("{label} error ({status}): {body}"))
+}
+
+fn json_to_query_result(status: u16, body: Value, start: Instant) -> QueryResult {
+ let rows_value =
+ body.pointer("/result/points").or_else(|| body.get("result")).or_else(|| body.get("data")).cloned();
+ if let Some(Value::Array(items)) = rows_value {
+ return values_to_query_result(items, start);
+ }
+ if let Some(Value::Array(items)) = body.pointer("/result/collections").cloned() {
+ return values_to_query_result(items, start);
+ }
+ QueryResult {
+ columns: vec!["status".to_string(), "response".to_string()],
+ column_types: Vec::new(),
+ column_sortables: vec![],
+ rows: vec![vec![
+ Value::Number(status.into()),
+ Value::String(serde_json::to_string_pretty(&body).unwrap_or_else(|_| body.to_string())),
+ ]],
+ affected_rows: 0,
+ execution_time_ms: start.elapsed().as_millis(),
+ truncated: false,
+ session_id: None,
+ has_more: false,
+ }
+}
+
+fn values_to_query_result(items: Vec, start: Instant) -> QueryResult {
+ let docs: Vec