From 3687a9cbfb307d03875be70c33ed9f4d2bfe1a66 Mon Sep 17 00:00:00 2001 From: Terry Yao <69723+yaoxinghuo@users.noreply.github.com> Date: Mon, 21 Sep 2026 18:07:44 +0800 Subject: [PATCH] feat(solr): add Apache Solr connection support --- apps/desktop/public/icons/database/solr.svg | 1 + apps/desktop/src/App.vue | 4 +- .../connection/ConnectionDialog.vue | 10 +- .../src/components/diff/DataCompareDialog.vue | 2 +- .../components/diff/SchemaDiffConfigStep.vue | 2 +- .../components/document/DocumentBrowser.vue | 64 +- .../src/components/editor/QueryEditor.vue | 10 +- .../export/DatabaseExportDialog.vue | 2 +- .../src/components/icons/DatabaseIcon.vue | 9 +- .../src/components/layout/ContentArea.vue | 9 +- .../src/components/layout/EditorToolbar.vue | 2 + .../src/components/sidebar/ConnectionTree.vue | 2 +- .../sidebar/SidebarTreeRuntimeHost.vue | 14 +- .../desktop/src/components/solr/SolrAdmin.vue | 1394 +++++++++++++++++ .../sql-file/SqlFileExecutionDialog.vue | 2 +- apps/desktop/src/composables/useQuickOpen.ts | 2 +- .../src/composables/useSqlExecution.ts | 7 +- apps/desktop/src/i18n/locales/en.ts | 148 ++ apps/desktop/src/i18n/locales/es.ts | 148 ++ apps/desktop/src/i18n/locales/it.ts | 148 ++ apps/desktop/src/i18n/locales/ja.ts | 148 ++ apps/desktop/src/i18n/locales/ko.ts | 148 ++ apps/desktop/src/i18n/locales/pt-BR.ts | 148 ++ apps/desktop/src/i18n/locales/zh-CN.ts | 148 ++ apps/desktop/src/i18n/locales/zh-TW.ts | 148 ++ .../src/lib/__tests__/ai/aiSolr.spec.ts | 103 ++ .../app/documentStoreProvider.solr.spec.ts | 23 + .../connection/connectionUrl.solr.spec.ts | 80 + .../spannerConnectionDialog.spec.ts | 2 +- .../lib/__tests__/sql/sqlRisk.solr.spec.ts | 83 + apps/desktop/src/lib/ai/ai.ts | 79 + .../desktop/src/lib/app/documentJsonValues.ts | 3 + .../src/lib/app/documentStoreProvider.ts | 31 +- apps/desktop/src/lib/app/documentStoreSave.ts | 7 + .../lib/connection/connectionPresentation.ts | 4 + .../src/lib/connection/connectionUrl.ts | 4 +- .../lib/connection/connectionUrlBuilder.ts | 2 +- .../connection/connectionVisibleDatabases.ts | 2 +- .../lib/dataGrid/dataGridColumnNameCopy.ts | 2 +- .../src/lib/dataGrid/dataGridColumnType.ts | 17 +- .../src/lib/dataGrid/dataGridSaveUi.ts | 2 +- .../lib/database/databaseNamespaceCreation.ts | 1 + .../lib/database/databasePropertyEditing.ts | 1 + .../database/sqlExecutionTargetRegistry.ts | 1 + .../lib/editor/codemirrorInsertValueHints.ts | 2 +- .../elasticsearchJsonResponse.ts | 9 +- .../src/lib/sidebar/sidebarNodeOrdering.ts | 2 +- apps/desktop/src/lib/solr/solrAdmin.ts | 572 +++++++ apps/desktop/src/lib/solr/solrRequestRisk.ts | 173 ++ .../src/lib/sql/semantic/diagnostics.ts | 2 + apps/desktop/src/lib/sql/sqlRisk.ts | 15 +- .../desktop/src/lib/sql/sqlStatementRanges.ts | 7 +- .../lib/table/tableMetadataCapabilities.ts | 6 + apps/desktop/src/stores/connectionStore.ts | 12 +- apps/desktop/src/stores/queryStore.ts | 24 + apps/desktop/src/types/database.ts | 5 + .../src/types/generated/connectionProfiles.ts | 3 + .../src/types/generated/databaseTypes.ts | 1 + .../assets/database-drivers.manifest.json | 29 + crates/dbx-core/src/ai/agent_tools.rs | 85 + crates/dbx-core/src/connection/mod.rs | 46 + crates/dbx-core/src/query/document_ops.rs | 43 +- crates/dbx-core/src/query/mod.rs | 21 + .../dbx-core/src/schema/providers/native.rs | 4 + crates/dbx-drivers/src/db/mod.rs | 1 + crates/dbx-drivers/src/db/solr_driver.rs | 1143 ++++++++++++++ crates/dbx-mcp/src/backend.rs | 23 + crates/dbx-mcp/src/server.rs | 14 + crates/dbx-sql/src/query_execution_sql.rs | 191 +++ crates/dbx-sql/src/query_result_sql.rs | 5 +- crates/dbx-sql/src/sql_dialect/ddl_profile.rs | 2 +- crates/dbx-types/src/models/connection.rs | 2 + .../connection-types/profiles/catalog.yaml | 7 + plugins/connection-types/solr.yaml | 29 + src-tauri/src/commands/connection.rs | 33 + 75 files changed, 5596 insertions(+), 82 deletions(-) create mode 100644 apps/desktop/public/icons/database/solr.svg create mode 100644 apps/desktop/src/components/solr/SolrAdmin.vue create mode 100644 apps/desktop/src/lib/__tests__/ai/aiSolr.spec.ts create mode 100644 apps/desktop/src/lib/__tests__/app/documentStoreProvider.solr.spec.ts create mode 100644 apps/desktop/src/lib/__tests__/connection/connectionUrl.solr.spec.ts create mode 100644 apps/desktop/src/lib/__tests__/sql/sqlRisk.solr.spec.ts create mode 100644 apps/desktop/src/lib/solr/solrAdmin.ts create mode 100644 apps/desktop/src/lib/solr/solrRequestRisk.ts create mode 100644 crates/dbx-drivers/src/db/solr_driver.rs create mode 100644 plugins/connection-types/solr.yaml diff --git a/apps/desktop/public/icons/database/solr.svg b/apps/desktop/public/icons/database/solr.svg new file mode 100644 index 0000000000..b4c1bb7f12 --- /dev/null +++ b/apps/desktop/public/icons/database/solr.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/apps/desktop/src/App.vue b/apps/desktop/src/App.vue index 0dbc6329bc..6c18902f71 100644 --- a/apps/desktop/src/App.vue +++ b/apps/desktop/src/App.vue @@ -3207,7 +3207,7 @@ async function handleQuickOpenSelect(item: any) { await connectionStore.loadConsulRoot(item.connectionId); } else if (config?.db_type === "mongodb") { await connectionStore.loadMongoDatabases(item.connectionId); - } else if (config?.db_type === "elasticsearch" || config?.db_type === "easysearch" || config?.db_type === "meilisearch") { + } else if (config?.db_type === "elasticsearch" || config?.db_type === "easysearch" || config?.db_type === "meilisearch" || config?.db_type === "solr") { await connectionStore.openElasticsearchConnectionTree(item.connectionId); } else if (config?.db_type === "qdrant" || config?.db_type === "milvus" || config?.db_type === "weaviate" || config?.db_type === "chromadb") { await connectionStore.loadVectorCollections(item.connectionId); @@ -3234,7 +3234,7 @@ async function handleQuickOpenSelect(item: any) { await connectionStore.loadConsulRoot(item.connectionId); } else if (config?.db_type === "mongodb") { await connectionStore.loadMongoDatabases(item.connectionId); - } else if (config?.db_type === "elasticsearch" || config?.db_type === "easysearch" || config?.db_type === "meilisearch") { + } else if (config?.db_type === "elasticsearch" || config?.db_type === "easysearch" || config?.db_type === "meilisearch" || config?.db_type === "solr") { await connectionStore.openElasticsearchConnectionTree(item.connectionId); } else if (config?.db_type === "qdrant" || config?.db_type === "milvus" || config?.db_type === "weaviate" || config?.db_type === "chromadb") { await connectionStore.loadVectorCollections(item.connectionId); diff --git a/apps/desktop/src/components/connection/ConnectionDialog.vue b/apps/desktop/src/components/connection/ConnectionDialog.vue index a22e80dc84..9eef79123a 100644 --- a/apps/desktop/src/components/connection/ConnectionDialog.vue +++ b/apps/desktop/src/components/connection/ConnectionDialog.vue @@ -3241,6 +3241,7 @@ const tlsCapableDatabaseTypes = new Set([ "elasticsearch", "easysearch", "meilisearch", + "solr", "hbase", "qdrant", "milvus", @@ -4569,12 +4570,12 @@ function connectionConfigForSubmit(id: string, generatedName = "", validatePlugi if ((config.client_cert_path && !config.client_key_path) || (!config.client_cert_path && config.client_key_path)) { throw new Error(t("connection.etcdClientCertPairRequired")); } - } else if (form.value.db_type !== "consul" && config.db_type !== "elasticsearch" && config.db_type !== "easysearch") { + } else if (form.value.db_type !== "consul" && config.db_type !== "elasticsearch" && config.db_type !== "easysearch" && config.db_type !== "solr") { config.etcd_endpoints = undefined; config.client_cert_path = undefined; config.client_key_path = undefined; } - if (config.db_type === "elasticsearch" || config.db_type === "easysearch") { + if (config.db_type === "elasticsearch" || config.db_type === "easysearch" || config.db_type === "solr") { config.client_cert_path = config.client_cert_path?.trim() || ""; config.client_key_path = config.client_key_path?.trim() || ""; if ((config.client_cert_path && !config.client_key_path) || (!config.client_cert_path && config.client_key_path)) { @@ -4591,7 +4592,8 @@ function connectionConfigForSubmit(id: string, generatedName = "", validatePlugi config.db_type !== "victoriametrics" && config.db_type !== "zookeeper" && config.db_type !== "elasticsearch" && - config.db_type !== "easysearch" + config.db_type !== "easysearch" && + config.db_type !== "solr" ) { config.ca_cert_path = undefined; } else { @@ -8115,7 +8117,7 @@ function openExternalUrl(url: string) { -
+
diff --git a/apps/desktop/src/components/diff/DataCompareDialog.vue b/apps/desktop/src/components/diff/DataCompareDialog.vue index fbc4b3c472..2bd43027e3 100644 --- a/apps/desktop/src/components/diff/DataCompareDialog.vue +++ b/apps/desktop/src/components/diff/DataCompareDialog.vue @@ -114,7 +114,7 @@ let initializingPrefillGeneration = 0; let componentUnmounted = false; let shownSessionError = ""; -const sqlConnections = computed(() => store.connections.filter((connection) => !["redis", "mongodb", "elasticsearch", "easysearch", "meilisearch", "qdrant", "milvus", "weaviate", "chromadb", "etcd", "zookeeper", "consul", "mq", "nacos"].includes(connection.db_type))); +const sqlConnections = computed(() => store.connections.filter((connection) => !["redis", "mongodb", "elasticsearch", "easysearch", "meilisearch", "solr", "qdrant", "milvus", "weaviate", "chromadb", "etcd", "zookeeper", "consul", "mq", "nacos"].includes(connection.db_type))); const selectedSourceTableNames = computed(() => sourceTables.value.filter((table) => selectedSourceTables.value.has(table))); const isBatchCompare = computed(() => selectedSourceTableNames.value.length > 1); // Bridge the shared TableMultiSelect `string[]` v-model with the Set-based selection store. diff --git a/apps/desktop/src/components/diff/SchemaDiffConfigStep.vue b/apps/desktop/src/components/diff/SchemaDiffConfigStep.vue index 67853c6f02..f7590813f5 100644 --- a/apps/desktop/src/components/diff/SchemaDiffConfigStep.vue +++ b/apps/desktop/src/components/diff/SchemaDiffConfigStep.vue @@ -84,7 +84,7 @@ const targetSchemas = ref([]); const sourceDbVersion = ref(null); const targetDbVersion = ref(null); -const sqlConnections = computed(() => store.connections.filter((c: any) => !["mongodb", "redis", "elasticsearch", "easysearch", "meilisearch", "etcd", "zookeeper", "consul", "mq", "nacos"].includes(c.db_type))); +const sqlConnections = computed(() => store.connections.filter((c: any) => !["mongodb", "redis", "elasticsearch", "easysearch", "meilisearch", "solr", "etcd", "zookeeper", "consul", "mq", "nacos"].includes(c.db_type))); const sourceConfig = computed(() => store.getConfig(props.sourceConnectionId)); const targetConfig = computed(() => store.getConfig(props.targetConnectionId)); diff --git a/apps/desktop/src/components/document/DocumentBrowser.vue b/apps/desktop/src/components/document/DocumentBrowser.vue index 9ebcffa22e..574cb647ec 100644 --- a/apps/desktop/src/components/document/DocumentBrowser.vue +++ b/apps/desktop/src/components/document/DocumentBrowser.vue @@ -71,7 +71,7 @@ import { stringifyDocumentStoreValue, documentStoreValueForGrid, } from "@/lib/app/documentJsonValues"; -import { applyDocumentStoreIdentityPlan, formatMeilisearchDocumentOperationPreview, insertDocumentStoreDocument as insertDocumentStoreDocumentCore } from "@/lib/app/documentStoreSave"; +import { applyDocumentStoreIdentityPlan, formatMeilisearchDocumentOperationPreview, formatSolrDocumentOperationPreview, insertDocumentStoreDocument as insertDocumentStoreDocumentCore } from "@/lib/app/documentStoreSave"; import { restoreDocumentBrowserState, saveDocumentBrowserState, type DocumentBrowserDataSnapshot } from "@/lib/tabs/documentBrowserStateCache"; import RedisJsonEditor from "@/components/redis/RedisJsonEditor.vue"; import { isLosslessJsonNumber, parseJsonPreservingLargeNumbers } from "@/lib/common/safeJsonFormat"; @@ -145,7 +145,7 @@ const DYNAMODB_DEFAULT_EXPORT_ROW_LIMIT = 10_000; // stores (DynamoDB/Elasticsearch) cannot resume a page without their cursor // stacks, and infinite scroll always restarts from the first segment. const restoredDocumentBrowserState = props.stateKey ? restoreDocumentBrowserState(props.stateKey) : undefined; -const skipBasedDocumentStore = documentStoreProviderFor(props.databaseType).kind === "mongodb" || documentStoreProviderFor(props.databaseType).kind === "meilisearch"; +const skipBasedDocumentStore = ["mongodb", "meilisearch", "solr"].includes(documentStoreProviderFor(props.databaseType).kind); const restoresSkipBasedPage = !!restoredDocumentBrowserState && !settingsStore.editorSettings.infiniteScroll && skipBasedDocumentStore; const documents = ref([]); @@ -466,6 +466,12 @@ function elasticsearchGridColumnTypesFor(columns: readonly string[]): string[] { }); } const elasticsearchGridColumnTypes = computed(() => elasticsearchGridColumnTypesFor(lastGridColumns.value)); +const solrSchemaFields = ref([]); +const solrFieldTypes = computed(() => new Map(solrSchemaFields.value.map((field) => [field.name, field.data_type]))); +function solrGridColumnTypesFor(columns: readonly string[]): string[] { + return columns.map((column) => (column === "_id" ? "string" : (solrFieldTypes.value.get(column) ?? ""))); +} +const solrGridColumnTypes = computed(() => solrGridColumnTypesFor(lastGridColumns.value)); const dynamodbTableDescription = ref(null); const dynamodbIndexName = ref("__table__"); const dynamodbPageCursors = ref>([undefined]); @@ -536,6 +542,9 @@ const deleteDetails = computed(() => { }); } const displayId = mongoDocumentIdForGrid(id); + if (props.databaseType === "solr") { + return `Solr core: ${props.collection}\nDocument _id: ${String(displayId)}`; + } if (props.databaseType === "elasticsearch" || props.databaseType === "easysearch" || props.databaseType === "meilisearch") { const product = props.databaseType === "easysearch" ? "Easysearch" : props.databaseType === "meilisearch" ? "Meilisearch" : "Elasticsearch"; return `${product} index: ${props.collection}\nDocument _id: ${String(displayId)}`; @@ -608,7 +617,7 @@ function commitLoadedDocuments(nextDocuments: JsonRecord[], nextCopyDocuments: J const gridResult = computed(() => { const docs = documents.value; - const columnTypes = documentStoreProvider.value.kind === "elasticsearch" ? elasticsearchGridColumnTypes.value : lastGridColumnTypes.value; + const columnTypes = documentStoreProvider.value.kind === "elasticsearch" ? elasticsearchGridColumnTypes.value : documentStoreProvider.value.kind === "solr" ? solrGridColumnTypes.value : lastGridColumnTypes.value; if (!docs.length) { return { columns: lastGridColumns.value, @@ -637,7 +646,7 @@ const gridResult = computed(() => { async function exportAllDocumentStoreDocuments(onProgress?: (info: { rowsExported: number; totalRows: number | null }) => void): Promise { const kind = documentStoreProvider.value.kind; - if (kind !== "mongodb" && kind !== "dynamodb" && kind !== "elasticsearch") return undefined; + if (kind !== "mongodb" && kind !== "dynamodb" && kind !== "elasticsearch" && kind !== "solr") return undefined; const connectionId = props.connectionId; const database = props.database; @@ -673,7 +682,7 @@ async function exportAllDocumentStoreDocuments(onProgress?: (info: { rowsExporte } } - if ((kind === "mongodb" || kind === "elasticsearch") && result.total_is_exact !== false) totalRows = Math.min(result.total, rowLimit); + if ((kind === "mongodb" || kind === "elasticsearch" || kind === "solr") && result.total_is_exact !== false) totalRows = Math.min(result.total, rowLimit); onProgress?.({ rowsExported: exportedDocuments.length, totalRows }); if (kind === "dynamodb" || kind === "elasticsearch") { @@ -705,7 +714,7 @@ async function exportAllDocumentStoreDocuments(onProgress?: (info: { rowsExporte const result = mongoDocumentsToQueryResult(exportedDocuments, performance.now() - exportStartedAt, totalRows ?? exportedDocuments.length, exportedCopyDocuments, totalRows !== null); if (result.columns.length === 0) result.columns = gridResult.value.columns; - result.column_types = kind === "mongodb" ? mongoDocumentGridColumnTypes(exportedDocuments, result.columns) : kind === "elasticsearch" ? elasticsearchGridColumnTypesFor(result.columns) : undefined; + result.column_types = kind === "mongodb" ? mongoDocumentGridColumnTypes(exportedDocuments, result.columns) : kind === "elasticsearch" ? elasticsearchGridColumnTypesFor(result.columns) : kind === "solr" ? solrGridColumnTypesFor(result.columns) : undefined; result.affected_rows = exportedDocuments.length; result.truncated = (kind === "dynamodb" || kind === "elasticsearch") && !!cursor && exportedDocuments.length >= rowLimit; result.has_more = result.truncated; @@ -717,10 +726,19 @@ const elasticsearchFilterFieldNames = computed(() => { const names = [...elasticsearchMappingFields.value.map((field) => field.name), ...gridResult.value.columns, "_id", "_routing"]; return [...new Set(names.filter(Boolean))]; }); +const solrFilterFieldNames = computed(() => { + const names = [...solrSchemaFields.value.map((field) => field.name), ...gridResult.value.columns, "_id"]; + return [...new Set(names.filter(Boolean))]; +}); const documentFilterFieldTree = computed(() => { if (documentStoreProvider.value.kind === "elasticsearch") { return elasticsearchFieldPathTreeFromFieldNames(elasticsearchFilterFieldNames.value, elasticsearchFieldTypes.value); } + if (documentStoreProvider.value.kind === "solr") { + // Schema fields give the filter builder a complete field list even before + // any document page has loaded; unknown Solr types stay leaf-selectable. + return elasticsearchFieldPathTreeFromFieldNames(solrFilterFieldNames.value, solrFieldTypes.value); + } const tree = documentFieldPathTreeFromDocuments(documents.value); if (tree.length > 0) return tree; return gridResult.value.columns.map((column) => ({ @@ -1395,12 +1413,16 @@ async function applyDocumentStructuredFilters() { applyFilter(); } -async function loadElasticsearchMappingFields() { - if (documentStoreProvider.value.kind !== "elasticsearch") return; +async function loadDocumentStoreSchemaFields() { + const kind = documentStoreProvider.value.kind; + if (kind !== "elasticsearch" && kind !== "solr") return; try { - elasticsearchMappingFields.value = (await api.getColumns(props.connectionId, props.database, "", props.collection)) ?? []; + const fields = (await api.getColumns(props.connectionId, props.database, "", props.collection)) ?? []; + if (kind === "elasticsearch") elasticsearchMappingFields.value = fields; + else solrSchemaFields.value = fields; } catch { - elasticsearchMappingFields.value = []; + if (kind === "elasticsearch") elasticsearchMappingFields.value = []; + else solrSchemaFields.value = []; } } @@ -1687,9 +1709,9 @@ async function previewDocumentChanges(changes: DocumentGridChanges): Promise= 0 ? newRow[idColIdx] : null; - const id = idValue === null || idValue === undefined || idValue === "" ? undefined : parseDocumentStoreInputValue(idValue, "meilisearch"); - stmts.push(formatMeilisearchDocumentOperationPreview({ action: id === undefined ? "insert" : "upsert", index: coll, id, document: doc })); + const id = idValue === null || idValue === undefined || idValue === "" ? undefined : parseDocumentStoreInputValue(idValue, kind === "solr" ? "solr" : "meilisearch"); + stmts.push(kind === "solr" ? formatSolrDocumentOperationPreview({ action: id === undefined ? "insert" : "upsert", core: coll, id, document: doc }) : formatMeilisearchDocumentOperationPreview({ action: id === undefined ? "insert" : "upsert", index: coll, id, document: doc })); continue; } const id = idColIdx >= 0 ? documentIdFromGridValue(newRow[idColIdx]) : null; @@ -1917,7 +1939,7 @@ async function load(options: { page?: number; append?: boolean; offset?: number; if (documentLoadExecutionId.value !== executionId) return; if (connectionId !== props.connectionId || database !== props.database || collection !== props.collection || storeKind !== documentStoreProvider.value.kind) return; const nextDocuments = - storeKind === "elasticsearch" && result.raw_documents?.length === result.documents.length + (storeKind === "elasticsearch" || storeKind === "solr") && result.raw_documents?.length === result.documents.length ? result.raw_documents.map((raw, index) => { try { return asRecord(parseJsonPreservingLargeNumbers(raw)); @@ -1997,7 +2019,7 @@ async function countExactDocumentTotal(): Promise { paginationTotal.value = totals.paginationTotal; return exactCount; } - const exactCount = request.storeKind === "dynamodb" ? await api.documentCountDocuments(request.connectionId, request.collection, request.filter) : await api.mongoCountDocuments(request.connectionId, request.database, request.collection, request.filter, "accurate"); + const exactCount = request.storeKind === "dynamodb" || request.storeKind === "solr" ? await api.documentCountDocuments(request.connectionId, request.collection, request.filter) : await api.mongoCountDocuments(request.connectionId, request.database, request.collection, request.filter, "accurate"); if (!isCurrentDocumentQueryTotalCountRequest(request)) return undefined; if (!Number.isFinite(exactCount) || exactCount < 0) { throw new Error("invalid count"); @@ -2633,9 +2655,9 @@ onMounted(async () => { console.warn("[DBX] ensureConnected failed for", props.connectionId, e); } await loadDynamoDbTableDescription(); - // Mapping metadata enriches the filter builder, but it must not delay the - // first page of documents when the mapping endpoint is slow. - void loadElasticsearchMappingFields(); + // Schema metadata enriches the filter builder, but it must not delay the + // first page of documents when the schema endpoint is slow. + void loadDocumentStoreSchemaFields(); // A restored snapshot already holds the rows the last load produced, so a tab // switch must not re-issue the collection query (#8679). Refresh and every // mutation path still force a real load. diff --git a/apps/desktop/src/components/editor/QueryEditor.vue b/apps/desktop/src/components/editor/QueryEditor.vue index 69f2829257..a62050d26a 100644 --- a/apps/desktop/src/components/editor/QueryEditor.vue +++ b/apps/desktop/src/components/editor/QueryEditor.vue @@ -270,7 +270,7 @@ function sqlBehaviorDialect(): "mysql" | "postgres" | "sqlserver" | undefined { function queryEditorSelectionLanguage(): "sql" | "text" { const databaseType = props.databaseType; - return databaseType === "redis" || databaseType === "mongodb" || databaseType === "elasticsearch" || databaseType === "easysearch" || databaseType === "meilisearch" || databaseType === "victoriametrics" ? "text" : "sql"; + return databaseType === "redis" || databaseType === "mongodb" || databaseType === "elasticsearch" || databaseType === "easysearch" || databaseType === "meilisearch" || databaseType === "solr" || databaseType === "victoriametrics" ? "text" : "sql"; } const COMPLETION_REMOTE_LATENCY_BUDGET_MS = 120; @@ -3902,7 +3902,7 @@ async function refreshSemanticDiagnostics(options: { preserveOutsideRanges?: boo setSemanticDiagnostics([]); return; } - if (props.databaseType === "elasticsearch" || props.databaseType === "easysearch" || props.databaseType === "meilisearch" || props.databaseType === "victoriametrics") { + if (props.databaseType === "elasticsearch" || props.databaseType === "easysearch" || props.databaseType === "meilisearch" || props.databaseType === "solr" || props.databaseType === "victoriametrics") { setSemanticDiagnostics([]); return; } @@ -4955,7 +4955,7 @@ function localCompletionSchemasForDatabaseDisambiguation(completionContext: Retu } function shouldInsertSqlCompletionSpace(): boolean { - return props.databaseType !== "mongodb" && props.databaseType !== "redis" && props.databaseType !== "elasticsearch" && props.databaseType !== "easysearch" && props.databaseType !== "meilisearch" && props.databaseType !== "victoriametrics"; + return props.databaseType !== "mongodb" && props.databaseType !== "redis" && props.databaseType !== "elasticsearch" && props.databaseType !== "easysearch" && props.databaseType !== "meilisearch" && props.databaseType !== "solr" && props.databaseType !== "victoriametrics"; } // Snippet expansion normally follows from the item type; a provider can also @@ -5196,7 +5196,7 @@ async function provideSqlCompletions(context: CompletionContext) { if (props.databaseType === "mongodb") { return provideMongoCompletions(currentState, position, explicit); } - if (props.databaseType === "meilisearch") return null; + if (props.databaseType === "meilisearch" || props.databaseType === "solr") return null; if (props.databaseType === "elasticsearch" || props.databaseType === "easysearch") { if (!isSqlLikeCompletionStatement(fullDoc, position, sqlCompletionDialectOptions())) { return provideElasticsearchCompletions(currentState, position, explicit); @@ -5571,7 +5571,7 @@ function shouldStartSqlCompletionAfterInput(insertedText: string, removedText: s if (props.databaseType === "mongodb") { return !!(insertedText || removedText) && shouldAutoOpenMongoCompletion(fullDoc, position); } - if (props.databaseType === "victoriametrics" || props.databaseType === "meilisearch") return false; + if (props.databaseType === "victoriametrics" || props.databaseType === "meilisearch" || props.databaseType === "solr") return false; if (props.databaseType === "redis" || props.databaseType === "elasticsearch" || props.databaseType === "easysearch") { // Preserve old character-based checks for non-SQL providers. if (!insertedText && removedText) { diff --git a/apps/desktop/src/components/export/DatabaseExportDialog.vue b/apps/desktop/src/components/export/DatabaseExportDialog.vue index bfce98ebb6..51d32e5efa 100644 --- a/apps/desktop/src/components/export/DatabaseExportDialog.vue +++ b/apps/desktop/src/components/export/DatabaseExportDialog.vue @@ -115,7 +115,7 @@ const exportElapsedText = computed(() => { return formatDataTransferDuration((exportFinishedAt.value ?? currentTime.value) - exportStartedAt.value); }); -const sqlConnections = computed(() => store.connections.filter((c) => !["redis", "mongodb", "elasticsearch", "easysearch", "meilisearch", "qdrant", "milvus", "weaviate", "chromadb", "etcd", "zookeeper", "consul", "mq", "nacos"].includes(c.db_type))); +const sqlConnections = computed(() => store.connections.filter((c) => !["redis", "mongodb", "elasticsearch", "easysearch", "meilisearch", "solr", "qdrant", "milvus", "weaviate", "chromadb", "etcd", "zookeeper", "consul", "mq", "nacos"].includes(c.db_type))); const canExport = computed(() => { const hasContent = includeStructure.value || includeData.value || includeObjects.value; diff --git a/apps/desktop/src/components/icons/DatabaseIcon.vue b/apps/desktop/src/components/icons/DatabaseIcon.vue index fe60885964..741f1ed90f 100644 --- a/apps/desktop/src/components/icons/DatabaseIcon.vue +++ b/apps/desktop/src/components/icons/DatabaseIcon.vue @@ -31,6 +31,7 @@ const assetIcons: Record = { elasticsearch: "elasticsearch", easysearch: "easysearch", meilisearch: "meilisearch", + solr: "solr", oracle: "oracle", "oracle-10g": "oracle", "oracle-legacy": "oracle", @@ -135,7 +136,7 @@ const assetSrc = computed(() => { @@ -152,4 +153,10 @@ const assetSrc = computed(() => { .database-logo-impala { transform: scale(1.55); } + +/* solr.svg 的图形撑满整个 viewBox(无内边距),其他 logo 留白约 20-25%, + 统一 scale(1.35) 下视觉偏大,单独收敛到与多数 logo 一致的占幅。 */ +.database-logo-solr { + transform: scale(1.02); +} diff --git a/apps/desktop/src/components/layout/ContentArea.vue b/apps/desktop/src/components/layout/ContentArea.vue index 8ea28b0762..c66e81129e 100644 --- a/apps/desktop/src/components/layout/ContentArea.vue +++ b/apps/desktop/src/components/layout/ContentArea.vue @@ -129,6 +129,7 @@ const DamengJobAdmin = defineAsyncComponent(() => import("@/components/admin/Dam const DamengUserAdmin = defineAsyncComponent(() => import("@/components/admin/DamengUserAdmin.vue")); const DamengRoleAdmin = defineAsyncComponent(() => import("@/components/admin/DamengRoleAdmin.vue")); +const SolrAdmin = defineAsyncComponent(() => import("@/components/solr/SolrAdmin.vue")); const PluginFilesystemTab = defineAsyncComponent(() => import("@/components/plugins/PluginFilesystemTab.vue")); const ExplainPlanViewer = defineAsyncComponent(() => import("@/components/explain/ExplainPlanViewer.vue")); const QueryChart = defineAsyncComponent(() => import("@/components/chart/QueryChart.vue")); @@ -505,7 +506,7 @@ const activeStatementExecutionMarkers = computed(() => const activeElasticsearchJsonResponse = computed(() => elasticsearchJsonResponseForResult(activeEffectiveDatabaseType.value, activeResultSql.value, props.activeTab.result)); /** Whether the active result is an Elasticsearch _source table that also has a raw JSON toggle. */ const activeElasticsearchRawBody = computed(() => { - if (activeEffectiveDatabaseType.value !== "elasticsearch" && activeEffectiveDatabaseType.value !== "easysearch") return undefined; + if (activeEffectiveDatabaseType.value !== "elasticsearch" && activeEffectiveDatabaseType.value !== "easysearch" && activeEffectiveDatabaseType.value !== "solr") return undefined; return props.activeTab.result?.elasticsearch_raw_body; }); /** ES `_search?profile=true` body extracted from the active result, when present. */ @@ -2863,6 +2864,12 @@ defineExpose({
+ +