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
@@ -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", "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"],
|
||||
"defaultSqlCapabilities": ["connect", "test_connection", "metadata", "query", "paged_query", "transaction", "ddl"],
|
||||
"commonMethods": [
|
||||
@@ -42,6 +42,6 @@
|
||||
"disconnect",
|
||||
"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"]
|
||||
}
|
||||
@@ -95,7 +95,23 @@ pub async fn mongo_rename_collection_core(
|
||||
let pool = state.pool_handle(connection_id).await.ok_or("Not found")?;
|
||||
match &pool {
|
||||
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()),
|
||||
}
|
||||
}
|
||||
@@ -1200,6 +1216,10 @@ pub async fn execute_mongo_command_core(
|
||||
mongo_drop_collection_core(state, connection_id, database, collection).await?;
|
||||
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 } => {
|
||||
let result = mongo_find_one_and_update_core(
|
||||
state,
|
||||
|
||||
@@ -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", "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"],
|
||||
"defaultSqlCapabilities": ["connect", "test_connection", "metadata", "query", "paged_query", "transaction", "ddl", "multi_session", "structured_error_v1"],
|
||||
"commonMethods": [
|
||||
@@ -46,7 +46,7 @@
|
||||
"disconnect",
|
||||
"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"],
|
||||
"sessionField": "agentSessionId",
|
||||
"cursorSessionField": "sessionId"
|
||||
|
||||
@@ -1043,6 +1043,7 @@ pub enum AgentCapability {
|
||||
MongoInsertDocuments,
|
||||
MongoReplaceDocument,
|
||||
MongoBulkWrite,
|
||||
MongoRenameCollection,
|
||||
MongoFindCursor,
|
||||
MultiSession,
|
||||
StructuredErrorV1,
|
||||
@@ -1126,7 +1127,7 @@ fn parse_agent_rpc_error_header(header: &str) -> (Option<i64>, String) {
|
||||
}
|
||||
|
||||
impl AgentCapability {
|
||||
pub const ALL: [Self; 27] = [
|
||||
pub const ALL: [Self; 28] = [
|
||||
Self::Connect,
|
||||
Self::TestConnection,
|
||||
Self::Metadata,
|
||||
@@ -1151,6 +1152,7 @@ impl AgentCapability {
|
||||
Self::MongoInsertDocuments,
|
||||
Self::MongoReplaceDocument,
|
||||
Self::MongoBulkWrite,
|
||||
Self::MongoRenameCollection,
|
||||
Self::MongoFindCursor,
|
||||
Self::MultiSession,
|
||||
Self::StructuredErrorV1,
|
||||
@@ -1182,6 +1184,7 @@ impl AgentCapability {
|
||||
Self::MongoInsertDocuments => "mongo_insert_documents",
|
||||
Self::MongoReplaceDocument => "mongo_replace_document",
|
||||
Self::MongoBulkWrite => "mongo_bulk_write",
|
||||
Self::MongoRenameCollection => "mongo_rename_collection",
|
||||
Self::MongoFindCursor => "mongo_find_cursor",
|
||||
Self::MultiSession => "multi_session",
|
||||
Self::StructuredErrorV1 => "structured_error_v1",
|
||||
@@ -1387,6 +1390,7 @@ pub enum MongoAgentMethod {
|
||||
UpdateDocuments,
|
||||
ReplaceDocument,
|
||||
BulkWrite,
|
||||
RenameCollection,
|
||||
DeleteDocument,
|
||||
DeleteDocuments,
|
||||
RunCommand,
|
||||
@@ -1396,7 +1400,7 @@ pub enum MongoAgentMethod {
|
||||
}
|
||||
|
||||
impl MongoAgentMethod {
|
||||
pub const ALL: [Self; 27] = [
|
||||
pub const ALL: [Self; 28] = [
|
||||
Self::ListDatabases,
|
||||
Self::ListCollections,
|
||||
Self::FindDocuments,
|
||||
@@ -1418,6 +1422,7 @@ impl MongoAgentMethod {
|
||||
Self::UpdateDocuments,
|
||||
Self::ReplaceDocument,
|
||||
Self::BulkWrite,
|
||||
Self::RenameCollection,
|
||||
Self::DeleteDocument,
|
||||
Self::DeleteDocuments,
|
||||
Self::RunCommand,
|
||||
@@ -1449,6 +1454,7 @@ impl MongoAgentMethod {
|
||||
Self::UpdateDocuments => "update_documents",
|
||||
Self::ReplaceDocument => "replace_document",
|
||||
Self::BulkWrite => "bulk_write",
|
||||
Self::RenameCollection => "rename_collection",
|
||||
Self::DeleteDocument => "delete_document",
|
||||
Self::DeleteDocuments => "delete_documents",
|
||||
Self::RunCommand => "run_command",
|
||||
@@ -2859,6 +2865,13 @@ impl AgentDriverClient {
|
||||
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>(
|
||||
&mut self,
|
||||
params: Value,
|
||||
@@ -3016,6 +3029,7 @@ pub fn agent_supports_capability(handshake: Option<&AgentHandshake>, capability:
|
||||
| AgentCapability::MongoInsertDocuments
|
||||
| AgentCapability::MongoReplaceDocument
|
||||
| AgentCapability::MongoBulkWrite
|
||||
| AgentCapability::MongoRenameCollection
|
||||
| AgentCapability::MongoFindCursor
|
||||
) {
|
||||
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::MongoReplaceDocument.as_str(), "mongo_replace_document");
|
||||
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::MultiSession.as_str(), "multi_session");
|
||||
assert_eq!(AgentCapability::StructuredErrorV1.as_str(), "structured_error_v1");
|
||||
assert_eq!(AgentCapability::ALL.len(), 27);
|
||||
assert_eq!(AgentCapability::ALL.len(), 28);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -5119,6 +5134,7 @@ for line in sys.stdin:
|
||||
assert_eq!(MongoAgentMethod::UpdateDocuments.as_str(), "update_documents");
|
||||
assert_eq!(MongoAgentMethod::ReplaceDocument.as_str(), "replace_document");
|
||||
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::DeleteDocuments.as_str(), "delete_documents");
|
||||
assert_eq!(MongoAgentMethod::StartFindCursor.as_str(), "start_find_cursor");
|
||||
|
||||
@@ -78,6 +78,12 @@ pub enum MongoCommand {
|
||||
DropIndexes { collection: String, indexes: Option<String>, single: bool },
|
||||
#[serde(rename = "dropCollection")]
|
||||
DropCollection { collection: String },
|
||||
#[serde(rename = "renameCollection")]
|
||||
RenameCollection {
|
||||
collection: String,
|
||||
#[serde(rename = "newName")]
|
||||
new_name: String,
|
||||
},
|
||||
#[serde(rename = "findOneAndUpdate")]
|
||||
FindOneAndUpdate { collection: String, filter: String, update: String, options: Option<String> },
|
||||
#[serde(rename = "findOneAndReplace")]
|
||||
@@ -108,6 +114,7 @@ impl MongoCommand {
|
||||
| Self::CreateIndex { .. }
|
||||
| Self::DropIndexes { .. }
|
||||
| Self::DropCollection { .. }
|
||||
| Self::RenameCollection { .. }
|
||||
| Self::FindOneAndUpdate { .. }
|
||||
| Self::FindOneAndReplace { .. }
|
||||
| Self::FindOneAndDelete { .. }
|
||||
@@ -863,6 +870,27 @@ pub fn parse(input: &str) -> Result<MongoCommand, String> {
|
||||
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 !tail.is_empty() || !args.is_empty() {
|
||||
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]
|
||||
fn parses_show_databases_aliases_as_read_only() {
|
||||
for source in [
|
||||
|
||||
@@ -2020,6 +2020,20 @@ impl DbxBackend for WebBackend {
|
||||
.collect::<Vec<_>>();
|
||||
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 } => {
|
||||
self.request(
|
||||
reqwest::Method::POST,
|
||||
|
||||
Reference in new issue
Block a user