From 8e7b74d0eec7ce40f39b68caba702ccec7ecca61 Mon Sep 17 00:00:00 2001 From: onenewcode Date: Sun, 2 Aug 2026 22:45:33 +0800 Subject: [PATCH] feat(mongodb): improve sidebar index management --- .../java/com/dbx/agent/AgentProtocol.java | 28 +- .../src/main/resources/agent-protocol-v1.json | 4 +- .../src/main/resources/agent-protocol-v2.json | 4 +- .../com/dbx/agent/mongodb/MongoAgent.java | 180 ++++++- .../com/dbx/agent/mongodb/MongoAgentTest.java | 92 ++++ .../connection/ConnectionDialog.vue | 14 +- .../components/document/DocumentBrowser.vue | 4 +- apps/desktop/src/components/grid/DataGrid.vue | 120 +++-- .../src/components/layout/ContentArea.vue | 2 +- .../sidebar/SidebarTreeItemDialogs.vue | 93 +++- .../sidebar/SidebarTreeRuntimeHost.vue | 114 ++++- .../SidebarTreeItemDialogs.mongoIndex.spec.ts | 102 ++++ .../sidebar/sidebarTreeDialogState.ts | 15 + ...abaseSpecificMutationRuntime.mongo.spec.ts | 435 ++++++++++++++++ ...eSidebarDatabaseSpecificMutationRuntime.ts | 228 +++++++-- apps/desktop/src/i18n/locales/en.ts | 12 + apps/desktop/src/i18n/locales/es.ts | 14 +- apps/desktop/src/i18n/locales/it.ts | 12 + apps/desktop/src/i18n/locales/ja.ts | 14 +- apps/desktop/src/i18n/locales/ko.ts | 12 + apps/desktop/src/i18n/locales/pt-BR.ts | 14 +- apps/desktop/src/i18n/locales/zh-CN.ts | 12 + apps/desktop/src/i18n/locales/zh-TW.ts | 14 +- .../database/databaseFeatureSupport.spec.ts | 1 + apps/desktop/src/lib/backend/http.ts | 3 +- apps/desktop/src/lib/backend/tauri.ts | 13 +- .../lib/database/databaseNamespaceCreation.ts | 3 +- .../mongo/__tests__/mongoCapabilities.spec.ts | 54 ++ .../__tests__/mongoIndexMetadata.spec.ts | 51 ++ .../src/lib/mongo/mongoCapabilities.ts | 28 ++ .../src/lib/mongo/mongoIndexMetadata.ts | 40 ++ .../src/lib/mongo/mongoShellCommand.ts | 10 +- .../__tests__/mongoCollectionMutation.spec.ts | 96 +++- .../__tests__/runMongoSidebarMutation.spec.ts | 25 + .../lib/sidebar/mongoCollectionMutation.ts | 75 +++ .../lib/sidebar/runMongoSidebarMutation.ts | 7 +- .../tableMetadataCapabilities.spec.ts | 14 + .../lib/table/tableMetadataCapabilities.ts | 6 + ...nectionStore.mongoCollectionGroups.spec.ts | 121 +++++ .../connectionStore.mongoLegacy.spec.ts | 120 +++++ apps/desktop/src/stores/connectionStore.ts | 36 +- apps/desktop/src/stores/queryStore.ts | 24 +- crates/dbx-core/assets/agent-protocol-v1.json | 4 +- crates/dbx-core/assets/agent-protocol-v2.json | 4 +- crates/dbx-core/src/db/agent_driver.rs | 43 +- crates/dbx-core/src/db/mongo_driver.rs | 381 ++++++++++++-- crates/dbx-core/src/document_ops.rs | 70 ++- crates/dbx-core/src/mongo_ops.rs | 467 +++++++++++++++++- crates/dbx-core/src/storage.rs | 138 +++++- crates/dbx-mcp/src/backend.rs | 72 ++- crates/dbx-web/src/routes/connection.rs | 164 +++++- crates/dbx-web/src/routes/mongo.rs | 40 +- packages/app-tests/mongoShellCommand.test.ts | 12 + .../productionGuardEntrypoints.test.ts | 34 +- packages/app-tests/queryStore.test.ts | 59 ++- src-tauri/src/commands/connection.rs | 50 +- 56 files changed, 3558 insertions(+), 246 deletions(-) create mode 100644 apps/desktop/src/components/sidebar/__tests__/SidebarTreeItemDialogs.mongoIndex.spec.ts create mode 100644 apps/desktop/src/composables/__tests__/useSidebarDatabaseSpecificMutationRuntime.mongo.spec.ts create mode 100644 apps/desktop/src/lib/mongo/__tests__/mongoCapabilities.spec.ts create mode 100644 apps/desktop/src/lib/mongo/__tests__/mongoIndexMetadata.spec.ts create mode 100644 apps/desktop/src/lib/mongo/mongoCapabilities.ts create mode 100644 apps/desktop/src/lib/mongo/mongoIndexMetadata.ts create mode 100644 apps/desktop/src/lib/table/__tests__/tableMetadataCapabilities.spec.ts create mode 100644 apps/desktop/src/stores/__tests__/connectionStore.mongoCollectionGroups.spec.ts create mode 100644 apps/desktop/src/stores/__tests__/connectionStore.mongoLegacy.spec.ts diff --git a/agents/common/src/main/java/com/dbx/agent/AgentProtocol.java b/agents/common/src/main/java/com/dbx/agent/AgentProtocol.java index c91ec03ef0..6af23152ae 100644 --- a/agents/common/src/main/java/com/dbx/agent/AgentProtocol.java +++ b/agents/common/src/main/java/com/dbx/agent/AgentProtocol.java @@ -57,6 +57,7 @@ public final class AgentProtocol { public static final String MONGO_METHOD_CREATE_INDEX = "create_index"; public static final String MONGO_METHOD_DROP_INDEXES = "drop_indexes"; public static final String MONGO_METHOD_DROP_COLLECTION = "drop_collection"; + public static final String MONGO_METHOD_DROP_DATABASE = "drop_database"; public static final String MONGO_METHOD_INSERT_DOCUMENT = "insert_document"; public static final String MONGO_METHOD_UPDATE_DOCUMENT = "update_document"; public static final String MONGO_METHOD_UPDATE_DOCUMENTS = "update_documents"; @@ -112,6 +113,7 @@ public final class AgentProtocol { public static final String CAPABILITY_ETCD_WATCH = "etcd_watch"; public static final String CAPABILITY_ETCD_LEASE = "etcd_lease"; public static final String CAPABILITY_ETCD_AUTH = "etcd_auth"; + public static final String CAPABILITY_MONGO_DROP_DATABASE = "mongo_drop_database"; public static final String CAPABILITY_MULTI_SESSION = "multi_session"; public static final String CAPABILITY_STRUCTURED_ERROR_V1 = "structured_error_v1"; @@ -143,11 +145,14 @@ public final class AgentProtocol { CAPABILITY_ETCD_DEFRAG, CAPABILITY_ETCD_WATCH, CAPABILITY_ETCD_LEASE, - CAPABILITY_ETCD_AUTH + CAPABILITY_ETCD_AUTH, + CAPABILITY_MONGO_DROP_DATABASE )); public static final List MULTI_SESSION_CAPABILITIES; public static final List MULTI_SESSION_ALL_CAPABILITIES; + public static final List MONGO_LEGACY_CAPABILITIES; + public static final List MONGO_LEGACY_MULTI_SESSION_CAPABILITIES; public static final List MULTI_SESSION_JDBC_CAPABILITIES; public static final List MULTI_SESSION_JDBC_ALL_CAPABILITIES; @@ -197,6 +202,14 @@ public final class AgentProtocol { allCapabilities.add(CAPABILITY_MULTI_SESSION); MULTI_SESSION_ALL_CAPABILITIES = Collections.unmodifiableList(allCapabilities); + List mongoCapabilities = new java.util.ArrayList<>(CAPABILITIES); + mongoCapabilities.add(CAPABILITY_MONGO_DROP_DATABASE); + MONGO_LEGACY_CAPABILITIES = Collections.unmodifiableList(mongoCapabilities); + + List mongoMultiSessionCapabilities = new java.util.ArrayList<>(MULTI_SESSION_CAPABILITIES); + mongoMultiSessionCapabilities.add(CAPABILITY_MONGO_DROP_DATABASE); + MONGO_LEGACY_MULTI_SESSION_CAPABILITIES = Collections.unmodifiableList(mongoMultiSessionCapabilities); + List jdbcCapabilities = new java.util.ArrayList<>(MULTI_SESSION_CAPABILITIES); jdbcCapabilities.add(CAPABILITY_STRUCTURED_ERROR_V1); MULTI_SESSION_JDBC_CAPABILITIES = Collections.unmodifiableList(jdbcCapabilities); @@ -226,6 +239,7 @@ public final class AgentProtocol { MONGO_METHOD_CREATE_INDEX, MONGO_METHOD_DROP_INDEXES, MONGO_METHOD_DROP_COLLECTION, + MONGO_METHOD_DROP_DATABASE, MONGO_METHOD_INSERT_DOCUMENT, MONGO_METHOD_UPDATE_DOCUMENT, MONGO_METHOD_UPDATE_DOCUMENTS, @@ -281,6 +295,18 @@ public final class AgentProtocol { ); } + public static HandshakeResult mongoLegacyHandshakeResult() { + return new HandshakeResult(PROTOCOL_VERSION, PROTOCOL_VERSION, MONGO_LEGACY_CAPABILITIES); + } + + public static HandshakeResult mongoLegacyMultiSessionHandshakeResult() { + return new HandshakeResult( + MULTI_SESSION_PROTOCOL_VERSION, + MULTI_SESSION_PROTOCOL_VERSION, + MONGO_LEGACY_MULTI_SESSION_CAPABILITIES + ); + } + /** * Handshake for pooled JDBC Agents that emit the v1 structured error contract. * Generic/custom v2 handlers must continue using multiSessionHandshakeResult(). diff --git a/agents/common/src/main/resources/agent-protocol-v1.json b/agents/common/src/main/resources/agent-protocol-v1.json index b2c77b6f88..ca4dc704b3 100644 --- a/agents/common/src/main/resources/agent-protocol-v1.json +++ b/agents/common/src/main/resources/agent-protocol-v1.json @@ -2,7 +2,7 @@ "protocolVersion": 1, "handshakeMethod": "handshake", "handshakeResponseFields": ["protocolVersion", "agentProtocolVersion", "capabilities"], - "allCapabilities": ["connect", "test_connection", "metadata", "query", "paged_query", "transaction", "ddl", "kv", "kv_ttl", "kv_cas", "kv_list_values", "kv_status", "kv_history", "etcd_compaction", "etcd_defrag", "etcd_watch", "etcd_lease", "etcd_auth"], + "allCapabilities": ["connect", "test_connection", "metadata", "query", "paged_query", "transaction", "ddl", "kv", "kv_ttl", "kv_cas", "kv_list_values", "kv_status", "kv_history", "etcd_compaction", "etcd_defrag", "etcd_watch", "etcd_lease", "etcd_auth", "mongo_drop_database"], "capabilities": ["connect", "test_connection", "metadata", "query", "paged_query", "transaction", "ddl"], "defaultSqlCapabilities": ["connect", "test_connection", "metadata", "query", "paged_query", "transaction", "ddl"], "commonMethods": [ @@ -39,6 +39,6 @@ "disconnect", "shutdown" ], - "mongoLegacyMethods": ["list_databases", "list_collections", "find_documents", "find_documents_extended_json", "count_documents", "server_version", "create_index", "drop_indexes", "drop_collection", "insert_document", "update_document", "update_documents", "delete_document", "delete_documents"], + "mongoLegacyMethods": ["list_databases", "list_collections", "find_documents", "find_documents_extended_json", "count_documents", "server_version", "create_index", "drop_indexes", "drop_collection", "drop_database", "insert_document", "update_document", "update_documents", "delete_document", "delete_documents"], "kvMethods": ["kv_list_prefix", "kv_get", "kv_put", "kv_delete", "kv_rename", "kv_history", "kv_status", "etcd_compact", "etcd_defrag", "etcd_watch_start", "etcd_watch_poll", "etcd_watch_stop", "etcd_lease_list", "etcd_lease_get", "etcd_lease_grant", "etcd_lease_keepalive_once", "etcd_lease_revoke", "etcd_auth_user_list", "etcd_auth_user_get", "etcd_auth_user_add", "etcd_auth_user_delete", "etcd_auth_user_change_password", "etcd_auth_user_grant_role", "etcd_auth_user_revoke_role", "etcd_auth_role_list", "etcd_auth_role_get", "etcd_auth_role_add", "etcd_auth_role_delete", "etcd_auth_role_grant_permission", "etcd_auth_role_revoke_permission"] } diff --git a/agents/common/src/main/resources/agent-protocol-v2.json b/agents/common/src/main/resources/agent-protocol-v2.json index bc282be4a9..079cebc278 100644 --- a/agents/common/src/main/resources/agent-protocol-v2.json +++ b/agents/common/src/main/resources/agent-protocol-v2.json @@ -2,7 +2,7 @@ "protocolVersion": 2, "handshakeMethod": "handshake", "handshakeResponseFields": ["protocolVersion", "agentProtocolVersion", "capabilities"], - "allCapabilities": ["connect", "test_connection", "metadata", "query", "paged_query", "transaction", "ddl", "kv", "kv_ttl", "kv_cas", "kv_list_values", "kv_status", "kv_history", "etcd_compaction", "etcd_defrag", "etcd_watch", "etcd_lease", "etcd_auth", "multi_session", "structured_error_v1"], + "allCapabilities": ["connect", "test_connection", "metadata", "query", "paged_query", "transaction", "ddl", "kv", "kv_ttl", "kv_cas", "kv_list_values", "kv_status", "kv_history", "etcd_compaction", "etcd_defrag", "etcd_watch", "etcd_lease", "etcd_auth", "mongo_drop_database", "multi_session", "structured_error_v1"], "capabilities": ["connect", "test_connection", "metadata", "query", "paged_query", "transaction", "ddl", "multi_session"], "defaultSqlCapabilities": ["connect", "test_connection", "metadata", "query", "paged_query", "transaction", "ddl", "multi_session", "structured_error_v1"], "commonMethods": [ @@ -43,7 +43,7 @@ "disconnect", "shutdown" ], - "mongoLegacyMethods": ["list_databases", "list_collections", "find_documents", "find_documents_extended_json", "count_documents", "server_version", "create_index", "drop_indexes", "drop_collection", "insert_document", "update_document", "update_documents", "delete_document", "delete_documents"], + "mongoLegacyMethods": ["list_databases", "list_collections", "find_documents", "find_documents_extended_json", "count_documents", "server_version", "create_index", "drop_indexes", "drop_collection", "drop_database", "insert_document", "update_document", "update_documents", "delete_document", "delete_documents"], "kvMethods": ["kv_list_prefix", "kv_get", "kv_put", "kv_delete", "kv_rename", "kv_history", "kv_status", "etcd_compact", "etcd_defrag", "etcd_watch_start", "etcd_watch_poll", "etcd_watch_stop", "etcd_lease_list", "etcd_lease_get", "etcd_lease_grant", "etcd_lease_keepalive_once", "etcd_lease_revoke", "etcd_auth_user_list", "etcd_auth_user_get", "etcd_auth_user_add", "etcd_auth_user_delete", "etcd_auth_user_change_password", "etcd_auth_user_grant_role", "etcd_auth_user_revoke_role", "etcd_auth_role_list", "etcd_auth_role_get", "etcd_auth_role_add", "etcd_auth_role_delete", "etcd_auth_role_grant_permission", "etcd_auth_role_revoke_permission"], "sessionField": "agentSessionId", "cursorSessionField": "sessionId" diff --git a/agents/drivers/mongodb/src/main/java/com/dbx/agent/mongodb/MongoAgent.java b/agents/drivers/mongodb/src/main/java/com/dbx/agent/mongodb/MongoAgent.java index 1b63f1b72d..08f61c694c 100644 --- a/agents/drivers/mongodb/src/main/java/com/dbx/agent/mongodb/MongoAgent.java +++ b/agents/drivers/mongodb/src/main/java/com/dbx/agent/mongodb/MongoAgent.java @@ -21,6 +21,7 @@ import java.io.BufferedReader; import java.io.FileInputStream; import java.io.InputStream; import java.io.InputStreamReader; +import java.math.BigDecimal; import java.net.URLDecoder; import java.nio.charset.StandardCharsets; import java.nio.file.Files; @@ -48,6 +49,7 @@ import java.util.Set; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; +import java.util.function.Consumer; import javax.net.ssl.KeyManager; import javax.net.ssl.KeyManagerFactory; import javax.net.ssl.SSLContext; @@ -56,6 +58,7 @@ import javax.net.ssl.TrustManagerFactory; import org.bson.Document; import org.bson.json.JsonMode; import org.bson.json.JsonWriterSettings; +import org.bson.types.Decimal128; import org.bson.types.ObjectId; public final class MongoAgent { @@ -66,6 +69,7 @@ public final class MongoAgent { .outputMode(JsonMode.RELAXED) .build(); private static final String LEGACY_SESSION_ID = "__legacy__"; + private static final String DEFAULT_ID_INDEX_NAME = "_id_"; private static final int MAX_SESSIONS = 256; private static final ThreadLocal CURRENT_CLIENT = new ThreadLocal<>(); private static MongoClient legacyClient; @@ -321,6 +325,25 @@ public final class MongoAgent { private static Object listCollections(JsonObject params) { MongoClient c = requireClient(); String database = params.get("database").getAsString(); + boolean includeTypes = params.has("include_types") + && !params.get("include_types").isJsonNull() + && params.get("include_types").getAsBoolean(); + // Older DBX clients expect a simple string array. Opt into collection + // metadata so this RPC remains compatible with already-installed agents. + if (includeTypes) { + List> result = new ArrayList<>(); + for (Document spec : c.getDatabase(database).listCollections()) { + String name = spec.getString("name"); + // Collection identifiers are passed through verbatim elsewhere; + // only an impossible empty listCollections name is discarded. + if (name == null || name.isEmpty()) { + continue; + } + result.add(collectionSpec(name, spec.getString("type"))); + } + return result; + } + List result = new ArrayList<>(); for (String name : c.getDatabase(database).listCollectionNames()) { result.add(name); @@ -328,6 +351,23 @@ public final class MongoAgent { return result; } + static Map collectionSpec(String name, String type) { + Map result = new LinkedHashMap<>(); + result.put("name", name); + result.put("kind", collectionKind(type)); + return result; + } + + static String collectionKind(String type) { + if ("view".equalsIgnoreCase(type)) { + return "view"; + } + if ("timeseries".equalsIgnoreCase(type)) { + return "timeseries"; + } + return "collection"; + } + private static Object listIndexes(JsonObject params) { MongoClient c = requireClient(); String database = params.get("database").getAsString(); @@ -497,6 +537,37 @@ public final class MongoAgent { return version; } + static boolean serverVersionRequiresSerialDropIndexes(String version) { + if (version == null) { + return false; + } + int start = 0; + while (start < version.length() && !Character.isDigit(version.charAt(start))) { + start++; + } + String[] components = version.substring(start).split("\\.", 3); + if (components.length < 2) { + return false; + } + try { + int major = Integer.parseInt(components[0]); + int minor = Integer.parseInt(components[1]); + return major < 4 || (major == 4 && minor < 2); + } catch (NumberFormatException error) { + return false; + } + } + + private static boolean serverRequiresSerialDropIndexes(MongoClient client, String database) { + try { + Document buildInfo = client.getDatabase(database).runCommand(new Document("buildInfo", 1)); + return serverVersionRequiresSerialDropIndexes(serverVersionFromBuildInfo(buildInfo)); + } catch (RuntimeException error) { + // Without an explicit old version, preserve MongoDB's single-command array semantics. + return false; + } + } + private static Object createIndex(JsonObject params) { MongoClient c = requireClient(); String database = params.get("database").getAsString(); @@ -509,12 +580,22 @@ public final class MongoAgent { Document index = new Document("key", keys); Document options = documentOrNull(params, "options_json"); if (options != null) { + if (options.containsKey("key")) { + throw new IllegalArgumentException("Index options cannot contain \"key\"; specify index fields in keys JSON"); + } index.putAll(options); } - String name = index.getString("name"); - if (name == null || name.isBlank()) { + String name; + if (!index.containsKey("name")) { name = defaultIndexName(keys); index.put("name", name); + } else if (!(index.get("name") instanceof String)) { + throw new IllegalArgumentException("Index option \"name\" must be a non-empty string"); + } else { + name = (String) index.get("name"); + if (name.isBlank()) { + throw new IllegalArgumentException("Index option \"name\" must be a non-empty string"); + } } c.getDatabase(database).runCommand( @@ -532,14 +613,24 @@ public final class MongoAgent { return document; } - private static String defaultIndexName(Document keys) { + static String defaultIndexName(Document keys) { List parts = new ArrayList<>(); for (Map.Entry entry : keys.entrySet()) { - parts.add(entry.getKey() + "_" + String.valueOf(entry.getValue())); + parts.add(entry.getKey() + "_" + defaultIndexNameValue(entry.getValue())); } return String.join("_", parts); } + private static String defaultIndexNameValue(Object value) { + if (value instanceof Double number && Double.isFinite(number)) { + // The Rust driver's BSON formatter omits a fractional suffix for + // whole doubles (for example, 1.0 becomes 1). Keep unnamed index + // names identical on Native and Legacy connections. + return BigDecimal.valueOf(number).stripTrailingZeros().toPlainString(); + } + return String.valueOf(value); + } + private static Object dropIndexes(JsonObject params) { MongoClient c = requireClient(); String database = params.get("database").getAsString(); @@ -548,13 +639,50 @@ public final class MongoAgent { boolean single = params.has("single") && !params.get("single").isJsonNull() && params.get("single").getAsBoolean(); Object index = parseDropIndexesValue(indexesJson, single); + if (index instanceof List indexes && serverRequiresSerialDropIndexes(c, database)) { + // Array-form dropIndexes was added in MongoDB 4.2. Older servers + // require individual commands and therefore return partial results. + return dropNamedIndexes( + indexes, + indexName -> c.getDatabase(database).runCommand( + new Document("dropIndexes", collection).append("index", indexName) + ) + ); + } + List before = listIndexInfos(c, database, collection); c.getDatabase(database).runCommand(new Document("dropIndexes", collection).append("index", index)); List after = listIndexInfos(c, database, collection); List droppedNames = diffDroppedIndexNames(before, after); + return dropIndexesResult(droppedNames, Collections.emptyList()); + } + + static Map dropNamedIndexes(List indexes, Consumer dropCommand) { + List droppedNames = new ArrayList<>(); + List> failures = new ArrayList<>(); + for (Object indexName : indexes) { + String name = String.valueOf(indexName); + try { + dropCommand.accept(indexName); + droppedNames.add(name); + } catch (RuntimeException error) { + String message = error.getMessage() == null ? error.toString() : error.getMessage(); + failures.add(Map.of("name", name, "message", message)); + } + } + return dropIndexesResult(droppedNames, failures); + } + + private static Map dropIndexesResult( + List droppedNames, + List> failures + ) { Map result = new LinkedHashMap<>(); result.put("dropped_names", droppedNames); result.put("affected_rows", droppedNames.size()); + if (!failures.isEmpty()) { + result.put("failures", failures); + } return result; } @@ -566,7 +694,14 @@ public final class MongoAgent { return Collections.singletonMap("ok", true); } - private static Object parseDropIndexesValue(String indexesJson, boolean single) { + private static Object dropDatabase(JsonObject params) { + MongoClient c = requireClient(); + String database = params.get("database").getAsString(); + c.getDatabase(database).drop(); + return Collections.singletonMap("ok", true); + } + + static Object parseDropIndexesValue(String indexesJson, boolean single) { if (indexesJson == null || indexesJson.isBlank()) { if (single) { throw new IllegalArgumentException("dropIndex requires a string index name or JSON document"); @@ -583,6 +718,9 @@ public final class MongoAgent { if (single && "*".equals(name)) { throw new IllegalArgumentException("dropIndex does not accept \"*\"; use dropIndexes() or dropIndexes(\"*\") instead"); } + if (DEFAULT_ID_INDEX_NAME.equals(name)) { + throw new IllegalArgumentException("The default MongoDB _id_ index cannot be dropped"); + } return name; } if (value.isJsonObject()) { @@ -590,7 +728,11 @@ public final class MongoAgent { if (object.size() == 0) { throw new IllegalArgumentException("Index specification is required"); } - return Document.parse(indexesJson); + Document specification = Document.parse(indexesJson); + if (isDefaultIdIndexSpecification(specification)) { + throw new IllegalArgumentException("The default MongoDB _id_ index cannot be dropped"); + } + return specification; } if (value.isJsonArray()) { if (single) { @@ -606,6 +748,9 @@ public final class MongoAgent { if (names.isEmpty()) { throw new IllegalArgumentException("dropIndexes only accepts non-empty string arrays"); } + if (names.contains(DEFAULT_ID_INDEX_NAME)) { + throw new IllegalArgumentException("The default MongoDB _id_ index cannot be dropped"); + } return names; } if (single) { @@ -614,6 +759,20 @@ public final class MongoAgent { throw new IllegalArgumentException("dropIndexes only accepts a string index name, JSON document, or string array"); } + private static boolean isDefaultIdIndexSpecification(Document specification) { + if (specification.size() != 1 || !specification.containsKey("_id")) { + return false; + } + Object direction = specification.get("_id"); + if (direction instanceof Number number) { + return number.doubleValue() == 1.0; + } + // Document.parse turns Extended JSON $numberDecimal values into + // Decimal128, which does not implement Number in the legacy driver. + return direction instanceof Decimal128 decimal + && decimal.bigDecimalValue().compareTo(BigDecimal.ONE) == 0; + } + private static List listIndexInfos(MongoClient c, String database, String collection) { List result = new ArrayList<>(); for (Document index : c.getDatabase(database).getCollection(collection).listIndexes()) { @@ -992,7 +1151,7 @@ public final class MongoAgent { private static Object dispatch(String method, JsonObject params) { return switch (method) { - case AgentProtocol.METHOD_HANDSHAKE -> AgentProtocol.handshakeResult(); + case AgentProtocol.METHOD_HANDSHAKE -> AgentProtocol.mongoLegacyHandshakeResult(); case AgentProtocol.METHOD_CONNECT -> connect(params); case AgentProtocol.MONGO_METHOD_LIST_DATABASES -> listDatabases(); case AgentProtocol.MONGO_METHOD_LIST_COLLECTIONS -> listCollections(params); @@ -1004,6 +1163,7 @@ public final class MongoAgent { case AgentProtocol.MONGO_METHOD_CREATE_INDEX -> createIndex(params); case AgentProtocol.MONGO_METHOD_DROP_INDEXES -> dropIndexes(params); case AgentProtocol.MONGO_METHOD_DROP_COLLECTION -> dropCollection(params); + case AgentProtocol.MONGO_METHOD_DROP_DATABASE -> dropDatabase(params); case AgentProtocol.MONGO_METHOD_INSERT_DOCUMENT -> insertDocument(params); case AgentProtocol.MONGO_METHOD_UPDATE_DOCUMENT -> updateDocument(params); case AgentProtocol.MONGO_METHOD_UPDATE_DOCUMENTS -> updateDocuments(params); @@ -1020,6 +1180,10 @@ public final class MongoAgent { }; } + static AgentProtocol.HandshakeResult runtimeHandshakeResult() { + return AgentProtocol.mongoLegacyMultiSessionHandshakeResult(); + } + private static MongoClient requireClient() { MongoClient client = CURRENT_CLIENT.get(); if (client == null) { @@ -1119,7 +1283,7 @@ public final class MongoAgent { try { Object result; if (AgentProtocol.METHOD_HANDSHAKE.equals(method)) { - result = AgentProtocol.multiSessionHandshakeResult(); + result = runtimeHandshakeResult(); } else if (AgentProtocol.METHOD_OPEN_SESSION.equals(method)) { result = openSession(requiredSessionId(params), params); } else if (AgentProtocol.METHOD_CLOSE_SESSION.equals(method)) { diff --git a/agents/drivers/mongodb/src/test/java/com/dbx/agent/mongodb/MongoAgentTest.java b/agents/drivers/mongodb/src/test/java/com/dbx/agent/mongodb/MongoAgentTest.java index 68b69ebb80..0f78d5a885 100644 --- a/agents/drivers/mongodb/src/test/java/com/dbx/agent/mongodb/MongoAgentTest.java +++ b/agents/drivers/mongodb/src/test/java/com/dbx/agent/mongodb/MongoAgentTest.java @@ -8,6 +8,7 @@ import static org.junit.jupiter.api.Assertions.assertTrue; import com.dbx.agent.AgentProtocol; import com.dbx.agent.IndexInfo; +import com.google.gson.Gson; import com.google.gson.JsonArray; import com.google.gson.JsonObject; import com.google.gson.JsonParser; @@ -113,6 +114,7 @@ class MongoAgentTest { assertTrue(containsCapability(result.getAsJsonArray("capabilities"), AgentProtocol.CAPABILITY_CONNECT)); assertTrue(containsCapability(result.getAsJsonArray("capabilities"), AgentProtocol.CAPABILITY_QUERY)); assertTrue(containsCapability(result.getAsJsonArray("capabilities"), AgentProtocol.CAPABILITY_METADATA)); + assertTrue(containsCapability(result.getAsJsonArray("capabilities"), AgentProtocol.CAPABILITY_MONGO_DROP_DATABASE)); } @Test @@ -125,6 +127,15 @@ class MongoAgentTest { assertFalse(containsCapability(result.getAsJsonArray("capabilities"), AgentProtocol.CAPABILITY_MULTI_SESSION)); } + @Test + void runtimeHandshakeAdvertisesDropDatabaseForMultiSessionConnections() { + JsonObject result = new Gson().toJsonTree(MongoAgent.runtimeHandshakeResult()).getAsJsonObject(); + + assertEquals(AgentProtocol.MULTI_SESSION_PROTOCOL_VERSION, result.get("protocolVersion").getAsInt()); + assertTrue(containsCapability(result.getAsJsonArray("capabilities"), AgentProtocol.CAPABILITY_MULTI_SESSION)); + assertTrue(containsCapability(result.getAsJsonArray("capabilities"), AgentProtocol.CAPABILITY_MONGO_DROP_DATABASE)); + } + @Test void listIndexesMethodIsRecognizedOverJsonRpc() { String response = MongoAgent.handleRequest( @@ -137,6 +148,14 @@ class MongoAgentTest { assertFalse(json.getAsJsonObject("error").get("message").getAsString().contains("Unknown method")); } + @Test + void collectionSpecsPreserveCollectionKindsForTypeAwareClients() { + assertEquals(Map.of("name", "orders", "kind", "collection"), MongoAgent.collectionSpec("orders", "collection")); + assertEquals(Map.of("name", "report_view", "kind", "view"), MongoAgent.collectionSpec("report_view", "view")); + assertEquals(Map.of("name", "metrics", "kind", "timeseries"), MongoAgent.collectionSpec("metrics", "timeseries")); + assertEquals("collection", MongoAgent.collectionKind("futureType")); + } + @Test void countDocumentsMethodIsRecognizedOverJsonRpc() { String response = MongoAgent.handleRequest( @@ -289,6 +308,14 @@ class MongoAgentTest { assertTrue(AgentProtocol.MONGO_LEGACY_METHODS.contains(AgentProtocol.MONGO_METHOD_CREATE_INDEX)); } + @Test + void defaultIndexNameMatchesNativeDriverForWholeDoubles() { + assertEquals( + "email_1_createdAt_-1", + MongoAgent.defaultIndexName(Document.parse("{\"email\":1.0,\"createdAt\":-1.0}")) + ); + } + @Test void dropIndexesMethodIsRecognizedOverJsonRpc() { String response = MongoAgent.handleRequest( @@ -303,6 +330,57 @@ class MongoAgentTest { assertTrue(AgentProtocol.MONGO_LEGACY_METHODS.contains(AgentProtocol.MONGO_METHOD_DROP_INDEXES)); } + @Test + void dropIndexesRejectsTheDefaultIdIndex() { + for (String indexesJson : List.of( + "\"_id_\"", + "{\"_id\":1}", + "{\"_id\":{\"$numberDecimal\":\"1.0\"}}", + "[\"email_1\",\"_id_\"]" + )) { + IllegalArgumentException error = assertThrows( + IllegalArgumentException.class, + () -> MongoAgent.parseDropIndexesValue(indexesJson, false) + ); + assertEquals("The default MongoDB _id_ index cannot be dropped", error.getMessage()); + } + + assertThrows( + IllegalArgumentException.class, + () -> MongoAgent.parseDropIndexesValue("\"_id_\"", true) + ); + assertEquals("*", MongoAgent.parseDropIndexesValue("\"*\"", false)); + } + + @Test + void batchDropIndexesReportsPartialFailuresAndContinues() { + List calls = new ArrayList<>(); + + Map result = MongoAgent.dropNamedIndexes(List.of("email_1", "missing_1", "created_at_-1"), name -> { + calls.add(String.valueOf(name)); + if ("missing_1".equals(name)) { + throw new IllegalStateException("index not found"); + } + }); + + assertEquals(List.of("email_1", "missing_1", "created_at_-1"), calls); + assertEquals(List.of("email_1", "created_at_-1"), result.get("dropped_names")); + assertEquals(2, result.get("affected_rows")); + assertEquals( + List.of(Map.of("name", "missing_1", "message", "index not found")), + result.get("failures") + ); + } + + @Test + void batchDropIndexesUsesSerialFallbackOnlyBeforeMongo42() { + assertTrue(MongoAgent.serverVersionRequiresSerialDropIndexes("3.4.24")); + assertTrue(MongoAgent.serverVersionRequiresSerialDropIndexes("4.0.28")); + assertFalse(MongoAgent.serverVersionRequiresSerialDropIndexes("4.2.0")); + assertFalse(MongoAgent.serverVersionRequiresSerialDropIndexes("7.0.14")); + assertFalse(MongoAgent.serverVersionRequiresSerialDropIndexes("unknown")); + } + @Test void dropCollectionMethodIsRecognizedOverJsonRpc() { String response = MongoAgent.handleRequest( @@ -316,6 +394,20 @@ class MongoAgentTest { assertTrue(AgentProtocol.MONGO_LEGACY_METHODS.contains(AgentProtocol.MONGO_METHOD_DROP_COLLECTION)); } + @Test + void dropDatabaseMethodIsRecognizedOverJsonRpc() { + String response = MongoAgent.handleRequest( + "{\"jsonrpc\":\"2.0\",\"id\":15,\"method\":\"drop_database\"," + + "\"params\":{\"database\":\"app\"}}" + ); + + JsonObject json = JsonParser.parseString(response).getAsJsonObject(); + assertEquals(15, json.get("id").getAsInt()); + assertEquals("Not connected", json.getAsJsonObject("error").get("message").getAsString()); + assertFalse(json.getAsJsonObject("error").get("message").getAsString().contains("Unknown method")); + assertTrue(AgentProtocol.MONGO_LEGACY_METHODS.contains(AgentProtocol.MONGO_METHOD_DROP_DATABASE)); + } + @Test void updateDocumentsMethodIsRecognizedOverJsonRpc() { String response = MongoAgent.handleRequest( diff --git a/apps/desktop/src/components/connection/ConnectionDialog.vue b/apps/desktop/src/components/connection/ConnectionDialog.vue index ac75ce528e..96c7e22848 100644 --- a/apps/desktop/src/components/connection/ConnectionDialog.vue +++ b/apps/desktop/src/components/connection/ConnectionDialog.vue @@ -38,6 +38,7 @@ import { firstZooKeeperEndpoint, normalizeZooKeeperConnectString } from "@/lib/z import { isLocalFileTypeDb } from "@/lib/connection/connectionFile"; import { MQ_PINNED_VERSION_OPTIONS, pinnedVersionToSelection, selectionToPinnedVersion } from "@/lib/mq/mqPinnedVersionOptions"; import { mongodbAuthFailureHint, mongoUrlParam, mongoUrlParamIsTrue, normalizeMongoTlsFormState, setMongoUrlParam, setMongoUrlParamBoolean } from "@/lib/mongo/mongoConnectionOptions"; +import { isMongoLegacyDriverProfile } from "@/lib/mongo/mongoCapabilities"; import { mysqlCleartextPasswordAuthEnabled, setMysqlCleartextPasswordAuthEnabled } from "@/lib/database/mysqlConnectionOptions"; import { applyDamengSslUrlParams, damengSslFormConfig } from "@/lib/database/damengSslOptions"; import { DamengJvmSystemPropertyError, damengJvmSystemPropertiesText, parseDamengJvmSystemProperties } from "@/lib/database/damengJvmOptions"; @@ -2884,7 +2885,7 @@ const mongoRetryWrites = computed({ }, }); const mongoDriverMode = computed({ - get: () => (form.value.driver_profile === "mongodb-legacy" ? "legacy" : "auto"), + get: () => (isMongoLegacyDriverProfile(form.value.driver_profile) ? "legacy" : "auto"), set: (value: string) => { form.value.driver_profile = value === "legacy" ? "mongodb-legacy" : "mongodb"; form.value.driver_label = value === "legacy" ? "MongoDB (Legacy)" : "MongoDB"; @@ -3276,9 +3277,14 @@ function connectionConfigForSubmit(id: string, generatedName = ""): ConnectionCo } else if (config.db_type === "mongodb") { config.connection_string = normalizeMongoConnectionString(config.connection_string?.trim() || ""); } - if (config.db_type === "mongodb" && config.driver_profile !== "mongodb-legacy") { - config.driver_profile = "mongodb"; - config.driver_label = "MongoDB"; + if (config.db_type === "mongodb") { + if (isMongoLegacyDriverProfile(config.driver_profile)) { + config.driver_profile = "mongodb-legacy"; + config.driver_label = "MongoDB (Legacy)"; + } else { + config.driver_profile = "mongodb"; + config.driver_label = "MongoDB"; + } } if (config.db_type === "mongodb") { const mongoTls = normalizeMongoTlsFormState(!!config.ssl, config.url_params, config.ca_cert_path); diff --git a/apps/desktop/src/components/document/DocumentBrowser.vue b/apps/desktop/src/components/document/DocumentBrowser.vue index 4e38d65d07..6ba9e80eb2 100644 --- a/apps/desktop/src/components/document/DocumentBrowser.vue +++ b/apps/desktop/src/components/document/DocumentBrowser.vue @@ -71,7 +71,7 @@ import { documentGridColumnVisibilityScopeKey, migrateDocumentGridColumnVisibili import { useSettingsStore } from "@/stores/settingsStore"; import JsonEditNode from "./JsonEditNode.vue"; import type { EditNode } from "@/types/editor"; -import type { ColumnInfo, DatabaseType, QueryResult } from "@/types/database"; +import type { ColumnInfo, DatabaseType, QueryResult, QueryTab } from "@/types/database"; import type { CustomSaveHandler } from "@/composables/useDataGridEditor"; import { Splitpanes, Pane } from "splitpanes"; import "splitpanes/dist/splitpanes.css"; @@ -85,6 +85,7 @@ const props = defineProps<{ database: string; collection: string; databaseType?: DatabaseType; + tableMeta?: NonNullable; }>(); type JsonRecord = Record; @@ -1667,6 +1668,7 @@ defineExpose({ focusSearch }); :result="gridResult" :connection-id="props.connectionId" :database="props.database" + :table-meta="props.tableMeta" :column-layout-scope-key="documentColumnLayoutScopeKey" context="results" :database-type="props.databaseType" diff --git a/apps/desktop/src/components/grid/DataGrid.vue b/apps/desktop/src/components/grid/DataGrid.vue index 588c35a01d..f9150bf04a 100644 --- a/apps/desktop/src/components/grid/DataGrid.vue +++ b/apps/desktop/src/components/grid/DataGrid.vue @@ -221,6 +221,10 @@ import { supportsTableStructureEditing } from "@/lib/database/databaseCapabiliti import { rememberDataGridConditionHistory } from "@/lib/dataGrid/dataGridConditionHistory"; import { restoreDataGridLocalColumnFilters, serializeDataGridLocalColumnFilters } from "@/lib/dataGrid/dataGridLocalColumnFilterState"; import { effectiveDatabaseTypeForConnection } from "@/lib/database/jdbcDialect"; +import { mongoCollectionSupportsIndexes, supportsMongoIndexMutations } from "@/lib/mongo/mongoCapabilities"; +import { refreshLoadedMongoIndexes } from "@/lib/mongo/mongoIndexMetadata"; +import { isProtectedMongoIndex, mongoDropAllIndexesPreview, mongoDropIndexFailureCount, mongoDropIndexPreview } from "@/lib/sidebar/mongoCollectionMutation"; +import { runMongoMutation } from "@/lib/sidebar/runMongoSidebarMutation"; import { dataGridConditionColumnOptions, dataGridConditionIdentifierQuote } from "@/lib/dataGrid/dataGridConditionCompletion"; import { isMacOS } from "@/lib/backend/platform"; import { appendDebugLog, isDebugLoggingEnabled } from "@/lib/backend/debugLog"; @@ -7336,7 +7340,8 @@ function toggleCellDetailPanelLayout() { const tableMetadataCapabilities = computed(() => getTableMetadataCapabilities(props.databaseType)); const canOpenTableStructureEditor = computed(() => !!props.connectionId && !!props.database && !!props.tableMeta?.tableName && supportsTableStructureEditing(resolvedDatabaseType.value)); const mongoConnectionConfig = resolvedConnectionConfig; -const canManageMongoIndexes = computed(() => resolvedDatabaseType.value === "mongodb" && !!props.connectionId && !!props.database && !!props.tableMeta?.tableName && mongoConnectionConfig.value?.db_type === "mongodb" && mongoConnectionConfig.value?.driver_profile !== "mongodb-legacy"); +const canManageMongoIndexes = computed(() => resolvedDatabaseType.value === "mongodb" && !!props.connectionId && !!props.database && !!props.tableMeta?.tableName && supportsMongoIndexMutations(mongoConnectionConfig.value, props.tableMeta?.tableType)); +const canShowTableIndexes = computed(() => tableMetadataCapabilities.value.indexes && (resolvedDatabaseType.value !== "mongodb" || mongoCollectionSupportsIndexes(props.tableMeta?.tableType))); const tableInfoTabs = computed(() => { const tabs: TableInfoTabItem[] = []; if (tableMetadataCapabilities.value.ddl) { @@ -7350,7 +7355,7 @@ const tableInfoTabs = computed(() => { count: props.tableMeta?.columns.length, }); } - if (tableMetadataCapabilities.value.indexes) { + if (canShowTableIndexes.value) { tabs.push({ id: "indexes", label: t("grid.tableInfoIndexes"), icon: KeyRound, count: indexes.value.length }); } if (tableMetadataCapabilities.value.foreignKeys) { @@ -7435,6 +7440,19 @@ async function reloadIndexes() { await fetchIndexes(); } +async function refreshMongoIndexMetadataAfterMutation() { + await reloadIndexes(); + const connectionId = props.connectionId; + const database = props.database; + const collection = props.tableMeta?.tableName; + if (!connectionId || !database || !collection) return; + try { + await refreshLoadedMongoIndexes(connectionStore, { connectionId, database, collection }); + } catch (e: any) { + toast(t("contextMenu.mongoIndexRefreshFailed", { message: String(e?.message || e) }), 5000); + } +} + async function fetchForeignKeys() { const requestIdentity = currentForeignKeyTableIdentity.value; if (!props.connectionId || !props.tableMeta || !requestIdentity || foreignKeysLoaded.value || foreignKeysLoading.value) return; @@ -7775,7 +7793,8 @@ const filteredIndexes = computed(() => { return indexes.value.filter((i) => i.name.toLowerCase().includes(q) || i.columns.some((c) => c.toLowerCase().includes(q))); }); -const droppableMongoIndexes = computed(() => indexes.value.filter((index) => !index.is_primary)); +const droppableMongoIndexes = computed(() => indexes.value.filter((index) => !isProtectedMongoIndex(index))); + const dropMongoIndexConfirmMessage = computed(() => pendingDropMongoIndex.value ? t("contextMenu.confirmDropMongoIndexMessage", { @@ -7784,51 +7803,86 @@ const dropMongoIndexConfirmMessage = computed(() => }) : "", ); +const dropMongoIndexPreview = computed(() => (pendingDropMongoIndex.value ? mongoDropIndexPreview(props.database || "", props.tableMeta?.tableName || "", pendingDropMongoIndex.value.name) : "")); const dropAllMongoIndexesConfirmMessage = computed(() => t("contextMenu.confirmDropMongoAllIndexesMessage", { name: props.tableMeta?.tableName || "" })); const dropAllMongoIndexesConfirmDetails = computed(() => t("contextMenu.confirmDropMongoAllIndexesDetails")); -const dropMongoIndexPreview = computed(() => (pendingDropMongoIndex.value ? `db.getCollection(${JSON.stringify(props.tableMeta?.tableName || "")}).dropIndex(${JSON.stringify(pendingDropMongoIndex.value.name)})` : "")); -const dropAllMongoIndexesPreview = computed(() => `db.getCollection(${JSON.stringify(props.tableMeta?.tableName || "")}).dropIndexes()`); +const dropAllMongoIndexesPreview = computed(() => mongoDropAllIndexesPreview(props.database || "", props.tableMeta?.tableName || "")); function requestDropMongoIndex(index: IndexInfo) { + if (!canManageMongoIndexes.value || isProtectedMongoIndex(index)) return; pendingDropMongoIndex.value = index; showDropMongoIndexConfirm.value = true; } function requestDropAllMongoIndexes() { + if (!canManageMongoIndexes.value || droppableMongoIndexes.value.length === 0) return; showDropAllMongoIndexesConfirm.value = true; } async function confirmDropMongoIndex() { - if (!props.connectionId || !props.database || !props.tableMeta?.tableName || !pendingDropMongoIndex.value || dropMongoIndexLoading.value) return; - dropMongoIndexLoading.value = true; - try { - await connectionStore.ensureConnected(props.connectionId); - await api.mongoDropIndexes(props.connectionId, props.database, props.tableMeta.tableName, JSON.stringify(pendingDropMongoIndex.value.name), true); - toast(t("contextMenu.dropTableChildObjectSuccess", { name: pendingDropMongoIndex.value.name }), 3000); - showDropMongoIndexConfirm.value = false; - pendingDropMongoIndex.value = null; - await reloadIndexes(); - } catch (e: any) { - toast(t("contextMenu.tableOperationFailed", { message: e?.message || String(e) }), 5000); - } finally { - dropMongoIndexLoading.value = false; - } + const index = pendingDropMongoIndex.value; + const connectionId = props.connectionId; + const database = props.database; + const tableName = props.tableMeta?.tableName; + if (!connectionId || !database || !tableName || !index || !canManageMongoIndexes.value || isProtectedMongoIndex(index) || dropMongoIndexLoading.value) return; + await runMongoMutation({ + connection: connectionStore.getConfig(connectionId), + database, + reviewText: dropMongoIndexPreview.value, + source: t("production.sourceDataGrid"), + loading: dropMongoIndexLoading, + beforeExecute: () => connectionStore.ensureConnected(connectionId), + execute: async () => { + try { + return await api.mongoDropIndexes(connectionId, database, tableName, JSON.stringify(index.name), true); + } finally { + await refreshMongoIndexMetadataAfterMutation(); + } + }, + onSuccess: (result) => { + const failed = mongoDropIndexFailureCount(result); + if (failed > 0) { + toast(t("contextMenu.dropIndexesPartialFailure", { success: result.dropped_names.length, failed }), 5000); + } else { + toast(t("contextMenu.dropTableChildObjectSuccess", { name: index.name }), 3000); + } + showDropMongoIndexConfirm.value = false; + pendingDropMongoIndex.value = null; + }, + onError: (e: any) => toast(t("contextMenu.tableOperationFailed", { message: e?.message || String(e) }), 5000), + }); } async function confirmDropAllMongoIndexes() { - if (!props.connectionId || !props.database || !props.tableMeta?.tableName || dropAllMongoIndexesLoading.value) return; - dropAllMongoIndexesLoading.value = true; - try { - await connectionStore.ensureConnected(props.connectionId); - const result = await api.mongoDropIndexes(props.connectionId, props.database, props.tableMeta.tableName, undefined, false); - toast(t("contextMenu.dropAllIndexesSuccess", { count: result.dropped_names.length, name: props.tableMeta.tableName }), 3000); - showDropAllMongoIndexesConfirm.value = false; - await reloadIndexes(); - } catch (e: any) { - toast(t("contextMenu.tableOperationFailed", { message: e?.message || String(e) }), 5000); - } finally { - dropAllMongoIndexesLoading.value = false; - } + const connectionId = props.connectionId; + const database = props.database; + const tableName = props.tableMeta?.tableName; + if (!connectionId || !database || !tableName || !canManageMongoIndexes.value || dropAllMongoIndexesLoading.value) return; + await runMongoMutation({ + connection: connectionStore.getConfig(connectionId), + database, + reviewText: dropAllMongoIndexesPreview.value, + source: t("production.sourceDataGrid"), + loading: dropAllMongoIndexesLoading, + beforeExecute: () => connectionStore.ensureConnected(connectionId), + execute: async () => { + try { + return await api.mongoDropIndexes(connectionId, database, tableName, undefined, false); + } finally { + await refreshMongoIndexMetadataAfterMutation(); + } + }, + onSuccess: (result) => { + const failed = mongoDropIndexFailureCount(result); + if (failed > 0) { + toast(t("contextMenu.dropIndexesPartialFailure", { success: result.dropped_names.length, failed }), 5000); + } else { + toast(t("contextMenu.dropAllIndexesSuccess", { count: result.dropped_names.length, name: tableName }), 3000); + } + showDropAllMongoIndexesConfirm.value = false; + }, + onError: (e: any) => toast(t("contextMenu.tableOperationFailed", { message: e?.message || String(e) }), 5000), + }); } const filteredForeignKeys = computed(() => { @@ -9373,7 +9427,7 @@ const gridContextMenuItems = computed(() => { {{ index.columns.join(", ") }} - diff --git a/apps/desktop/src/components/layout/ContentArea.vue b/apps/desktop/src/components/layout/ContentArea.vue index 59469fd400..c4434f8aca 100644 --- a/apps/desktop/src/components/layout/ContentArea.vue +++ b/apps/desktop/src/components/layout/ContentArea.vue @@ -1843,7 +1843,7 @@ defineExpose({ focusSearch, refreshData, refreshQueryEditorCompletionCache, hand diff --git a/apps/desktop/src/components/sidebar/SidebarTreeItemDialogs.vue b/apps/desktop/src/components/sidebar/SidebarTreeItemDialogs.vue index e40c231b46..5c84c2e0db 100644 --- a/apps/desktop/src/components/sidebar/SidebarTreeItemDialogs.vue +++ b/apps/desktop/src/components/sidebar/SidebarTreeItemDialogs.vue @@ -1,10 +1,12 @@