feat(mongo): add dropIndex and dropIndexes support across full stack
This commit is contained in:
1 parent
bb72a58e54
commit
93eeeaa9c6
29 files changed
+1142
-24
No files matched your search
@@ -9,7 +9,7 @@ use super::with_connection_timeout;
|
||||
use crate::types::IndexInfo;
|
||||
use futures::TryStreamExt;
|
||||
use percent_encoding::percent_decode_str;
|
||||
use std::time::Duration;
|
||||
use std::{collections::HashSet, time::Duration};
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct MongoDocumentResult {
|
||||
@@ -17,6 +17,12 @@ pub struct MongoDocumentResult {
|
||||
pub total: u64,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct MongoDropIndexesResult {
|
||||
pub dropped_names: Vec<String>,
|
||||
pub affected_rows: u64,
|
||||
}
|
||||
|
||||
pub async fn connect(url: &str, timeout: Duration, idle_timeout: Duration) -> Result<Client, String> {
|
||||
let url = normalize_mongo_uri_direct_connection(url);
|
||||
let is_multi_host = is_multi_host_mongo_uri(&url);
|
||||
@@ -367,6 +373,102 @@ pub async fn create_index(
|
||||
Ok(result.index_name)
|
||||
}
|
||||
|
||||
pub async fn drop_indexes(
|
||||
client: &Client,
|
||||
database: &str,
|
||||
collection: &str,
|
||||
indexes_json: Option<&str>,
|
||||
single: bool,
|
||||
) -> Result<MongoDropIndexesResult, String> {
|
||||
let database = database.trim();
|
||||
let collection = collection.trim();
|
||||
if database.is_empty() {
|
||||
return Err("Database name is required".to_string());
|
||||
}
|
||||
if collection.is_empty() {
|
||||
return Err("Collection name is required".to_string());
|
||||
}
|
||||
|
||||
let index = parse_drop_indexes_value(indexes_json, single)?;
|
||||
let before = list_indexes(client, database, collection).await?;
|
||||
client
|
||||
.database(database)
|
||||
.run_command(doc! { "dropIndexes": collection, "index": index })
|
||||
.await
|
||||
.map_err(|e| e.to_string())?;
|
||||
let after = list_indexes(client, database, collection).await?;
|
||||
let dropped_names = diff_dropped_index_names(&before, &after);
|
||||
Ok(MongoDropIndexesResult { affected_rows: dropped_names.len() as u64, dropped_names })
|
||||
}
|
||||
|
||||
fn diff_dropped_index_names(before: &[IndexInfo], after: &[IndexInfo]) -> Vec<String> {
|
||||
let remaining = after.iter().map(|index| index.name.as_str()).collect::<HashSet<_>>();
|
||||
before.iter().filter(|index| !remaining.contains(index.name.as_str())).map(|index| index.name.clone()).collect()
|
||||
}
|
||||
|
||||
fn parse_drop_indexes_value(indexes_json: Option<&str>, single: bool) -> Result<Bson, String> {
|
||||
match indexes_json.map(str::trim).filter(|value| !value.is_empty()) {
|
||||
Some(json) => parse_drop_indexes_json(json, single),
|
||||
None if single => Err("dropIndex requires a string index name or JSON document".to_string()),
|
||||
None => Ok(Bson::String("*".to_string())),
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_drop_indexes_json(json: &str, single: bool) -> Result<Bson, String> {
|
||||
let value: serde_json::Value = serde_json::from_str(json).map_err(|e| format!("Invalid index JSON: {e}"))?;
|
||||
if single {
|
||||
validate_single_drop_index_value(&value)?;
|
||||
} else {
|
||||
validate_multi_drop_indexes_value(&value)?;
|
||||
}
|
||||
Ok(json_value_to_bson(&value))
|
||||
}
|
||||
|
||||
fn validate_single_drop_index_value(value: &serde_json::Value) -> Result<(), String> {
|
||||
match value {
|
||||
serde_json::Value::String(name) => {
|
||||
if name.trim().is_empty() {
|
||||
Err("Index name is required".to_string())
|
||||
} else if name == "*" {
|
||||
Err(r#"dropIndex does not accept "*"; use dropIndexes() or dropIndexes("*") instead"#.to_string())
|
||||
} else {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
serde_json::Value::Object(doc) if doc.is_empty() => Err("Index specification is required".to_string()),
|
||||
serde_json::Value::Object(_) => Ok(()),
|
||||
serde_json::Value::Array(_) => {
|
||||
Err("dropIndex only accepts a string index name or JSON document; arrays are not supported".to_string())
|
||||
}
|
||||
_ => Err("dropIndex only accepts a string index name or JSON document".to_string()),
|
||||
}
|
||||
}
|
||||
|
||||
fn validate_multi_drop_indexes_value(value: &serde_json::Value) -> Result<(), String> {
|
||||
match value {
|
||||
serde_json::Value::String(name) => {
|
||||
if name.trim().is_empty() {
|
||||
Err("Index name is required".to_string())
|
||||
} else {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
serde_json::Value::Object(doc) if doc.is_empty() => Err("Index specification is required".to_string()),
|
||||
serde_json::Value::Object(_) => Ok(()),
|
||||
serde_json::Value::Array(items) if items.is_empty() => {
|
||||
Err("dropIndexes only accepts non-empty string arrays".to_string())
|
||||
}
|
||||
serde_json::Value::Array(items) => {
|
||||
if items.iter().all(|item| matches!(item, serde_json::Value::String(name) if !name.trim().is_empty())) {
|
||||
Ok(())
|
||||
} else {
|
||||
Err("dropIndexes only accepts arrays of string index names".to_string())
|
||||
}
|
||||
}
|
||||
_ => Err("dropIndexes only accepts a string index name, JSON document, or string array".to_string()),
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn insert_document(
|
||||
client: &Client,
|
||||
database: &str,
|
||||
@@ -991,6 +1093,95 @@ mod tests {
|
||||
assert!(index.is_primary);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_drop_indexes_value_validates_drop_index_arguments() {
|
||||
assert!(matches!(
|
||||
parse_drop_indexes_value(Some(r#""users_email_unique""#), true),
|
||||
Ok(Bson::String(name)) if name == "users_email_unique"
|
||||
));
|
||||
assert!(matches!(
|
||||
parse_drop_indexes_value(Some(r#"{"email":1}"#), true),
|
||||
Ok(Bson::Document(doc)) if doc.get_i64("email").ok() == Some(1)
|
||||
));
|
||||
|
||||
let wildcard = parse_drop_indexes_value(Some(r#""*""#), true).unwrap_err();
|
||||
assert!(wildcard.contains("dropIndex does not accept"));
|
||||
|
||||
let array = parse_drop_indexes_value(Some(r#"["a_1"]"#), true).unwrap_err();
|
||||
assert!(array.contains("arrays are not supported"));
|
||||
|
||||
let empty = parse_drop_indexes_value(None, true).unwrap_err();
|
||||
assert!(empty.contains("dropIndex requires"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_drop_indexes_value_validates_drop_indexes_arguments() {
|
||||
assert!(matches!(
|
||||
parse_drop_indexes_value(None, false),
|
||||
Ok(Bson::String(name)) if name == "*"
|
||||
));
|
||||
assert!(matches!(
|
||||
parse_drop_indexes_value(Some(r#""*""#), false),
|
||||
Ok(Bson::String(name)) if name == "*"
|
||||
));
|
||||
assert!(matches!(
|
||||
parse_drop_indexes_value(Some(r#""users_email_unique""#), false),
|
||||
Ok(Bson::String(name)) if name == "users_email_unique"
|
||||
));
|
||||
assert!(matches!(
|
||||
parse_drop_indexes_value(Some(r#"{"email":1}"#), false),
|
||||
Ok(Bson::Document(doc)) if doc.get_i64("email").ok() == Some(1)
|
||||
));
|
||||
assert!(matches!(
|
||||
parse_drop_indexes_value(Some(r#"["a_1","b_1"]"#), false),
|
||||
Ok(Bson::Array(values))
|
||||
if values
|
||||
== vec![Bson::String("a_1".to_string()), Bson::String("b_1".to_string())]
|
||||
));
|
||||
|
||||
let invalid_array = parse_drop_indexes_value(Some(r#"[{"a":1}]"#), false).unwrap_err();
|
||||
assert!(invalid_array.contains("arrays of string index names"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn diff_dropped_index_names_reports_removed_indexes() {
|
||||
let before = vec![
|
||||
IndexInfo {
|
||||
name: "_id_".to_string(),
|
||||
columns: vec!["_id".to_string()],
|
||||
is_unique: true,
|
||||
is_primary: true,
|
||||
filter: None,
|
||||
index_type: Some("_id: 1".to_string()),
|
||||
included_columns: None,
|
||||
comment: None,
|
||||
},
|
||||
IndexInfo {
|
||||
name: "users_email_unique".to_string(),
|
||||
columns: vec!["email".to_string()],
|
||||
is_unique: true,
|
||||
is_primary: false,
|
||||
filter: None,
|
||||
index_type: Some("email: 1".to_string()),
|
||||
included_columns: None,
|
||||
comment: None,
|
||||
},
|
||||
IndexInfo {
|
||||
name: "users_status_idx".to_string(),
|
||||
columns: vec!["status".to_string()],
|
||||
is_unique: false,
|
||||
is_primary: false,
|
||||
filter: None,
|
||||
index_type: Some("status: 1".to_string()),
|
||||
included_columns: None,
|
||||
comment: None,
|
||||
},
|
||||
];
|
||||
let after = vec![before[0].clone(), before[2].clone()];
|
||||
|
||||
assert_eq!(diff_dropped_index_names(&before, &after), vec!["users_email_unique".to_string()]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn json_object_to_document_parses_extended_json_date() {
|
||||
let value = serde_json::json!({
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
use crate::connection::{AppState, PoolKind};
|
||||
use crate::db::mongo_driver::{self, MongoDocumentResult};
|
||||
use crate::db::mongo_driver::{self, MongoDocumentResult, MongoDropIndexesResult};
|
||||
use crate::document_ops::CollectionInfo;
|
||||
|
||||
async fn ensure_document_pool(state: &AppState, connection_id: &str) -> Result<(), String> {
|
||||
@@ -186,6 +186,25 @@ pub async fn mongo_create_index_core(
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn mongo_drop_indexes_core(
|
||||
state: &AppState,
|
||||
connection_id: &str,
|
||||
database: &str,
|
||||
collection: &str,
|
||||
indexes_json: Option<&str>,
|
||||
single: bool,
|
||||
) -> Result<MongoDropIndexesResult, String> {
|
||||
ensure_document_pool(state, connection_id).await?;
|
||||
let connections = state.connections.read().await;
|
||||
match connections.get(connection_id).ok_or("Not found")? {
|
||||
PoolKind::MongoDb(client) => {
|
||||
mongo_driver::drop_indexes(client, database, collection, indexes_json, single).await
|
||||
}
|
||||
PoolKind::Agent(_) => Err("MongoDB legacy agent does not support dropIndex/dropIndexes".to_string()),
|
||||
_ => Err("Not a MongoDB connection".to_string()),
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn mongo_insert_document_core(
|
||||
state: &AppState,
|
||||
connection_id: &str,
|
||||
|
||||
Reference in new issue
Block a user