feat(mongodb): support db.collection.renameCollection()
This commit is contained in:
1 parent
392138f59a
commit
2db28fc236
16 files changed
+239
-17
No files matched your search
@@ -75,6 +75,7 @@ public final class AgentProtocol {
|
|||||||
public static final String MONGO_METHOD_UPDATE_DOCUMENTS = "update_documents";
|
public static final String MONGO_METHOD_UPDATE_DOCUMENTS = "update_documents";
|
||||||
public static final String MONGO_METHOD_REPLACE_DOCUMENT = "replace_document";
|
public static final String MONGO_METHOD_REPLACE_DOCUMENT = "replace_document";
|
||||||
public static final String MONGO_METHOD_BULK_WRITE = "bulk_write";
|
public static final String MONGO_METHOD_BULK_WRITE = "bulk_write";
|
||||||
|
public static final String MONGO_METHOD_RENAME_COLLECTION = "rename_collection";
|
||||||
public static final String MONGO_METHOD_DELETE_DOCUMENT = "delete_document";
|
public static final String MONGO_METHOD_DELETE_DOCUMENT = "delete_document";
|
||||||
public static final String MONGO_METHOD_DELETE_DOCUMENTS = "delete_documents";
|
public static final String MONGO_METHOD_DELETE_DOCUMENTS = "delete_documents";
|
||||||
public static final String MONGO_METHOD_RUN_COMMAND = "run_command";
|
public static final String MONGO_METHOD_RUN_COMMAND = "run_command";
|
||||||
@@ -134,6 +135,7 @@ public final class AgentProtocol {
|
|||||||
public static final String CAPABILITY_MONGO_INSERT_DOCUMENTS = "mongo_insert_documents";
|
public static final String CAPABILITY_MONGO_INSERT_DOCUMENTS = "mongo_insert_documents";
|
||||||
public static final String CAPABILITY_MONGO_REPLACE_DOCUMENT = "mongo_replace_document";
|
public static final String CAPABILITY_MONGO_REPLACE_DOCUMENT = "mongo_replace_document";
|
||||||
public static final String CAPABILITY_MONGO_BULK_WRITE = "mongo_bulk_write";
|
public static final String CAPABILITY_MONGO_BULK_WRITE = "mongo_bulk_write";
|
||||||
|
public static final String CAPABILITY_MONGO_RENAME_COLLECTION = "mongo_rename_collection";
|
||||||
public static final String CAPABILITY_MONGO_FIND_CURSOR = "mongo_find_cursor";
|
public static final String CAPABILITY_MONGO_FIND_CURSOR = "mongo_find_cursor";
|
||||||
public static final String CAPABILITY_MULTI_SESSION = "multi_session";
|
public static final String CAPABILITY_MULTI_SESSION = "multi_session";
|
||||||
public static final String CAPABILITY_STRUCTURED_ERROR_V1 = "structured_error_v1";
|
public static final String CAPABILITY_STRUCTURED_ERROR_V1 = "structured_error_v1";
|
||||||
@@ -173,6 +175,7 @@ public final class AgentProtocol {
|
|||||||
CAPABILITY_MONGO_INSERT_DOCUMENTS,
|
CAPABILITY_MONGO_INSERT_DOCUMENTS,
|
||||||
CAPABILITY_MONGO_REPLACE_DOCUMENT,
|
CAPABILITY_MONGO_REPLACE_DOCUMENT,
|
||||||
CAPABILITY_MONGO_BULK_WRITE,
|
CAPABILITY_MONGO_BULK_WRITE,
|
||||||
|
CAPABILITY_MONGO_RENAME_COLLECTION,
|
||||||
CAPABILITY_MONGO_FIND_CURSOR
|
CAPABILITY_MONGO_FIND_CURSOR
|
||||||
));
|
));
|
||||||
|
|
||||||
@@ -239,6 +242,7 @@ public final class AgentProtocol {
|
|||||||
mongoCapabilities.add(CAPABILITY_MONGO_INSERT_DOCUMENTS);
|
mongoCapabilities.add(CAPABILITY_MONGO_INSERT_DOCUMENTS);
|
||||||
mongoCapabilities.add(CAPABILITY_MONGO_REPLACE_DOCUMENT);
|
mongoCapabilities.add(CAPABILITY_MONGO_REPLACE_DOCUMENT);
|
||||||
mongoCapabilities.add(CAPABILITY_MONGO_BULK_WRITE);
|
mongoCapabilities.add(CAPABILITY_MONGO_BULK_WRITE);
|
||||||
|
mongoCapabilities.add(CAPABILITY_MONGO_RENAME_COLLECTION);
|
||||||
mongoCapabilities.add(CAPABILITY_MONGO_FIND_CURSOR);
|
mongoCapabilities.add(CAPABILITY_MONGO_FIND_CURSOR);
|
||||||
MONGO_LEGACY_CAPABILITIES = Collections.unmodifiableList(mongoCapabilities);
|
MONGO_LEGACY_CAPABILITIES = Collections.unmodifiableList(mongoCapabilities);
|
||||||
|
|
||||||
@@ -249,6 +253,7 @@ public final class AgentProtocol {
|
|||||||
mongoMultiSessionCapabilities.add(CAPABILITY_MONGO_INSERT_DOCUMENTS);
|
mongoMultiSessionCapabilities.add(CAPABILITY_MONGO_INSERT_DOCUMENTS);
|
||||||
mongoMultiSessionCapabilities.add(CAPABILITY_MONGO_REPLACE_DOCUMENT);
|
mongoMultiSessionCapabilities.add(CAPABILITY_MONGO_REPLACE_DOCUMENT);
|
||||||
mongoMultiSessionCapabilities.add(CAPABILITY_MONGO_BULK_WRITE);
|
mongoMultiSessionCapabilities.add(CAPABILITY_MONGO_BULK_WRITE);
|
||||||
|
mongoMultiSessionCapabilities.add(CAPABILITY_MONGO_RENAME_COLLECTION);
|
||||||
mongoMultiSessionCapabilities.add(CAPABILITY_MONGO_FIND_CURSOR);
|
mongoMultiSessionCapabilities.add(CAPABILITY_MONGO_FIND_CURSOR);
|
||||||
MONGO_LEGACY_MULTI_SESSION_CAPABILITIES = Collections.unmodifiableList(mongoMultiSessionCapabilities);
|
MONGO_LEGACY_MULTI_SESSION_CAPABILITIES = Collections.unmodifiableList(mongoMultiSessionCapabilities);
|
||||||
|
|
||||||
@@ -293,6 +298,7 @@ public final class AgentProtocol {
|
|||||||
MONGO_METHOD_UPDATE_DOCUMENTS,
|
MONGO_METHOD_UPDATE_DOCUMENTS,
|
||||||
MONGO_METHOD_REPLACE_DOCUMENT,
|
MONGO_METHOD_REPLACE_DOCUMENT,
|
||||||
MONGO_METHOD_BULK_WRITE,
|
MONGO_METHOD_BULK_WRITE,
|
||||||
|
MONGO_METHOD_RENAME_COLLECTION,
|
||||||
MONGO_METHOD_DELETE_DOCUMENT,
|
MONGO_METHOD_DELETE_DOCUMENT,
|
||||||
MONGO_METHOD_DELETE_DOCUMENTS,
|
MONGO_METHOD_DELETE_DOCUMENTS,
|
||||||
MONGO_METHOD_RUN_COMMAND,
|
MONGO_METHOD_RUN_COMMAND,
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
"protocolVersion": 1,
|
"protocolVersion": 1,
|
||||||
"handshakeMethod": "handshake",
|
"handshakeMethod": "handshake",
|
||||||
"handshakeResponseFields": ["protocolVersion", "agentProtocolVersion", "capabilities"],
|
"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", "mongo_drop_database", "mongo_clone_collection", "mongo_run_command", "mongo_insert_documents", "mongo_replace_document", "mongo_bulk_write", "mongo_find_cursor"],
|
"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", "mongo_clone_collection", "mongo_run_command", "mongo_insert_documents", "mongo_replace_document", "mongo_bulk_write", "mongo_rename_collection", "mongo_find_cursor"],
|
||||||
"capabilities": ["connect", "test_connection", "metadata", "query", "paged_query", "transaction", "ddl"],
|
"capabilities": ["connect", "test_connection", "metadata", "query", "paged_query", "transaction", "ddl"],
|
||||||
"defaultSqlCapabilities": ["connect", "test_connection", "metadata", "query", "paged_query", "transaction", "ddl"],
|
"defaultSqlCapabilities": ["connect", "test_connection", "metadata", "query", "paged_query", "transaction", "ddl"],
|
||||||
"commonMethods": [
|
"commonMethods": [
|
||||||
@@ -42,6 +42,6 @@
|
|||||||
"disconnect",
|
"disconnect",
|
||||||
"shutdown"
|
"shutdown"
|
||||||
],
|
],
|
||||||
"mongoLegacyMethods": ["list_databases", "list_collections", "find_documents", "find_one", "explain_find", "aggregate_documents", "find_documents_extended_json", "count_documents", "server_version", "create_index", "create_user", "drop_indexes", "drop_collection", "clone_collection", "drop_database", "insert_document", "insert_documents", "update_document", "update_documents", "replace_document", "bulk_write", "delete_document", "delete_documents", "run_command", "start_find_cursor", "fetch_find_cursor", "close_find_cursor"],
|
"mongoLegacyMethods": ["list_databases", "list_collections", "find_documents", "find_one", "explain_find", "aggregate_documents", "find_documents_extended_json", "count_documents", "server_version", "create_index", "create_user", "drop_indexes", "drop_collection", "clone_collection", "drop_database", "insert_document", "insert_documents", "update_document", "update_documents", "replace_document", "bulk_write", "rename_collection", "delete_document", "delete_documents", "run_command", "start_find_cursor", "fetch_find_cursor", "close_find_cursor"],
|
||||||
"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"]
|
"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"]
|
||||||
}
|
}
|
||||||
@@ -2,7 +2,7 @@
|
|||||||
"protocolVersion": 2,
|
"protocolVersion": 2,
|
||||||
"handshakeMethod": "handshake",
|
"handshakeMethod": "handshake",
|
||||||
"handshakeResponseFields": ["protocolVersion", "agentProtocolVersion", "capabilities"],
|
"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", "mongo_drop_database", "mongo_clone_collection", "mongo_run_command", "mongo_insert_documents", "mongo_replace_document", "mongo_bulk_write", "mongo_find_cursor", "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", "mongo_clone_collection", "mongo_run_command", "mongo_insert_documents", "mongo_replace_document", "mongo_bulk_write", "mongo_rename_collection", "mongo_find_cursor", "multi_session", "structured_error_v1"],
|
||||||
"capabilities": ["connect", "test_connection", "metadata", "query", "paged_query", "transaction", "ddl", "multi_session"],
|
"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"],
|
"defaultSqlCapabilities": ["connect", "test_connection", "metadata", "query", "paged_query", "transaction", "ddl", "multi_session", "structured_error_v1"],
|
||||||
"commonMethods": [
|
"commonMethods": [
|
||||||
@@ -46,7 +46,7 @@
|
|||||||
"disconnect",
|
"disconnect",
|
||||||
"shutdown"
|
"shutdown"
|
||||||
],
|
],
|
||||||
"mongoLegacyMethods": ["list_databases", "list_collections", "find_documents", "find_one", "explain_find", "aggregate_documents", "find_documents_extended_json", "count_documents", "server_version", "create_index", "create_user", "drop_indexes", "drop_collection", "clone_collection", "drop_database", "insert_document", "insert_documents", "update_document", "update_documents", "replace_document", "bulk_write", "delete_document", "delete_documents", "run_command", "start_find_cursor", "fetch_find_cursor", "close_find_cursor"],
|
"mongoLegacyMethods": ["list_databases", "list_collections", "find_documents", "find_one", "explain_find", "aggregate_documents", "find_documents_extended_json", "count_documents", "server_version", "create_index", "create_user", "drop_indexes", "drop_collection", "clone_collection", "drop_database", "insert_document", "insert_documents", "update_document", "update_documents", "replace_document", "bulk_write", "rename_collection", "delete_document", "delete_documents", "run_command", "start_find_cursor", "fetch_find_cursor", "close_find_cursor"],
|
||||||
"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"],
|
"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",
|
"sessionField": "agentSessionId",
|
||||||
"cursorSessionField": "sessionId"
|
"cursorSessionField": "sessionId"
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import com.google.gson.JsonObject;
|
|||||||
import com.google.gson.JsonParser;
|
import com.google.gson.JsonParser;
|
||||||
import com.mongodb.ConnectionString;
|
import com.mongodb.ConnectionString;
|
||||||
import com.mongodb.MongoBulkWriteException;
|
import com.mongodb.MongoBulkWriteException;
|
||||||
|
import com.mongodb.MongoNamespace;
|
||||||
import com.mongodb.MongoCredential;
|
import com.mongodb.MongoCredential;
|
||||||
import com.mongodb.MongoClientSettings;
|
import com.mongodb.MongoClientSettings;
|
||||||
import com.mongodb.ServerAddress;
|
import com.mongodb.ServerAddress;
|
||||||
@@ -1166,6 +1167,32 @@ public final class MongoAgent {
|
|||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static Object renameCollection(JsonObject params) {
|
||||||
|
MongoClient c = requireClient();
|
||||||
|
String database = params.get("database").getAsString();
|
||||||
|
String collection = params.get("collection").getAsString();
|
||||||
|
String newName = params.get("new_name").getAsString();
|
||||||
|
requireRenameableCollectionNames(collection, newName);
|
||||||
|
c.getDatabase(database).getCollection(collection).renameCollection(new MongoNamespace(database, newName));
|
||||||
|
return Collections.singletonMap("ok", true);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Mirrors the native driver's checks so both paths reject the same names before asking the server. */
|
||||||
|
static void requireRenameableCollectionNames(String collection, String newName) {
|
||||||
|
if (collection == null || collection.isBlank()) {
|
||||||
|
throw new IllegalArgumentException("Collection name is required");
|
||||||
|
}
|
||||||
|
if (newName == null || newName.isBlank()) {
|
||||||
|
throw new IllegalArgumentException("New collection name is required");
|
||||||
|
}
|
||||||
|
if (collection.equals(newName)) {
|
||||||
|
throw new IllegalArgumentException("New collection name must differ from the current name");
|
||||||
|
}
|
||||||
|
if (collection.startsWith("system.") || newName.startsWith("system.")) {
|
||||||
|
throw new IllegalArgumentException("System collections cannot be renamed");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private static Object dropCollection(JsonObject params) {
|
private static Object dropCollection(JsonObject params) {
|
||||||
MongoClient c = requireClient();
|
MongoClient c = requireClient();
|
||||||
String database = params.get("database").getAsString();
|
String database = params.get("database").getAsString();
|
||||||
@@ -2280,6 +2307,7 @@ public final class MongoAgent {
|
|||||||
case AgentProtocol.MONGO_METHOD_UPDATE_DOCUMENTS -> updateDocuments(params);
|
case AgentProtocol.MONGO_METHOD_UPDATE_DOCUMENTS -> updateDocuments(params);
|
||||||
case AgentProtocol.MONGO_METHOD_REPLACE_DOCUMENT -> replaceDocument(params);
|
case AgentProtocol.MONGO_METHOD_REPLACE_DOCUMENT -> replaceDocument(params);
|
||||||
case AgentProtocol.MONGO_METHOD_BULK_WRITE -> bulkWrite(params);
|
case AgentProtocol.MONGO_METHOD_BULK_WRITE -> bulkWrite(params);
|
||||||
|
case AgentProtocol.MONGO_METHOD_RENAME_COLLECTION -> renameCollection(params);
|
||||||
case AgentProtocol.MONGO_METHOD_DELETE_DOCUMENT -> deleteDocument(params);
|
case AgentProtocol.MONGO_METHOD_DELETE_DOCUMENT -> deleteDocument(params);
|
||||||
case AgentProtocol.MONGO_METHOD_DELETE_DOCUMENTS -> deleteDocuments(params);
|
case AgentProtocol.MONGO_METHOD_DELETE_DOCUMENTS -> deleteDocuments(params);
|
||||||
case AgentProtocol.MONGO_METHOD_RUN_COMMAND -> runCommand(params);
|
case AgentProtocol.MONGO_METHOD_RUN_COMMAND -> runCommand(params);
|
||||||
|
|||||||
@@ -1387,6 +1387,34 @@ class MongoAgentTest {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void renameCollectionMethodIsRecognizedOverJsonRpc() {
|
||||||
|
String response = MongoAgent.handleRequest(
|
||||||
|
"{\"jsonrpc\":\"2.0\",\"id\":13,\"method\":\"rename_collection\","
|
||||||
|
+ "\"params\":{\"database\":\"app\",\"collection\":\"orders\",\"new_name\":\"orders_2024\"}}");
|
||||||
|
|
||||||
|
JsonObject json = JsonParser.parseString(response).getAsJsonObject();
|
||||||
|
assertEquals(13, json.get("id").getAsInt());
|
||||||
|
assertEquals("Not connected", json.getAsJsonObject("error").get("message").getAsString());
|
||||||
|
assertTrue(AgentProtocol.MONGO_LEGACY_METHODS.contains(AgentProtocol.MONGO_METHOD_RENAME_COLLECTION));
|
||||||
|
assertTrue(AgentProtocol.MONGO_LEGACY_CAPABILITIES.contains(AgentProtocol.CAPABILITY_MONGO_RENAME_COLLECTION));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void rejectsUnrenameableCollectionNames() {
|
||||||
|
MongoAgent.requireRenameableCollectionNames("orders", "orders_2024");
|
||||||
|
for (String[] item : new String[][] {
|
||||||
|
{"", "x", "Collection name is required"},
|
||||||
|
{"orders", "", "New collection name is required"},
|
||||||
|
{"orders", "orders", "must differ"},
|
||||||
|
{"system.users", "x", "System collections cannot be renamed"},
|
||||||
|
{"orders", "system.x", "System collections cannot be renamed"},
|
||||||
|
}) {
|
||||||
|
IllegalArgumentException error = assertThrows(IllegalArgumentException.class, () -> MongoAgent.requireRenameableCollectionNames(item[0], item[1]));
|
||||||
|
assertTrue(error.getMessage().contains(item[2]), error.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void parsesBulkWriteOptions() {
|
void parsesBulkWriteOptions() {
|
||||||
assertTrue(MongoAgent.bulkWriteOptionsForWrite(null).isOrdered());
|
assertTrue(MongoAgent.bulkWriteOptionsForWrite(null).isOrdered());
|
||||||
|
|||||||
@@ -108,6 +108,7 @@ const COLLECTION_METHODS = [
|
|||||||
{ label: "createIndex", detail: "Create an index", apply: "createIndex({ ${field}: 1 })" },
|
{ label: "createIndex", detail: "Create an index", apply: "createIndex({ ${field}: 1 })" },
|
||||||
{ label: "dropIndex", detail: "Drop one index", apply: 'dropIndex("${indexName}")' },
|
{ label: "dropIndex", detail: "Drop one index", apply: 'dropIndex("${indexName}")' },
|
||||||
{ label: "dropIndexes", detail: "Drop collection indexes", apply: "dropIndexes()" },
|
{ label: "dropIndexes", detail: "Drop collection indexes", apply: "dropIndexes()" },
|
||||||
|
{ label: "renameCollection", detail: "Rename the collection", apply: 'renameCollection("${newName}")' },
|
||||||
{ label: "drop", detail: "Drop the collection", apply: "drop()" },
|
{ label: "drop", detail: "Drop the collection", apply: "drop()" },
|
||||||
] as const;
|
] as const;
|
||||||
|
|
||||||
@@ -136,6 +137,7 @@ const COLLECTION_METHOD_BOOST: Record<(typeof COLLECTION_METHODS)[number]["label
|
|||||||
dataSize: 75,
|
dataSize: 75,
|
||||||
storageSize: 70,
|
storageSize: 70,
|
||||||
totalIndexSize: 65,
|
totalIndexSize: 65,
|
||||||
|
renameCollection: 55,
|
||||||
dropIndex: 50,
|
dropIndex: 50,
|
||||||
dropIndexes: 45,
|
dropIndexes: 45,
|
||||||
drop: 30,
|
drop: 30,
|
||||||
|
|||||||
@@ -50,6 +50,7 @@ const COLLECTION_METHOD_SHAPES: Record<string, MongoMethodShape> = {
|
|||||||
updateMany: { expects: "a filter, an update, and optional options", roles: ["filter", "update", "options"] },
|
updateMany: { expects: "a filter, an update, and optional options", roles: ["filter", "update", "options"] },
|
||||||
replaceOne: { expects: "a filter, a replacement document, and optional options", roles: ["filter", "replacement", "options"] },
|
replaceOne: { expects: "a filter, a replacement document, and optional options", roles: ["filter", "replacement", "options"] },
|
||||||
bulkWrite: { expects: "an array of operations and optional options", roles: ["operations", "options"] },
|
bulkWrite: { expects: "an array of operations and optional options", roles: ["operations", "options"] },
|
||||||
|
renameCollection: { expects: "the new collection name", roles: ["newName"] },
|
||||||
deleteOne: { expects: "a filter", roles: ["filter"] },
|
deleteOne: { expects: "a filter", roles: ["filter"] },
|
||||||
deleteMany: { expects: "a filter", roles: ["filter"] },
|
deleteMany: { expects: "a filter", roles: ["filter"] },
|
||||||
findOneAndUpdate: { expects: "a filter, an update, and optional options", roles: ["filter", "update", "options"] },
|
findOneAndUpdate: { expects: "a filter, an update, and optional options", roles: ["filter", "update", "options"] },
|
||||||
@@ -80,6 +81,7 @@ const SUPPORTED_COLLECTION_METHODS = [
|
|||||||
"updateMany",
|
"updateMany",
|
||||||
"replaceOne",
|
"replaceOne",
|
||||||
"bulkWrite",
|
"bulkWrite",
|
||||||
|
"renameCollection",
|
||||||
"deleteOne",
|
"deleteOne",
|
||||||
"deleteMany",
|
"deleteMany",
|
||||||
"findOneAndUpdate",
|
"findOneAndUpdate",
|
||||||
@@ -168,6 +170,12 @@ function diagnoseMongoCommand(source: string): string | null {
|
|||||||
return `${method}() returns a result, not a cursor, so nothing can be chained after it: "${tail.length > 40 ? `${tail.slice(0, 40)}…` : tail}".`;
|
return `${method}() returns a result, not a cursor, so nothing can be chained after it: "${tail.length > 40 ? `${tail.slice(0, 40)}…` : tail}".`;
|
||||||
}
|
}
|
||||||
if (tail) return `Unexpected text after ${method}(...): "${tail.length > 40 ? `${tail.slice(0, 40)}…` : tail}".`;
|
if (tail) return `Unexpected text after ${method}(...): "${tail.length > 40 ? `${tail.slice(0, 40)}…` : tail}".`;
|
||||||
|
if (method === "renameCollection") {
|
||||||
|
if (args.length > 1) return "renameCollection() dropTarget is not supported; drop the target collection first.";
|
||||||
|
const newName = args[0] ? /^(["'])([^"']*)\1$/.exec(args[0].trim())?.[2] : undefined;
|
||||||
|
if (newName !== undefined && (!newName.trim() || newName.includes("$"))) return "renameCollection() requires a valid collection name.";
|
||||||
|
if (newName !== undefined && shape.groups?.collection === newName) return "renameCollection() new name must differ from the current name.";
|
||||||
|
}
|
||||||
if (method === "bulkWrite" && args[0]) {
|
if (method === "bulkWrite" && args[0]) {
|
||||||
const operations = normalizeJsonArgument(args[0]);
|
const operations = normalizeJsonArgument(args[0]);
|
||||||
const problem = operations ? validateBulkWriteOperations(operations) : null;
|
const problem = operations ? validateBulkWriteOperations(operations) : null;
|
||||||
@@ -287,7 +295,7 @@ export interface MongoDistinctCommand {
|
|||||||
filter?: string;
|
filter?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
type MongoWriteKind = "runCommand" | "insert" | "update" | "replace" | "bulkWrite" | "delete" | "createIndex" | "createUser" | "dropIndex" | "dropIndexes" | "dropCollection" | "findOneAndUpdate" | "findOneAndReplace" | "findOneAndDelete";
|
type MongoWriteKind = "runCommand" | "insert" | "update" | "replace" | "bulkWrite" | "delete" | "createIndex" | "createUser" | "dropIndex" | "dropIndexes" | "dropCollection" | "renameCollection" | "findOneAndUpdate" | "findOneAndReplace" | "findOneAndDelete";
|
||||||
|
|
||||||
export type MongoCommand =
|
export type MongoCommand =
|
||||||
| ({ kind: "find" } & MongoFindCommand)
|
| ({ kind: "find" } & MongoFindCommand)
|
||||||
@@ -312,6 +320,7 @@ export type MongoCommand =
|
|||||||
| { kind: "dropIndex"; collection: string; index: string }
|
| { kind: "dropIndex"; collection: string; index: string }
|
||||||
| { kind: "dropIndexes"; collection: string; indexes?: string }
|
| { kind: "dropIndexes"; collection: string; indexes?: string }
|
||||||
| { kind: "dropCollection"; collection: string }
|
| { kind: "dropCollection"; collection: string }
|
||||||
|
| { kind: "renameCollection"; collection: string; newName: string }
|
||||||
| { kind: "findOneAndUpdate"; collection: string; filter: string; update: string; options?: string }
|
| { kind: "findOneAndUpdate"; collection: string; filter: string; update: string; options?: string }
|
||||||
| { kind: "findOneAndReplace"; collection: string; filter: string; replacement: string; options?: string }
|
| { kind: "findOneAndReplace"; collection: string; filter: string; replacement: string; options?: string }
|
||||||
| { kind: "findOneAndDelete"; collection: string; filter: string; options?: string };
|
| { kind: "findOneAndDelete"; collection: string; filter: string; options?: string };
|
||||||
@@ -889,6 +898,17 @@ export function parseMongoWriteCommand(input: string): MongoWriteCommand | null
|
|||||||
return indexes !== null ? { kind: "dropIndexes", collection: dropIndexes.collection, ...(indexes ? { indexes } : {}) } : null;
|
return indexes !== null ? { kind: "dropIndexes", collection: dropIndexes.collection, ...(indexes ? { indexes } : {}) } : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const renameCollection = parseCollectionMethodTarget(source, "renameCollection");
|
||||||
|
if (renameCollection) {
|
||||||
|
const args = parseMethodArgs(source, renameCollection.methodCallIndex);
|
||||||
|
// A second argument would be dropTarget, which deletes an existing collection of the
|
||||||
|
// new name; it is rejected (see the diagnostics) rather than silently ignored.
|
||||||
|
if (!args || args.length !== 1) return null;
|
||||||
|
const newName = /^(["'])([^"'$]+)\1$/.exec(args[0]!.trim())?.[2]?.trim();
|
||||||
|
if (!newName || newName === renameCollection.collection) return null;
|
||||||
|
return { kind: "renameCollection", collection: renameCollection.collection, newName };
|
||||||
|
}
|
||||||
|
|
||||||
const dropCollection = parseCollectionMethodTarget(source, "drop");
|
const dropCollection = parseCollectionMethodTarget(source, "drop");
|
||||||
if (dropCollection) {
|
if (dropCollection) {
|
||||||
const args = parseMethodArgs(source, dropCollection.methodCallIndex);
|
const args = parseMethodArgs(source, dropCollection.methodCallIndex);
|
||||||
@@ -1184,6 +1204,15 @@ export function mongoUseToQueryResult(database: string, executionTimeMs: number)
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function mongoScalarToQueryResult(column: string, value: string, executionTimeMs: number): QueryResult {
|
||||||
|
return {
|
||||||
|
columns: [column],
|
||||||
|
rows: [[value]],
|
||||||
|
affected_rows: 1,
|
||||||
|
execution_time_ms: Math.max(0, Math.round(executionTimeMs)),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
export function mongoVersionToQueryResult(version: string, executionTimeMs: number): QueryResult {
|
export function mongoVersionToQueryResult(version: string, executionTimeMs: number): QueryResult {
|
||||||
return {
|
return {
|
||||||
columns: ["version"],
|
columns: ["version"],
|
||||||
|
|||||||
@@ -33,6 +33,7 @@ import {
|
|||||||
mongoUseToQueryResult,
|
mongoUseToQueryResult,
|
||||||
mongoVersionToQueryResult,
|
mongoVersionToQueryResult,
|
||||||
mongoBulkWriteToQueryResult,
|
mongoBulkWriteToQueryResult,
|
||||||
|
mongoScalarToQueryResult,
|
||||||
mongoWriteToQueryResult,
|
mongoWriteToQueryResult,
|
||||||
splitMongoCommandRanges,
|
splitMongoCommandRanges,
|
||||||
type MongoAggregateSafetyOptions,
|
type MongoAggregateSafetyOptions,
|
||||||
@@ -6745,6 +6746,7 @@ export const useQueryStore = defineStore("query", () => {
|
|||||||
case "createUser":
|
case "createUser":
|
||||||
case "dropIndex":
|
case "dropIndex":
|
||||||
case "dropIndexes":
|
case "dropIndexes":
|
||||||
|
case "renameCollection":
|
||||||
case "dropCollection": {
|
case "dropCollection": {
|
||||||
if (options?.mongoSafety) {
|
if (options?.mongoSafety) {
|
||||||
const safety = evaluateMongoWriteSafety(mongoCommand, options.mongoSafety);
|
const safety = evaluateMongoWriteSafety(mongoCommand, options.mongoSafety);
|
||||||
@@ -6782,6 +6784,9 @@ export const useQueryStore = defineStore("query", () => {
|
|||||||
} finally {
|
} finally {
|
||||||
await refreshLoadedMongoIndexesAfterMutation(executionConnectionId, currentDatabase, mongoCommand.collection, traceId);
|
await refreshLoadedMongoIndexesAfterMutation(executionConnectionId, currentDatabase, mongoCommand.collection, traceId);
|
||||||
}
|
}
|
||||||
|
} else if (mongoCommand.kind === "renameCollection") {
|
||||||
|
await api.mongoRenameCollection(executionConnectionId, currentDatabase, mongoCommand.collection, mongoCommand.newName);
|
||||||
|
allResults.push(markQueryResultRowsRaw(annotateMongoResult(mongoScalarToQueryResult("renamed", `${mongoCommand.collection} -> ${mongoCommand.newName}`, performance.now() - commandStartedAt))));
|
||||||
} else if (mongoCommand.kind === "dropCollection") {
|
} else if (mongoCommand.kind === "dropCollection") {
|
||||||
await api.mongoDropCollection(executionConnectionId, currentDatabase, mongoCommand.collection);
|
await api.mongoDropCollection(executionConnectionId, currentDatabase, mongoCommand.collection);
|
||||||
allResults.push(markQueryResultRowsRaw(annotateMongoResult(mongoWriteToQueryResult(1, performance.now() - commandStartedAt))));
|
allResults.push(markQueryResultRowsRaw(annotateMongoResult(mongoWriteToQueryResult(1, performance.now() - commandStartedAt))));
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
"protocolVersion": 1,
|
"protocolVersion": 1,
|
||||||
"handshakeMethod": "handshake",
|
"handshakeMethod": "handshake",
|
||||||
"handshakeResponseFields": ["protocolVersion", "agentProtocolVersion", "capabilities"],
|
"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", "mongo_drop_database", "mongo_clone_collection", "mongo_run_command", "mongo_insert_documents", "mongo_replace_document", "mongo_bulk_write", "mongo_find_cursor"],
|
"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", "mongo_clone_collection", "mongo_run_command", "mongo_insert_documents", "mongo_replace_document", "mongo_bulk_write", "mongo_rename_collection", "mongo_find_cursor"],
|
||||||
"capabilities": ["connect", "test_connection", "metadata", "query", "paged_query", "transaction", "ddl"],
|
"capabilities": ["connect", "test_connection", "metadata", "query", "paged_query", "transaction", "ddl"],
|
||||||
"defaultSqlCapabilities": ["connect", "test_connection", "metadata", "query", "paged_query", "transaction", "ddl"],
|
"defaultSqlCapabilities": ["connect", "test_connection", "metadata", "query", "paged_query", "transaction", "ddl"],
|
||||||
"commonMethods": [
|
"commonMethods": [
|
||||||
@@ -42,6 +42,6 @@
|
|||||||
"disconnect",
|
"disconnect",
|
||||||
"shutdown"
|
"shutdown"
|
||||||
],
|
],
|
||||||
"mongoLegacyMethods": ["list_databases", "list_collections", "find_documents", "find_one", "explain_find", "aggregate_documents", "find_documents_extended_json", "count_documents", "server_version", "create_index", "create_user", "drop_indexes", "drop_collection", "clone_collection", "drop_database", "insert_document", "insert_documents", "update_document", "update_documents", "replace_document", "bulk_write", "delete_document", "delete_documents", "run_command", "start_find_cursor", "fetch_find_cursor", "close_find_cursor"],
|
"mongoLegacyMethods": ["list_databases", "list_collections", "find_documents", "find_one", "explain_find", "aggregate_documents", "find_documents_extended_json", "count_documents", "server_version", "create_index", "create_user", "drop_indexes", "drop_collection", "clone_collection", "drop_database", "insert_document", "insert_documents", "update_document", "update_documents", "replace_document", "bulk_write", "rename_collection", "delete_document", "delete_documents", "run_command", "start_find_cursor", "fetch_find_cursor", "close_find_cursor"],
|
||||||
"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"]
|
"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"]
|
||||||
}
|
}
|
||||||
@@ -95,7 +95,23 @@ pub async fn mongo_rename_collection_core(
|
|||||||
let pool = state.pool_handle(connection_id).await.ok_or("Not found")?;
|
let pool = state.pool_handle(connection_id).await.ok_or("Not found")?;
|
||||||
match &pool {
|
match &pool {
|
||||||
PoolKind::MongoDb(client) => mongo_driver::rename_collection(client, database, collection, new_name).await,
|
PoolKind::MongoDb(client) => mongo_driver::rename_collection(client, database, collection, new_name).await,
|
||||||
PoolKind::Agent(_) => Err("MongoDB legacy agent does not support rename collection".to_string()),
|
PoolKind::Agent(client) => {
|
||||||
|
let mut client = client.lock().await;
|
||||||
|
if !client.supports_capability(AgentCapability::MongoRenameCollection) {
|
||||||
|
return Err(
|
||||||
|
"MongoDB Legacy Agent does not support renameCollection; upgrade or reinstall the MongoDB Legacy driver"
|
||||||
|
.to_string(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
let _: serde_json::Value = client
|
||||||
|
.mongo_rename_collection(serde_json::json!({
|
||||||
|
"database": database,
|
||||||
|
"collection": collection,
|
||||||
|
"new_name": new_name,
|
||||||
|
}))
|
||||||
|
.await?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
_ => Err("Not a MongoDB connection".to_string()),
|
_ => Err("Not a MongoDB connection".to_string()),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1200,6 +1216,10 @@ pub async fn execute_mongo_command_core(
|
|||||||
mongo_drop_collection_core(state, connection_id, database, collection).await?;
|
mongo_drop_collection_core(state, connection_id, database, collection).await?;
|
||||||
Ok(affected_query_result(1))
|
Ok(affected_query_result(1))
|
||||||
}
|
}
|
||||||
|
MongoCommand::RenameCollection { collection, new_name } => {
|
||||||
|
mongo_rename_collection_core(state, connection_id, database, collection, new_name).await?;
|
||||||
|
Ok(scalar_query_result("renamed", Value::String(format!("{collection} -> {new_name}"))))
|
||||||
|
}
|
||||||
MongoCommand::FindOneAndUpdate { collection, filter, update, options } => {
|
MongoCommand::FindOneAndUpdate { collection, filter, update, options } => {
|
||||||
let result = mongo_find_one_and_update_core(
|
let result = mongo_find_one_and_update_core(
|
||||||
state,
|
state,
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
"protocolVersion": 2,
|
"protocolVersion": 2,
|
||||||
"handshakeMethod": "handshake",
|
"handshakeMethod": "handshake",
|
||||||
"handshakeResponseFields": ["protocolVersion", "agentProtocolVersion", "capabilities"],
|
"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", "mongo_drop_database", "mongo_clone_collection", "mongo_run_command", "mongo_insert_documents", "mongo_replace_document", "mongo_bulk_write", "mongo_find_cursor", "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", "mongo_clone_collection", "mongo_run_command", "mongo_insert_documents", "mongo_replace_document", "mongo_bulk_write", "mongo_rename_collection", "mongo_find_cursor", "multi_session", "structured_error_v1"],
|
||||||
"capabilities": ["connect", "test_connection", "metadata", "query", "paged_query", "transaction", "ddl", "multi_session"],
|
"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"],
|
"defaultSqlCapabilities": ["connect", "test_connection", "metadata", "query", "paged_query", "transaction", "ddl", "multi_session", "structured_error_v1"],
|
||||||
"commonMethods": [
|
"commonMethods": [
|
||||||
@@ -46,7 +46,7 @@
|
|||||||
"disconnect",
|
"disconnect",
|
||||||
"shutdown"
|
"shutdown"
|
||||||
],
|
],
|
||||||
"mongoLegacyMethods": ["list_databases", "list_collections", "find_documents", "find_one", "explain_find", "aggregate_documents", "find_documents_extended_json", "count_documents", "server_version", "create_index", "create_user", "drop_indexes", "drop_collection", "clone_collection", "drop_database", "insert_document", "insert_documents", "update_document", "update_documents", "replace_document", "bulk_write", "delete_document", "delete_documents", "run_command", "start_find_cursor", "fetch_find_cursor", "close_find_cursor"],
|
"mongoLegacyMethods": ["list_databases", "list_collections", "find_documents", "find_one", "explain_find", "aggregate_documents", "find_documents_extended_json", "count_documents", "server_version", "create_index", "create_user", "drop_indexes", "drop_collection", "clone_collection", "drop_database", "insert_document", "insert_documents", "update_document", "update_documents", "replace_document", "bulk_write", "rename_collection", "delete_document", "delete_documents", "run_command", "start_find_cursor", "fetch_find_cursor", "close_find_cursor"],
|
||||||
"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"],
|
"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",
|
"sessionField": "agentSessionId",
|
||||||
"cursorSessionField": "sessionId"
|
"cursorSessionField": "sessionId"
|
||||||
|
|||||||
@@ -1043,6 +1043,7 @@ pub enum AgentCapability {
|
|||||||
MongoInsertDocuments,
|
MongoInsertDocuments,
|
||||||
MongoReplaceDocument,
|
MongoReplaceDocument,
|
||||||
MongoBulkWrite,
|
MongoBulkWrite,
|
||||||
|
MongoRenameCollection,
|
||||||
MongoFindCursor,
|
MongoFindCursor,
|
||||||
MultiSession,
|
MultiSession,
|
||||||
StructuredErrorV1,
|
StructuredErrorV1,
|
||||||
@@ -1126,7 +1127,7 @@ fn parse_agent_rpc_error_header(header: &str) -> (Option<i64>, String) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl AgentCapability {
|
impl AgentCapability {
|
||||||
pub const ALL: [Self; 27] = [
|
pub const ALL: [Self; 28] = [
|
||||||
Self::Connect,
|
Self::Connect,
|
||||||
Self::TestConnection,
|
Self::TestConnection,
|
||||||
Self::Metadata,
|
Self::Metadata,
|
||||||
@@ -1151,6 +1152,7 @@ impl AgentCapability {
|
|||||||
Self::MongoInsertDocuments,
|
Self::MongoInsertDocuments,
|
||||||
Self::MongoReplaceDocument,
|
Self::MongoReplaceDocument,
|
||||||
Self::MongoBulkWrite,
|
Self::MongoBulkWrite,
|
||||||
|
Self::MongoRenameCollection,
|
||||||
Self::MongoFindCursor,
|
Self::MongoFindCursor,
|
||||||
Self::MultiSession,
|
Self::MultiSession,
|
||||||
Self::StructuredErrorV1,
|
Self::StructuredErrorV1,
|
||||||
@@ -1182,6 +1184,7 @@ impl AgentCapability {
|
|||||||
Self::MongoInsertDocuments => "mongo_insert_documents",
|
Self::MongoInsertDocuments => "mongo_insert_documents",
|
||||||
Self::MongoReplaceDocument => "mongo_replace_document",
|
Self::MongoReplaceDocument => "mongo_replace_document",
|
||||||
Self::MongoBulkWrite => "mongo_bulk_write",
|
Self::MongoBulkWrite => "mongo_bulk_write",
|
||||||
|
Self::MongoRenameCollection => "mongo_rename_collection",
|
||||||
Self::MongoFindCursor => "mongo_find_cursor",
|
Self::MongoFindCursor => "mongo_find_cursor",
|
||||||
Self::MultiSession => "multi_session",
|
Self::MultiSession => "multi_session",
|
||||||
Self::StructuredErrorV1 => "structured_error_v1",
|
Self::StructuredErrorV1 => "structured_error_v1",
|
||||||
@@ -1387,6 +1390,7 @@ pub enum MongoAgentMethod {
|
|||||||
UpdateDocuments,
|
UpdateDocuments,
|
||||||
ReplaceDocument,
|
ReplaceDocument,
|
||||||
BulkWrite,
|
BulkWrite,
|
||||||
|
RenameCollection,
|
||||||
DeleteDocument,
|
DeleteDocument,
|
||||||
DeleteDocuments,
|
DeleteDocuments,
|
||||||
RunCommand,
|
RunCommand,
|
||||||
@@ -1396,7 +1400,7 @@ pub enum MongoAgentMethod {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl MongoAgentMethod {
|
impl MongoAgentMethod {
|
||||||
pub const ALL: [Self; 27] = [
|
pub const ALL: [Self; 28] = [
|
||||||
Self::ListDatabases,
|
Self::ListDatabases,
|
||||||
Self::ListCollections,
|
Self::ListCollections,
|
||||||
Self::FindDocuments,
|
Self::FindDocuments,
|
||||||
@@ -1418,6 +1422,7 @@ impl MongoAgentMethod {
|
|||||||
Self::UpdateDocuments,
|
Self::UpdateDocuments,
|
||||||
Self::ReplaceDocument,
|
Self::ReplaceDocument,
|
||||||
Self::BulkWrite,
|
Self::BulkWrite,
|
||||||
|
Self::RenameCollection,
|
||||||
Self::DeleteDocument,
|
Self::DeleteDocument,
|
||||||
Self::DeleteDocuments,
|
Self::DeleteDocuments,
|
||||||
Self::RunCommand,
|
Self::RunCommand,
|
||||||
@@ -1449,6 +1454,7 @@ impl MongoAgentMethod {
|
|||||||
Self::UpdateDocuments => "update_documents",
|
Self::UpdateDocuments => "update_documents",
|
||||||
Self::ReplaceDocument => "replace_document",
|
Self::ReplaceDocument => "replace_document",
|
||||||
Self::BulkWrite => "bulk_write",
|
Self::BulkWrite => "bulk_write",
|
||||||
|
Self::RenameCollection => "rename_collection",
|
||||||
Self::DeleteDocument => "delete_document",
|
Self::DeleteDocument => "delete_document",
|
||||||
Self::DeleteDocuments => "delete_documents",
|
Self::DeleteDocuments => "delete_documents",
|
||||||
Self::RunCommand => "run_command",
|
Self::RunCommand => "run_command",
|
||||||
@@ -2859,6 +2865,13 @@ impl AgentDriverClient {
|
|||||||
self.call_mongo_method(MongoAgentMethod::BulkWrite, params).await
|
self.call_mongo_method(MongoAgentMethod::BulkWrite, params).await
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub async fn mongo_rename_collection<T: DeserializeOwned + Send + 'static>(
|
||||||
|
&mut self,
|
||||||
|
params: Value,
|
||||||
|
) -> Result<T, String> {
|
||||||
|
self.call_mongo_method(MongoAgentMethod::RenameCollection, params).await
|
||||||
|
}
|
||||||
|
|
||||||
pub async fn mongo_delete_document<T: DeserializeOwned + Send + 'static>(
|
pub async fn mongo_delete_document<T: DeserializeOwned + Send + 'static>(
|
||||||
&mut self,
|
&mut self,
|
||||||
params: Value,
|
params: Value,
|
||||||
@@ -3016,6 +3029,7 @@ pub fn agent_supports_capability(handshake: Option<&AgentHandshake>, capability:
|
|||||||
| AgentCapability::MongoInsertDocuments
|
| AgentCapability::MongoInsertDocuments
|
||||||
| AgentCapability::MongoReplaceDocument
|
| AgentCapability::MongoReplaceDocument
|
||||||
| AgentCapability::MongoBulkWrite
|
| AgentCapability::MongoBulkWrite
|
||||||
|
| AgentCapability::MongoRenameCollection
|
||||||
| AgentCapability::MongoFindCursor
|
| AgentCapability::MongoFindCursor
|
||||||
) {
|
) {
|
||||||
return handshake.map(|value| value.supports(capability)).unwrap_or(false);
|
return handshake.map(|value| value.supports(capability)).unwrap_or(false);
|
||||||
@@ -5052,10 +5066,11 @@ for line in sys.stdin:
|
|||||||
assert_eq!(AgentCapability::MongoInsertDocuments.as_str(), "mongo_insert_documents");
|
assert_eq!(AgentCapability::MongoInsertDocuments.as_str(), "mongo_insert_documents");
|
||||||
assert_eq!(AgentCapability::MongoReplaceDocument.as_str(), "mongo_replace_document");
|
assert_eq!(AgentCapability::MongoReplaceDocument.as_str(), "mongo_replace_document");
|
||||||
assert_eq!(AgentCapability::MongoBulkWrite.as_str(), "mongo_bulk_write");
|
assert_eq!(AgentCapability::MongoBulkWrite.as_str(), "mongo_bulk_write");
|
||||||
|
assert_eq!(AgentCapability::MongoRenameCollection.as_str(), "mongo_rename_collection");
|
||||||
assert_eq!(AgentCapability::MongoFindCursor.as_str(), "mongo_find_cursor");
|
assert_eq!(AgentCapability::MongoFindCursor.as_str(), "mongo_find_cursor");
|
||||||
assert_eq!(AgentCapability::MultiSession.as_str(), "multi_session");
|
assert_eq!(AgentCapability::MultiSession.as_str(), "multi_session");
|
||||||
assert_eq!(AgentCapability::StructuredErrorV1.as_str(), "structured_error_v1");
|
assert_eq!(AgentCapability::StructuredErrorV1.as_str(), "structured_error_v1");
|
||||||
assert_eq!(AgentCapability::ALL.len(), 27);
|
assert_eq!(AgentCapability::ALL.len(), 28);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -5119,6 +5134,7 @@ for line in sys.stdin:
|
|||||||
assert_eq!(MongoAgentMethod::UpdateDocuments.as_str(), "update_documents");
|
assert_eq!(MongoAgentMethod::UpdateDocuments.as_str(), "update_documents");
|
||||||
assert_eq!(MongoAgentMethod::ReplaceDocument.as_str(), "replace_document");
|
assert_eq!(MongoAgentMethod::ReplaceDocument.as_str(), "replace_document");
|
||||||
assert_eq!(MongoAgentMethod::BulkWrite.as_str(), "bulk_write");
|
assert_eq!(MongoAgentMethod::BulkWrite.as_str(), "bulk_write");
|
||||||
|
assert_eq!(MongoAgentMethod::RenameCollection.as_str(), "rename_collection");
|
||||||
assert_eq!(MongoAgentMethod::DeleteDocument.as_str(), "delete_document");
|
assert_eq!(MongoAgentMethod::DeleteDocument.as_str(), "delete_document");
|
||||||
assert_eq!(MongoAgentMethod::DeleteDocuments.as_str(), "delete_documents");
|
assert_eq!(MongoAgentMethod::DeleteDocuments.as_str(), "delete_documents");
|
||||||
assert_eq!(MongoAgentMethod::StartFindCursor.as_str(), "start_find_cursor");
|
assert_eq!(MongoAgentMethod::StartFindCursor.as_str(), "start_find_cursor");
|
||||||
|
|||||||
@@ -78,6 +78,12 @@ pub enum MongoCommand {
|
|||||||
DropIndexes { collection: String, indexes: Option<String>, single: bool },
|
DropIndexes { collection: String, indexes: Option<String>, single: bool },
|
||||||
#[serde(rename = "dropCollection")]
|
#[serde(rename = "dropCollection")]
|
||||||
DropCollection { collection: String },
|
DropCollection { collection: String },
|
||||||
|
#[serde(rename = "renameCollection")]
|
||||||
|
RenameCollection {
|
||||||
|
collection: String,
|
||||||
|
#[serde(rename = "newName")]
|
||||||
|
new_name: String,
|
||||||
|
},
|
||||||
#[serde(rename = "findOneAndUpdate")]
|
#[serde(rename = "findOneAndUpdate")]
|
||||||
FindOneAndUpdate { collection: String, filter: String, update: String, options: Option<String> },
|
FindOneAndUpdate { collection: String, filter: String, update: String, options: Option<String> },
|
||||||
#[serde(rename = "findOneAndReplace")]
|
#[serde(rename = "findOneAndReplace")]
|
||||||
@@ -108,6 +114,7 @@ impl MongoCommand {
|
|||||||
| Self::CreateIndex { .. }
|
| Self::CreateIndex { .. }
|
||||||
| Self::DropIndexes { .. }
|
| Self::DropIndexes { .. }
|
||||||
| Self::DropCollection { .. }
|
| Self::DropCollection { .. }
|
||||||
|
| Self::RenameCollection { .. }
|
||||||
| Self::FindOneAndUpdate { .. }
|
| Self::FindOneAndUpdate { .. }
|
||||||
| Self::FindOneAndReplace { .. }
|
| Self::FindOneAndReplace { .. }
|
||||||
| Self::FindOneAndDelete { .. }
|
| Self::FindOneAndDelete { .. }
|
||||||
@@ -863,6 +870,27 @@ pub fn parse(input: &str) -> Result<MongoCommand, String> {
|
|||||||
single: false,
|
single: false,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
if let Some((args, tail)) = method_call(source, prefix_end, "renameCollection") {
|
||||||
|
if !tail.is_empty() || args.is_empty() {
|
||||||
|
return Err("MongoDB renameCollection() requires the new collection name.".to_string());
|
||||||
|
}
|
||||||
|
if args.len() > 1 {
|
||||||
|
// The second argument is dropTarget, which would delete an existing collection
|
||||||
|
// of the new name; reject it rather than silently ignore it.
|
||||||
|
return Err(
|
||||||
|
"MongoDB renameCollection() dropTarget is not supported; drop the target collection first.".to_string()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
let new_name = parse_string_arg(&args[0])?;
|
||||||
|
if new_name.trim().is_empty() || new_name.contains('$') {
|
||||||
|
return Err("MongoDB renameCollection() requires a valid collection name.".to_string());
|
||||||
|
}
|
||||||
|
if new_name == collection {
|
||||||
|
return Err("MongoDB renameCollection() new name must differ from the current name.".to_string());
|
||||||
|
}
|
||||||
|
return Ok(MongoCommand::RenameCollection { collection, new_name });
|
||||||
|
}
|
||||||
|
|
||||||
if let Some((args, tail)) = method_call(source, prefix_end, "drop") {
|
if let Some((args, tail)) = method_call(source, prefix_end, "drop") {
|
||||||
if !tail.is_empty() || !args.is_empty() {
|
if !tail.is_empty() || !args.is_empty() {
|
||||||
return Err("Invalid MongoDB drop() command.".to_string());
|
return Err("Invalid MongoDB drop() command.".to_string());
|
||||||
@@ -2219,6 +2247,32 @@ mod tests {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parses_rename_collection() {
|
||||||
|
let command = parse(r#"db.orders.renameCollection("orders_2024");"#).unwrap();
|
||||||
|
assert_eq!(
|
||||||
|
command,
|
||||||
|
MongoCommand::RenameCollection { collection: "orders".to_string(), new_name: "orders_2024".to_string() }
|
||||||
|
);
|
||||||
|
assert!(command.is_mutating());
|
||||||
|
assert!(!command.is_dangerous());
|
||||||
|
assert_eq!(serde_json::to_value(&command).unwrap()["newName"], "orders_2024");
|
||||||
|
assert!(matches!(parse(r#"db["a-b"].renameCollection('c')"#).unwrap(), MongoCommand::RenameCollection { .. }));
|
||||||
|
|
||||||
|
for (source, expected) in [
|
||||||
|
("db.orders.renameCollection()", "requires the new collection name"),
|
||||||
|
("db.orders.renameCollection(1)", "must be a string"),
|
||||||
|
(r#"db.orders.renameCollection("")"#, "valid collection name"),
|
||||||
|
(r#"db.orders.renameCollection("a$b")"#, "valid collection name"),
|
||||||
|
(r#"db.orders.renameCollection("orders")"#, "must differ"),
|
||||||
|
(r#"db.orders.renameCollection("x", true)"#, "dropTarget is not supported"),
|
||||||
|
(r#"db.orders.renameCollection("x").y()"#, "requires the new collection name"),
|
||||||
|
] {
|
||||||
|
let error = parse(source).unwrap_err();
|
||||||
|
assert!(error.contains(expected), "{source}\n expected: {expected}\n got: {error}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn parses_show_databases_aliases_as_read_only() {
|
fn parses_show_databases_aliases_as_read_only() {
|
||||||
for source in [
|
for source in [
|
||||||
|
|||||||
@@ -2020,6 +2020,20 @@ impl DbxBackend for WebBackend {
|
|||||||
.collect::<Vec<_>>();
|
.collect::<Vec<_>>();
|
||||||
Ok(mongo_drop_indexes_query_result(dropped_names, failures, affected_rows_from_value(&value)))
|
Ok(mongo_drop_indexes_query_result(dropped_names, failures, affected_rows_from_value(&value)))
|
||||||
}
|
}
|
||||||
|
MongoCommand::RenameCollection { collection, new_name } => {
|
||||||
|
self.request(
|
||||||
|
reqwest::Method::POST,
|
||||||
|
"/api/mongo/rename-collection",
|
||||||
|
Some(json!({
|
||||||
|
"connectionId": connection_id,
|
||||||
|
"database": database,
|
||||||
|
"collection": collection,
|
||||||
|
"newName": new_name,
|
||||||
|
})),
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
Ok(scalar_query_result("renamed", Value::String(format!("{collection} -> {new_name}"))))
|
||||||
|
}
|
||||||
MongoCommand::DropCollection { collection } => {
|
MongoCommand::DropCollection { collection } => {
|
||||||
self.request(
|
self.request(
|
||||||
reqwest::Method::POST,
|
reqwest::Method::POST,
|
||||||
|
|||||||
+2
-1
@@ -502,8 +502,9 @@ test("suggests only helpers the shell parser accepts", () => {
|
|||||||
assert.ok(methodLabels.includes("estimatedDocumentCount"));
|
assert.ok(methodLabels.includes("estimatedDocumentCount"));
|
||||||
assert.ok(methodLabels.includes("replaceOne"));
|
assert.ok(methodLabels.includes("replaceOne"));
|
||||||
assert.ok(methodLabels.includes("bulkWrite"));
|
assert.ok(methodLabels.includes("bulkWrite"));
|
||||||
|
assert.ok(methodLabels.includes("renameCollection"));
|
||||||
// Suggesting a helper DBX cannot run just hands the user a command that fails.
|
// Suggesting a helper DBX cannot run just hands the user a command that fails.
|
||||||
for (const unsupported of ["renameCollection", "mapReduce", "watch"]) {
|
for (const unsupported of ["mapReduce", "watch", "validate"]) {
|
||||||
assert.equal(methodLabels.includes(unsupported), false, `${unsupported} is not executable`);
|
assert.equal(methodLabels.includes(unsupported), false, `${unsupported} is not executable`);
|
||||||
}
|
}
|
||||||
// Cursor methods are not collection methods.
|
// Cursor methods are not collection methods.
|
||||||
|
|||||||
+22
-3
@@ -747,6 +747,25 @@ test("parseMongoWriteCommand accepts unquoted insert and update commands", () =>
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("parseMongoWriteCommand reads renameCollection", () => {
|
||||||
|
assert.deepEqual(parseMongoWriteCommand('db.orders.renameCollection("orders_2024");'), { kind: "renameCollection", collection: "orders", newName: "orders_2024" });
|
||||||
|
assert.equal(parseMongoWriteCommand("db[\"a-b\"].renameCollection('c')")?.kind, "renameCollection");
|
||||||
|
assert.equal(parseMongoWriteCommand('db.getCollection("a").renameCollection("b")')?.kind, "renameCollection");
|
||||||
|
|
||||||
|
for (const [source, expected] of [
|
||||||
|
["db.orders.renameCollection()", /expects the new collection name/],
|
||||||
|
["db.orders.renameCollection(1)", /expects the new collection name/],
|
||||||
|
['db.orders.renameCollection("")', /requires a valid collection name/],
|
||||||
|
['db.orders.renameCollection("a$b")', /requires a valid collection name/],
|
||||||
|
['db.orders.renameCollection("orders")', /must differ from the current name/],
|
||||||
|
['db.orders.renameCollection("x", true)', /dropTarget is not supported; drop the target collection first/],
|
||||||
|
['db.orders.renameCollection("x").y()', /Unexpected text after renameCollection/],
|
||||||
|
] as const) {
|
||||||
|
assert.equal(parseMongoCommand(source), null, source);
|
||||||
|
assert.match(describeMongoCommandParseFailure(source), expected, source);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
test("parseMongoWriteCommand reads bulkWrite with every operation kind", () => {
|
test("parseMongoWriteCommand reads bulkWrite with every operation kind", () => {
|
||||||
const command = parseMongoWriteCommand(`db.orders.bulkWrite([
|
const command = parseMongoWriteCommand(`db.orders.bulkWrite([
|
||||||
{ insertOne: { document: { sku: "A1", stock: 1 } } },
|
{ insertOne: { document: { sku: "A1", stock: 1 } } },
|
||||||
@@ -1383,10 +1402,10 @@ test("describeMongoCommandParseFailure explains the argument shape a known metho
|
|||||||
});
|
});
|
||||||
|
|
||||||
test("describeMongoCommandParseFailure names unsupported methods and points at alternatives", () => {
|
test("describeMongoCommandParseFailure names unsupported methods and points at alternatives", () => {
|
||||||
const rename = describeMongoCommandParseFailure('db.c.renameCollection("d")');
|
const mapReduce = describeMongoCommandParseFailure("db.c.mapReduce()");
|
||||||
assert.match(rename, /^Collection method renameCollection\(\) is not supported\. Supported collection methods: find, findOne/);
|
assert.match(mapReduce, /^Collection method mapReduce\(\) is not supported\. Supported collection methods: find, findOne/);
|
||||||
// Bracket and getCollection targets are recognised too.
|
// Bracket and getCollection targets are recognised too.
|
||||||
assert.match(describeMongoCommandParseFailure('db["my-coll"].renameCollection("x")'), /renameCollection\(\) is not supported/);
|
assert.match(describeMongoCommandParseFailure('db["my-coll"].validate()'), /validate\(\) is not supported/);
|
||||||
assert.match(describeMongoCommandParseFailure('db.getCollection("my-coll").watch()'), /watch\(\) is not supported/);
|
assert.match(describeMongoCommandParseFailure('db.getCollection("my-coll").watch()'), /watch\(\) is not supported/);
|
||||||
|
|
||||||
assert.match(describeMongoCommandParseFailure('db.createCollection("c")'), /db\.createCollection\(\) is not supported; collections are created on first insert/);
|
assert.match(describeMongoCommandParseFailure('db.createCollection("c")'), /db\.createCollection\(\) is not supported; collections are created on first insert/);
|
||||||
|
|||||||
Reference in new issue
Block a user