From bf9ef4365b9af2e1829e9d9787767c012039b8e8 Mon Sep 17 00:00:00 2001 From: t8y2 <1156263951@qq.com> Date: Thu, 25 Jun 2026 00:31:07 +0800 Subject: [PATCH] feat(vector): add weaviate support --- .../public/icons/database/weaviate.png | Bin 0 -> 2198 bytes apps/desktop/src/App.vue | 4 +- .../connection/ConnectionDialog.vue | 5 +- .../src/components/diff/DataCompareDialog.vue | 2 +- .../export/DatabaseExportDialog.vue | 2 +- .../src/components/icons/DatabaseIcon.vue | 1 + .../src/components/layout/EditorToolbar.vue | 4 +- .../src/components/sidebar/ConnectionTree.vue | 2 +- .../src/components/sidebar/TreeItem.vue | 5 +- .../sql-file/SqlFileExecutionDialog.vue | 2 +- .../src/components/vector/VectorBrowser.vue | 22 ++++- .../src/composables/useNavigationTargets.ts | 2 +- .../lib/__tests__/sqlStatementRanges.spec.ts | 1 + .../desktop/src/lib/connectionPresentation.ts | 1 + apps/desktop/src/lib/connectionUrl.ts | 2 + .../src/lib/connectionVisibleDatabases.ts | 2 +- apps/desktop/src/lib/sidebarNodeOrdering.ts | 2 +- .../desktop/src/lib/sqlSemanticDiagnostics.ts | 2 +- apps/desktop/src/lib/sqlStatementRanges.ts | 3 +- .../src/lib/tableMetadataCapabilities.ts | 6 ++ apps/desktop/src/stores/connectionStore.ts | 5 +- apps/desktop/src/types/database.ts | 1 + .../assets/database-drivers.manifest.json | 29 +++++++ crates/dbx-core/src/connection.rs | 8 +- crates/dbx-core/src/db/vector_driver.rs | 78 +++++++++++++++++- crates/dbx-core/src/models/connection.rs | 6 +- crates/dbx-core/src/query.rs | 1 + crates/dbx-core/src/query_execution_sql.rs | 1 + packages/mcp-server/src/index.ts | 2 +- packages/node-core/src/diagnostics.ts | 1 + src-tauri/src/commands/connection.rs | 6 +- 31 files changed, 180 insertions(+), 28 deletions(-) create mode 100644 apps/desktop/public/icons/database/weaviate.png diff --git a/apps/desktop/public/icons/database/weaviate.png b/apps/desktop/public/icons/database/weaviate.png new file mode 100644 index 0000000000000000000000000000000000000000..8159f50fb1306c51378b19d6edb67d9d6f8d184a GIT binary patch literal 2198 zcmV;H2x<3;P)Px#L}ge>W=%~1DgXcg2mk?xX#fNO00031000^Q000001E2u_0{{R30RRC20H6W@ z1ONa40RR91FrWhf1ONa40RR91FaQ7m0NXcg3;+NK07*naRA>e5SzBxz)fxU~X79;* zy^bAUVw^PLRzd=#tq>td2n0byc>#e0kBEmh;!**ReQ4FkK3A2>1Be%-A_QtnO9>Bv zxQH}Klavrh$i*ba&c>Hr?`>vhXNT`UJF}j#y-u8s6d~wj-lV}_I zpLOvQCjBRjT|w^}aJ?hgjX*a79eSXfLLEA=WA<(YI(A`)0#+S?^#dkGhO5w5&DkEQ zrU4^ERSXT9?H9F~xgr?4?qb788Lzx}7$5!TO}zi^->~=5b8SSfe)!Rc&tw1J-@>Qw zzlq}Btbq$Z^q^9StSG1LtaxD8y+7c&eTQ)4=2^sIbu=1O zArGAsjn`Hbyqd{qqz;ENo3dSO-aLaBetiIYcAvtZU;PC>`fL=A!#rDtmKYF?HZVL~ z#(N1^7V7 z0IpHM#2Y~YLGU6-LM&_AQ7q-4f2@Kvy|!9XDMT?no`O{|mSvH2T%1BdoxTAQjl~o) zKwRLbjRNX{Rl>+A!2R*%3`q3UvHh_rY<=V+BI!Cb!?Uuk!7fK};)6jPd3y+EKCD&@ zP)=hj7=7pYy8Jy+6FiG72uTvg8iiOQNEWkg3E_LI&v9Ajld;x8W?LDz?>mpomNG6j zG?rYQ{5qug$w_Nu8W? z5%7U~0z_B~yw?|{VAYE4s35&(4!yeynB>_pX{MCnT2PZJ;VutB8Z*+sx<6ci{&@z4 z_p-1Krd0Y!acBW2uQEp30NgtJ!FQQjVt{zoTt=8Of^$M5#7s8eygUPYX9@W=4hoGp zt#3|>$6MrqS4^H&uGGGrRk(MO;lLVLX{Qx+-%&08K&QE*0t4)Z4Ina$;CYI-yx|K} z9wCElwizD6N3(UzKFG_LLlG7qSs-8)VEz%1s?SytBjCB!6`^u^#kt`A3XT>{NOc^D z5>SZ8=WVv!N|%p-=#=0rPZ)&Fg5rM6Y}E&?(*EBM2l9#S^O5 za)Ci>5_|-(+(#N-h$X3cQM4=#-%)0cei%%!T*jKBnwl5>N(6j${%AM?9JPp;w~5d~ zNvVl4P^1JgL=o>MdWG72;86ibkU2)B#DxN0tY@SC@B)YokqxH!F4q$ZmHPu1Xx#MT ztOD6eO!PD(G0J7h5ITw!QHvAMV7AS?CyyEdgkO5XDhVe7KaMPvM9tdy51x}s&U!RY` zo#Lg>;-RIy&UraJ1dMt-fBE25G{hE87U-dI8n`(QZ9UP2akUgxxy)qxN~WGalZ8o) zK?)ndO?mQ@ThO<07Re0FZ$naQr}GTWH)VR5UWaUEf@(d)BVZ-ww8T|kS8QdT<9;in5@gp{j5)Jokk4~^KFkQ^82I?7az52Xs@yuv~bHg+YepW^$V@NTcs+w;A z!D}p8ktq|gZ>#K!K>^~c!2*>!$P;%{1onUdSIR2W$>pAjlRx3-4h_lAM=(Fphp*qd z2l=xD0YOW3Mu4{Y=Towpl7IvRF9>~T0ClFX7XqBOcn-HCVIMp*!RnT59rj0 zIZ|+;GyPQgWqPb%cM03>ItKX`k#cFxrg3`z9XRp!-6-dJ0^G}YFKd9XHMl48VHCC;fq)`VHqa)({CmOSmfX$p=Ew$YP zD+#JoQRym_Ehn?ax-QSvlD8$Vmi_wkbq%=QG3Z918-Wfz&`qHZ9oR8@Hv%2IutNbq Y0=RTRh@cc05C8xG07*qoM6N<$g0%JYEC2ui literal 0 HcmV?d00001 diff --git a/apps/desktop/src/App.vue b/apps/desktop/src/App.vue index 22e273456..7f4f6e4bb 100644 --- a/apps/desktop/src/App.vue +++ b/apps/desktop/src/App.vue @@ -1040,7 +1040,7 @@ async function handleQuickOpenSelect(item: any) { await connectionStore.loadMongoDatabases(item.connectionId); } else if (config?.db_type === "elasticsearch") { await connectionStore.loadElasticsearchIndices(item.connectionId); - } else if (config?.db_type === "qdrant" || config?.db_type === "milvus") { + } else if (config?.db_type === "qdrant" || config?.db_type === "milvus" || config?.db_type === "weaviate") { await connectionStore.loadVectorCollections(item.connectionId); } else if (config?.db_type === "mq") { await connectionStore.loadMqTenants(item.connectionId); @@ -1063,7 +1063,7 @@ async function handleQuickOpenSelect(item: any) { await connectionStore.loadMongoDatabases(item.connectionId); } else if (config?.db_type === "elasticsearch") { await connectionStore.loadElasticsearchIndices(item.connectionId); - } else if (config?.db_type === "qdrant" || config?.db_type === "milvus") { + } else if (config?.db_type === "qdrant" || config?.db_type === "milvus" || config?.db_type === "weaviate") { await connectionStore.loadVectorCollections(item.connectionId); } else if (config?.db_type === "mq") { await connectionStore.loadMqTenants(item.connectionId); diff --git a/apps/desktop/src/components/connection/ConnectionDialog.vue b/apps/desktop/src/components/connection/ConnectionDialog.vue index 240d374c3..1700e7f13 100644 --- a/apps/desktop/src/components/connection/ConnectionDialog.vue +++ b/apps/desktop/src/components/connection/ConnectionDialog.vue @@ -426,6 +426,7 @@ const driverProfiles: Record< }, qdrant: { type: "qdrant", port: 6333, user: "", label: "Qdrant", icon: "qdrant" }, milvus: { type: "milvus", port: 19530, user: "root", label: "Milvus", icon: "milvus" }, + weaviate: { type: "weaviate", port: 8080, user: "", label: "Weaviate", icon: "weaviate" }, mariadb: { type: "mysql", port: 3306, user: "root", label: "MariaDB", icon: "mariadb" }, tidb: { type: "mysql", port: 4000, user: "root", label: "TiDB", icon: "tidb" }, oceanbase: { type: "mysql", port: 2881, user: "root", label: "OceanBase", icon: "oceanbase" }, @@ -1006,6 +1007,7 @@ const iconTypeMap: Record = { elasticsearch: "elasticsearch", qdrant: "qdrant", milvus: "milvus", + weaviate: "weaviate", mariadb: "mariadb", tidb: "tidb", oceanbase: "oceanbase", @@ -1073,6 +1075,7 @@ const dbOptions: DbOption[] = [ { value: "elasticsearch", label: "Elasticsearch" }, { value: "qdrant", label: "Qdrant" }, { value: "milvus", label: "Milvus" }, + { value: "weaviate", label: "Weaviate" }, { value: "dm", label: "DM (Dameng)" }, { value: "opengauss", label: "openGauss" }, { value: "turso", label: "Turso" }, @@ -1178,7 +1181,7 @@ const sqliteExtensionPaths = computed({ form.value.url_params = setSqliteExtensionPaths(form.value.url_params, value); }, }); -const tlsCapableDatabaseTypes = new Set(["mysql", "postgres", "redshift", "gaussdb", "kwdb", "opengauss", "questdb", "redis", "etcd", "clickhouse", "elasticsearch", "qdrant", "milvus", "influxdb"]); +const tlsCapableDatabaseTypes = new Set(["mysql", "postgres", "redshift", "gaussdb", "kwdb", "opengauss", "questdb", "redis", "etcd", "clickhouse", "elasticsearch", "qdrant", "milvus", "weaviate", "influxdb"]); const supportsTlsToggle = computed(() => tlsCapableDatabaseTypes.has(form.value.db_type)); const supportsCaCertificatePath = computed(() => form.value.db_type === "clickhouse"); const supportsGenericUrlParams = computed(() => form.value.db_type !== "manticoresearch"); diff --git a/apps/desktop/src/components/diff/DataCompareDialog.vue b/apps/desktop/src/components/diff/DataCompareDialog.vue index ee236da7e..ff9b7f0e4 100644 --- a/apps/desktop/src/components/diff/DataCompareDialog.vue +++ b/apps/desktop/src/components/diff/DataCompareDialog.vue @@ -114,7 +114,7 @@ const showModified = ref(true); let syncPlanRequestId = 0; -const sqlConnections = computed(() => store.connections.filter((connection) => !["redis", "mongodb", "elasticsearch", "qdrant", "milvus", "etcd", "mq", "nacos"].includes(connection.db_type))); +const sqlConnections = computed(() => store.connections.filter((connection) => !["redis", "mongodb", "elasticsearch", "qdrant", "milvus", "weaviate", "etcd", "mq", "nacos"].includes(connection.db_type))); const selectedSourceTableNames = computed(() => sourceTables.value.filter((table) => selectedSourceTables.value.has(table))); const isBatchCompare = computed(() => selectedSourceTableNames.value.length > 1); const filteredSourceTables = computed(() => { diff --git a/apps/desktop/src/components/export/DatabaseExportDialog.vue b/apps/desktop/src/components/export/DatabaseExportDialog.vue index b544236ab..b32aaa40c 100644 --- a/apps/desktop/src/components/export/DatabaseExportDialog.vue +++ b/apps/desktop/src/components/export/DatabaseExportDialog.vue @@ -65,7 +65,7 @@ const exportCancelled = ref(false); const pendingPrefillTable = ref(""); const pendingPrefillTables = ref([]); -const sqlConnections = computed(() => store.connections.filter((c) => !["redis", "mongodb", "elasticsearch", "qdrant", "milvus", "etcd", "mq", "nacos"].includes(c.db_type))); +const sqlConnections = computed(() => store.connections.filter((c) => !["redis", "mongodb", "elasticsearch", "qdrant", "milvus", "weaviate", "etcd", "mq", "nacos"].includes(c.db_type))); const canExport = computed(() => connectionId.value && database.value && schema.value && !loadingTables.value && !tableError.value && (tables.value.length === 0 || selectedTables.value.length > 0) && (includeStructure.value || includeData.value || includeObjects.value) && !isExporting.value); diff --git a/apps/desktop/src/components/icons/DatabaseIcon.vue b/apps/desktop/src/components/icons/DatabaseIcon.vue index e58c038c4..7545ebbfe 100644 --- a/apps/desktop/src/components/icons/DatabaseIcon.vue +++ b/apps/desktop/src/components/icons/DatabaseIcon.vue @@ -79,6 +79,7 @@ const assetIcons: Record = { etcd: "etcd", qdrant: "qdrant", milvus: "milvus.png", + weaviate: "weaviate.png", mq: "pulsar", pulsar: "pulsar", nacos: "nacos.png", diff --git a/apps/desktop/src/components/layout/EditorToolbar.vue b/apps/desktop/src/components/layout/EditorToolbar.vue index d9c109912..a1bf62e5d 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 !== "qdrant" && dbType !== "milvus" && dbType !== "etcd" && dbType !== "mq" && dbType !== "nacos"; + return dbType !== "redis" && dbType !== "mongodb" && dbType !== "elasticsearch" && dbType !== "qdrant" && dbType !== "milvus" && dbType !== "weaviate" && dbType !== "etcd" && dbType !== "mq" && dbType !== "nacos"; }); const isSingleDb = computed(() => isSingleDatabase(props.activeConnection?.db_type)); const hasDefaultDatabaseOption = computed(() => activeDatabaseOptions.value.includes("")); @@ -251,7 +251,7 @@ function connectionById(connectionId: string): ConnectionConfig | undefined { -
+
{ 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 !== "qdrant" && dbType !== "milvus" && dbType !== "etcd" && dbType !== "mq" && dbType !== "nacos"; + return dbType !== "elasticsearch" && dbType !== "qdrant" && dbType !== "milvus" && dbType !== "weaviate" && dbType !== "etcd" && dbType !== "mq" && dbType !== "nacos"; }); const canCopyFinalProxyPort = computed(() => { if (props.node.type !== "connection" || !props.node.connectionId) return false; diff --git a/apps/desktop/src/components/sql-file/SqlFileExecutionDialog.vue b/apps/desktop/src/components/sql-file/SqlFileExecutionDialog.vue index ab64bc5b6..1eefb9d0f 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", "qdrant", "milvus", "etcd", "mq", "nacos"].includes(c.db_type))); +const sqlConnections = computed(() => store.connections.filter((c) => !["redis", "mongodb", "elasticsearch", "qdrant", "milvus", "weaviate", "etcd", "mq", "nacos"].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 index 8a8a6daa8..b773b0343 100644 --- a/apps/desktop/src/components/vector/VectorBrowser.vue +++ b/apps/desktop/src/components/vector/VectorBrowser.vue @@ -32,7 +32,7 @@ const operationMode = ref("browse"); const requestText = ref(defaultRequestText(props.databaseType, props.database, props.collection, operationMode.value)); let loadingTimer: ReturnType | undefined; -const productLabel = computed(() => (props.databaseType === "milvus" ? "Milvus" : "Qdrant")); +const productLabel = computed(() => (props.databaseType === "milvus" ? "Milvus" : props.databaseType === "weaviate" ? "Weaviate" : "Qdrant")); const collectionLabel = computed(() => props.collection || t("vector.collectionFallback")); const executeLabel = computed(() => (operationMode.value === "browse" ? t("vector.run") : t("vector.apply"))); const operationIcon = computed(() => (operationMode.value === "delete" ? Trash2 : operationMode.value === "upsert" ? Save : Play)); @@ -94,6 +94,26 @@ function defaultRequestText(databaseType: DatabaseType | undefined, database: st const endpoint = mode === "delete" ? "delete" : mode === "upsert" ? "upsert" : "query"; return `POST /v2/vectordb/entities/${endpoint}\n${JSON.stringify(body, null, 2)}`; } + if (databaseType === "weaviate") { + const collectionName = collection || "Collection"; + if (mode === "delete") { + return "DELETE /v1/objects/{id}"; + } + if (mode === "upsert") { + return `POST /v1/objects\n${JSON.stringify( + { + class: collectionName, + properties: { + title: "updated vector", + kind: "demo", + }, + }, + null, + 2, + )}`; + } + return `GET /v1/objects?class=${encodeURIComponent(collectionName)}&limit=100`; + } const collectionPath = pathSegment(collection); if (mode === "delete") { return `POST /collections/${collectionPath}/points/delete?wait=true\n${JSON.stringify({ points: [1] }, null, 2)}`; diff --git a/apps/desktop/src/composables/useNavigationTargets.ts b/apps/desktop/src/composables/useNavigationTargets.ts index d3d7936ae..a3ee5c6a4 100644 --- a/apps/desktop/src/composables/useNavigationTargets.ts +++ b/apps/desktop/src/composables/useNavigationTargets.ts @@ -25,7 +25,7 @@ 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") { + if (config?.db_type === "qdrant" || config?.db_type === "milvus" || config?.db_type === "weaviate") { await connectionStore.ensureConnected(target.connectionId); const tabId = queryStore.createTab(target.connectionId, target.database || "default", tabTitle, "vector"); queryStore.updateSql(tabId, target.tableName); diff --git a/apps/desktop/src/lib/__tests__/sqlStatementRanges.spec.ts b/apps/desktop/src/lib/__tests__/sqlStatementRanges.spec.ts index 7c0e404f1..91b7e74aa 100644 --- a/apps/desktop/src/lib/__tests__/sqlStatementRanges.spec.ts +++ b/apps/desktop/src/lib/__tests__/sqlStatementRanges.spec.ts @@ -330,6 +330,7 @@ describe("supportsExecutionTargetPicker", () => { expect(supportsExecutionTargetPicker("elasticsearch")).toBe(false); expect(supportsExecutionTargetPicker("qdrant")).toBe(false); expect(supportsExecutionTargetPicker("milvus")).toBe(false); + expect(supportsExecutionTargetPicker("weaviate")).toBe(false); expect(supportsExecutionTargetPicker("etcd")).toBe(false); expect(supportsExecutionTargetPicker("mq")).toBe(false); expect(supportsExecutionTargetPicker("neo4j")).toBe(false); diff --git a/apps/desktop/src/lib/connectionPresentation.ts b/apps/desktop/src/lib/connectionPresentation.ts index 068251fe3..0a6c0b360 100644 --- a/apps/desktop/src/lib/connectionPresentation.ts +++ b/apps/desktop/src/lib/connectionPresentation.ts @@ -129,6 +129,7 @@ export function connectionUrlPlaceholder(dbType: DatabaseType): string { case "elasticsearch": case "qdrant": case "milvus": + case "weaviate": 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 f76b5d437..94ce054fe 100644 --- a/apps/desktop/src/lib/connectionUrl.ts +++ b/apps/desktop/src/lib/connectionUrl.ts @@ -41,6 +41,7 @@ const SCHEME_PROFILES: Record = { 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 }, + weaviate: { type: "weaviate", profile: "weaviate", label: "Weaviate", defaultPort: 8080 }, 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 }, @@ -63,6 +64,7 @@ const HTTP_SELECTED_PROFILES: Record = { elasticsearch: SCHEME_PROFILES.elasticsearch, qdrant: SCHEME_PROFILES.qdrant, milvus: SCHEME_PROFILES.milvus, + weaviate: SCHEME_PROFILES.weaviate, }; function decodeUrlPart(value: string): string { diff --git a/apps/desktop/src/lib/connectionVisibleDatabases.ts b/apps/desktop/src/lib/connectionVisibleDatabases.ts index da7a6d684..6f53e373e 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", "qdrant", "milvus", "etcd"]); +const UNSUPPORTED_VISIBLE_DATABASE_TYPES = new Set(["elasticsearch", "qdrant", "milvus", "weaviate", "etcd"]); type VisibleDatabaseConnectionFields = Pick< ConnectionConfig, diff --git a/apps/desktop/src/lib/sidebarNodeOrdering.ts b/apps/desktop/src/lib/sidebarNodeOrdering.ts index 8cb54425b..491d0d5fe 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" || databaseType === "qdrant" || databaseType === "milvus") { + if (databaseType === "mongodb" || databaseType === "elasticsearch" || databaseType === "qdrant" || databaseType === "milvus" || databaseType === "weaviate") { return withConnectionUtilityOrder(sortByLabel(regularChildren)); } diff --git a/apps/desktop/src/lib/sqlSemanticDiagnostics.ts b/apps/desktop/src/lib/sqlSemanticDiagnostics.ts index 8ce38b1ca..e83821bb4 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 === "qdrant" || options.databaseType === "milvus" || options.databaseType === "redis") return false; + if (options.databaseType === "mongodb" || options.databaseType === "elasticsearch" || options.databaseType === "qdrant" || options.databaseType === "milvus" || options.databaseType === "weaviate" || 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/sqlStatementRanges.ts b/apps/desktop/src/lib/sqlStatementRanges.ts index a4c69afeb..b886f69de 100644 --- a/apps/desktop/src/lib/sqlStatementRanges.ts +++ b/apps/desktop/src/lib/sqlStatementRanges.ts @@ -11,7 +11,7 @@ export interface SqlTextRange { sql: string; } -const NON_SQL_EXECUTION_TARGET_TYPES: ReadonlySet = new Set(["mongodb", "elasticsearch", "qdrant", "milvus", "etcd", "mq", "neo4j"]); +const NON_SQL_EXECUTION_TARGET_TYPES: ReadonlySet = new Set(["mongodb", "elasticsearch", "qdrant", "milvus", "weaviate", "etcd", "mq", "neo4j"]); export function supportsExecutionTargetPicker(databaseType?: DatabaseType): boolean { return !!databaseType && (databaseType === "redis" || !NON_SQL_EXECUTION_TARGET_TYPES.has(databaseType)); @@ -88,6 +88,7 @@ const DATABASE_SOFT_STATEMENT_KEYWORDS: Partial { elasticsearch: "Elasticsearch", qdrant: "Qdrant", milvus: "Milvus", + weaviate: "Weaviate", doris: "Doris", starrocks: "StarRocks", manticoresearch: "Manticore Search", @@ -950,7 +951,7 @@ 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") { + } else if (config.db_type === "qdrant" || config.db_type === "milvus" || config.db_type === "weaviate") { await loadVectorCollections(connectionId); } else if (config.db_type === "mq") { await loadMqTenants(connectionId, { force: true }); @@ -2115,7 +2116,7 @@ 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") { + } else if (config?.db_type === "qdrant" || config?.db_type === "milvus" || config?.db_type === "weaviate") { await loadVectorCollections(node.connectionId); } else if (config?.db_type === "mq") { await loadMqTenants(node.connectionId, options); diff --git a/apps/desktop/src/types/database.ts b/apps/desktop/src/types/database.ts index 95b2f3ca7..0d35b5c6d 100644 --- a/apps/desktop/src/types/database.ts +++ b/apps/desktop/src/types/database.ts @@ -13,6 +13,7 @@ export type DatabaseType = | "elasticsearch" | "qdrant" | "milvus" + | "weaviate" | "doris" | "starrocks" | "manticoresearch" diff --git a/crates/dbx-core/assets/database-drivers.manifest.json b/crates/dbx-core/assets/database-drivers.manifest.json index ff30162b8..c0d39115f 100644 --- a/crates/dbx-core/assets/database-drivers.manifest.json +++ b/crates/dbx-core/assets/database-drivers.manifest.json @@ -378,6 +378,35 @@ "driverManagement": false } }, + { + "dbType": "weaviate", + "label": "Weaviate", + "runtimeMode": "native", + "mcpMode": "bridge", + "singleConnectionPool": false, + "metadataConnectionScoped": false, + "skipTcpProbe": false, + "defaultPort": 8080, + "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 069c1fbf2..054e9f44d 100644 --- a/crates/dbx-core/src/connection.rs +++ b/crates/dbx-core/src/connection.rs @@ -831,10 +831,11 @@ impl AppState { db::elasticsearch_driver::test_connection(&mut client, connect_timeout).await?; PoolKind::Elasticsearch(client) } - DatabaseType::Qdrant | DatabaseType::Milvus => { + DatabaseType::Qdrant | DatabaseType::Milvus | DatabaseType::Weaviate => { let kind = match db_config.db_type { DatabaseType::Qdrant => db::vector_driver::VectorDbKind::Qdrant, DatabaseType::Milvus => db::vector_driver::VectorDbKind::Milvus, + DatabaseType::Weaviate => db::vector_driver::VectorDbKind::Weaviate, _ => unreachable!(), }; let client = db::vector_driver::VectorClient::new( @@ -2124,7 +2125,10 @@ fn base_pool_key_for( 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 - && matches!(db_type, DatabaseType::Elasticsearch | DatabaseType::Qdrant | DatabaseType::Milvus)); + && matches!( + db_type, + DatabaseType::Elasticsearch | DatabaseType::Qdrant | DatabaseType::Milvus | DatabaseType::Weaviate + )); is_single && (!database_capabilities::is_agent_type(db_type) || shares_database_pool_with_connection(db_type)) }); diff --git a/crates/dbx-core/src/db/vector_driver.rs b/crates/dbx-core/src/db/vector_driver.rs index 8565f0a76..a74d404a0 100644 --- a/crates/dbx-core/src/db/vector_driver.rs +++ b/crates/dbx-core/src/db/vector_driver.rs @@ -25,10 +25,13 @@ const PATH_SEGMENT_ENCODE_SET: &AsciiSet = &CONTROLS .add(b'|') .add(b'}'); +const QUERY_VALUE_ENCODE_SET: &AsciiSet = &PATH_SEGMENT_ENCODE_SET.add(b'&').add(b'=').add(b'+'); + #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum VectorDbKind { Qdrant, Milvus, + Weaviate, } impl VectorDbKind { @@ -36,6 +39,7 @@ impl VectorDbKind { match self { VectorDbKind::Qdrant => "Qdrant", VectorDbKind::Milvus => "Milvus", + VectorDbKind::Weaviate => "Weaviate", } } } @@ -48,7 +52,7 @@ pub struct VectorClient { auth: Option, } -#[derive(Clone)] +#[derive(Clone, Debug, PartialEq, Eq)] enum VectorAuth { Basic(String, String), Bearer(String), @@ -108,6 +112,8 @@ fn vector_auth(kind: VectorDbKind, username: Option<&str>, password: Option<&str VectorDbKind::Qdrant => None, VectorDbKind::Milvus if !username.is_empty() => Some(VectorAuth::Bearer(format!("{username}:{password}"))), VectorDbKind::Milvus => None, + VectorDbKind::Weaviate if !password.is_empty() => Some(VectorAuth::Bearer(password.to_string())), + VectorDbKind::Weaviate => None, } } @@ -116,10 +122,12 @@ pub async fn test_connection(client: &VectorClient, timeout: Duration) -> Result let path = match client.kind { VectorDbKind::Qdrant => "/collections", VectorDbKind::Milvus => "/v2/vectordb/collections/list", + VectorDbKind::Weaviate => "/v1/meta", }; let request = match client.kind { VectorDbKind::Qdrant => client.get(path), VectorDbKind::Milvus => client.post(path).json(&serde_json::json!({ "dbName": "default" })), + VectorDbKind::Weaviate => client.get(path), }; let resp = with_connection_timeout(label, timeout, async { request.send().await.map_err(|e| format!("{label} connection failed: {}", format_reqwest_error(&e))) @@ -132,6 +140,7 @@ pub async fn list_collections(client: &VectorClient) -> Result, Stri match client.kind { VectorDbKind::Qdrant => list_qdrant_collections(client).await, VectorDbKind::Milvus => list_milvus_collections(client).await, + VectorDbKind::Weaviate => list_weaviate_collections(client).await, } } @@ -169,6 +178,22 @@ fn collection_name_from_milvus_item(item: &Value) -> Option { .or_else(|| item.get("name").and_then(Value::as_str).map(str::to_string)) } +async fn list_weaviate_collections(client: &VectorClient) -> Result, String> { + let body = send_json(client.get("/v1/schema"), "Weaviate").await?; + let mut names = weaviate_collection_names_from_schema(&body); + names.sort(); + Ok(names) +} + +fn weaviate_collection_names_from_schema(body: &Value) -> Vec { + body.get("classes") + .and_then(Value::as_array) + .into_iter() + .flatten() + .filter_map(|item| item.get("class").and_then(Value::as_str).map(str::to_string)) + .collect() +} + pub async fn find_documents( client: &VectorClient, collection: &str, @@ -197,6 +222,9 @@ pub async fn find_documents( "outputFields": ["*"], }) ), + VectorDbKind::Weaviate => { + format!("GET /v1/objects?class={}&limit={}&offset={}", query_value(collection), limit.max(1), skip) + } }; let result = execute_rest_query(client, &query).await?; let documents = result @@ -272,6 +300,7 @@ fn default_collection_query(client: &VectorClient, collection: &str) -> Result Ok(client.get(&format!("/v1/objects?class={}&limit=100", query_value(collection)))), } } @@ -283,6 +312,10 @@ fn path_segment(value: &str) -> String { utf8_percent_encode(value, PATH_SEGMENT_ENCODE_SET).to_string() } +fn query_value(value: &str) -> String { + utf8_percent_encode(value, QUERY_VALUE_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?; @@ -304,6 +337,9 @@ fn json_to_query_result(status: u16, body: Value, start: Instant) -> QueryResult if let Some(Value::Array(items)) = rows_value { return values_to_query_result(items, start); } + if let Some(Value::Array(items)) = body.get("objects").cloned() { + 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); } @@ -361,6 +397,11 @@ fn normalize_row_object(value: Value) -> Map { object.entry(key).or_insert(value); } } + if let Some(Value::Object(properties)) = object.remove("properties") { + for (key, value) in properties { + object.entry(key).or_insert(value); + } + } object } other => { @@ -386,7 +427,10 @@ fn format_reqwest_error(err: &reqwest::Error) -> String { #[cfg(test)] mod tests { - use super::{starts_with_http_method, values_to_query_result}; + use super::{ + starts_with_http_method, values_to_query_result, vector_auth, weaviate_collection_names_from_schema, + VectorAuth, VectorDbKind, + }; use serde_json::json; use std::time::Instant; @@ -405,4 +449,34 @@ mod tests { assert!(result.columns.contains(&"score".to_string())); assert!(result.columns.contains(&"title".to_string())); } + + #[test] + fn extracts_weaviate_schema_class_names() { + let names = weaviate_collection_names_from_schema(&json!({ + "classes": [ + { "class": "Article" }, + { "class": "Product" } + ] + })); + assert_eq!(names, vec!["Article".to_string(), "Product".to_string()]); + } + + #[test] + fn flattens_weaviate_properties_columns() { + let result = values_to_query_result( + vec![json!({"id": "abc", "class": "Article", "properties": {"title": "hello"}})], + Instant::now(), + ); + assert!(result.columns.contains(&"id".to_string())); + assert!(result.columns.contains(&"class".to_string())); + assert!(result.columns.contains(&"title".to_string())); + } + + #[test] + fn uses_bearer_auth_for_weaviate_tokens_even_with_username() { + assert_eq!( + vector_auth(VectorDbKind::Weaviate, Some("user"), Some("token")), + Some(VectorAuth::Bearer("token".to_string())) + ); + } } diff --git a/crates/dbx-core/src/models/connection.rs b/crates/dbx-core/src/models/connection.rs index 34f4dfa52..dec9e7f84 100644 --- a/crates/dbx-core/src/models/connection.rs +++ b/crates/dbx-core/src/models/connection.rs @@ -258,6 +258,8 @@ pub enum DatabaseType { Qdrant, #[serde(rename = "milvus")] Milvus, + #[serde(rename = "weaviate")] + Weaviate, Doris, #[serde(rename = "starrocks")] StarRocks, @@ -733,7 +735,7 @@ impl ConnectionConfig { format!("mongodb://{host}:{port}{db_part}{suffix}") } DatabaseType::Oracle => format!("oracle://{host}:{port}{db_part}"), - DatabaseType::Elasticsearch | DatabaseType::Qdrant | DatabaseType::Milvus => { + DatabaseType::Elasticsearch | DatabaseType::Qdrant | DatabaseType::Milvus | DatabaseType::Weaviate => { let scheme = if self.ssl { "https" } else { "http" }; format!("{scheme}://{host}:{port}") } @@ -869,7 +871,7 @@ impl ConnectionConfig { DatabaseType::Oracle => { format!("oracle://{}:{}@{host}:{port}{db_part}", username, password) } - DatabaseType::Elasticsearch | DatabaseType::Qdrant | DatabaseType::Milvus => { + DatabaseType::Elasticsearch | DatabaseType::Qdrant | DatabaseType::Milvus | DatabaseType::Weaviate => { let scheme = if self.ssl { "https" } else { "http" }; format!("{scheme}://{host}:{port}") } diff --git a/crates/dbx-core/src/query.rs b/crates/dbx-core/src/query.rs index 694bfa1c9..189c5ae78 100644 --- a/crates/dbx-core/src/query.rs +++ b/crates/dbx-core/src/query.rs @@ -733,6 +733,7 @@ fn should_discard_pool_after_query_timeout(db_type: Option) -> boo | DatabaseType::Elasticsearch | DatabaseType::Qdrant | DatabaseType::Milvus + | DatabaseType::Weaviate | DatabaseType::InfluxDb ) } diff --git a/crates/dbx-core/src/query_execution_sql.rs b/crates/dbx-core/src/query_execution_sql.rs index 22dfa6e7f..cca7a6d14 100644 --- a/crates/dbx-core/src/query_execution_sql.rs +++ b/crates/dbx-core/src/query_execution_sql.rs @@ -89,6 +89,7 @@ pub fn supports_sql_query(database_type: DatabaseType) -> bool { | DatabaseType::Elasticsearch | DatabaseType::Qdrant | DatabaseType::Milvus + | DatabaseType::Weaviate | DatabaseType::InfluxDb | DatabaseType::Neo4j | DatabaseType::Etcd diff --git a/packages/mcp-server/src/index.ts b/packages/mcp-server/src/index.ts index 999e15968..86b75b8a0 100644 --- a/packages/mcp-server/src/index.ts +++ b/packages/mcp-server/src/index.ts @@ -46,7 +46,7 @@ function formatQueryToolResult(result: QueryResult, title?: string) { } export const DBX_CONNECTION_TYPE_DESCRIPTION = - "Database type: postgres, mysql, sqlite, rqlite, redis, duckdb, clickhouse, sqlserver, mongodb, oracle, elasticsearch, etcd, doris, starrocks, manticoresearch, milvus, qdrant, redshift, dameng, kingbase, highgo, vastbase, goldendb, databend, gaussdb, kwdb, yashandb, databricks, saphana, teradata, vertica, firebird, exasol, opengauss, oceanbase-oracle, questdb, gbase, h2, snowflake, trino, prestosql, hive, db2, informix, influxdb, iris, neo4j, cassandra, bigquery, kylin, sundb, tdengine, iotdb, xugu, jdbc, access, mq"; + "Database type: postgres, mysql, sqlite, rqlite, redis, duckdb, clickhouse, sqlserver, mongodb, oracle, elasticsearch, etcd, doris, starrocks, manticoresearch, milvus, qdrant, weaviate, redshift, dameng, kingbase, highgo, vastbase, goldendb, databend, gaussdb, kwdb, yashandb, databricks, saphana, teradata, vertica, firebird, exasol, opengauss, oceanbase-oracle, questdb, gbase, h2, snowflake, trino, prestosql, hive, db2, informix, influxdb, iris, neo4j, cassandra, bigquery, kylin, sundb, tdengine, iotdb, xugu, jdbc, access, mq"; const FILE_CAPABLE_CONNECTION_TYPES = new Set(["sqlite", "duckdb", "access", "h2"]); interface McpScope { diff --git a/packages/node-core/src/diagnostics.ts b/packages/node-core/src/diagnostics.ts index 5e4815c62..4c225f543 100644 --- a/packages/node-core/src/diagnostics.ts +++ b/packages/node-core/src/diagnostics.ts @@ -22,6 +22,7 @@ export const BRIDGE_REQUIRED_TYPES = [ "elasticsearch", "qdrant", "milvus", + "weaviate", "etcd", "dameng", "kingbase", diff --git a/src-tauri/src/commands/connection.rs b/src-tauri/src/commands/connection.rs index 9bcd9ebfc..a76eb1192 100644 --- a/src-tauri/src/commands/connection.rs +++ b/src-tauri/src/commands/connection.rs @@ -712,10 +712,11 @@ pub async fn test_connection(state: State<'_, Arc>, config: Connection .await .map(|_| "Connection successful".to_string()) } - DatabaseType::Qdrant | DatabaseType::Milvus => { + DatabaseType::Qdrant | DatabaseType::Milvus | DatabaseType::Weaviate => { let kind = match config.db_type { DatabaseType::Qdrant => db::vector_driver::VectorDbKind::Qdrant, DatabaseType::Milvus => db::vector_driver::VectorDbKind::Milvus, + DatabaseType::Weaviate => db::vector_driver::VectorDbKind::Weaviate, _ => unreachable!(), }; let client = db::vector_driver::VectorClient::new( @@ -997,10 +998,11 @@ pub async fn connect_db(state: State<'_, Arc>, config: ConnectionConfi db::elasticsearch_driver::test_connection(&mut client, connect_timeout).await?; PoolKind::Elasticsearch(client) } - DatabaseType::Qdrant | DatabaseType::Milvus => { + DatabaseType::Qdrant | DatabaseType::Milvus | DatabaseType::Weaviate => { let kind = match db_config.db_type { DatabaseType::Qdrant => db::vector_driver::VectorDbKind::Qdrant, DatabaseType::Milvus => db::vector_driver::VectorDbKind::Milvus, + DatabaseType::Weaviate => db::vector_driver::VectorDbKind::Weaviate, _ => unreachable!(), }; let client = db::vector_driver::VectorClient::new(