Skip to content

Commit 357fca4

Browse files
RUST-372 Allow hinting the delete command (#190)
1 parent 6b74f93 commit 357fca4

File tree

6 files changed

+192
-35
lines changed

6 files changed

+192
-35
lines changed

src/coll/options.rs

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -254,7 +254,7 @@ pub struct ReplaceOptions {
254254
/// [`Collection::delete_one`](../struct.Collection.html#method.delete_one) or
255255
/// [`Collection::delete_many`](../struct.Collection.html#method.delete_many) operation.
256256
#[serde_with::skip_serializing_none]
257-
#[derive(Debug, Default, TypedBuilder, Serialize)]
257+
#[derive(Clone, Debug, Default, TypedBuilder, Serialize)]
258258
#[serde(rename_all = "camelCase")]
259259
#[non_exhaustive]
260260
pub struct DeleteOptions {
@@ -268,12 +268,17 @@ pub struct DeleteOptions {
268268
/// The write concern for the operation.
269269
#[builder(default)]
270270
pub write_concern: Option<WriteConcern>,
271+
272+
/// The index to use for the operation.
273+
/// Only available in MongoDB 4.4+.
274+
#[builder(default)]
275+
pub hint: Option<Hint>,
271276
}
272277

273278
/// Specifies the options to a
274279
/// [`Collection::find_one_and_delete`](../struct.Collection.html#method.find_one_and_delete)
275280
/// operation.
276-
#[derive(Debug, Default, TypedBuilder)]
281+
#[derive(Clone, Debug, Default, TypedBuilder)]
277282
#[non_exhaustive]
278283
pub struct FindOneAndDeleteOptions {
279284
/// The maximum amount of time to allow the query to run.
@@ -301,6 +306,11 @@ pub struct FindOneAndDeleteOptions {
301306
/// information on how to use this option.
302307
#[builder(default)]
303308
pub collation: Option<Collation>,
309+
310+
/// The index to use for the operation.
311+
/// Only available in MongoDB 4.4+.
312+
#[builder(default)]
313+
pub hint: Option<Hint>,
304314
}
305315

306316
/// Specifies the options to a

src/operation/find_and_modify/mod.rs

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,27 @@ impl Operation for FindAndModify {
7575
type O = Option<Document>;
7676
const NAME: &'static str = "findAndModify";
7777

78-
fn build(&self, _description: &StreamDescription) -> Result<Command> {
78+
fn build(&self, description: &StreamDescription) -> Result<Command> {
79+
if self.options.hint.is_some() {
80+
match description.max_wire_version {
81+
Some(version) if version < 8 => {
82+
return Err(ErrorKind::OperationError {
83+
message: "Specifying a hint is not supported on server versions < 4.4"
84+
.to_string(),
85+
}
86+
.into());
87+
}
88+
None => {
89+
return Err(ErrorKind::OperationError {
90+
message: "Specifying a hint is not supported on server versions < 4.4"
91+
.to_string(),
92+
}
93+
.into());
94+
}
95+
_ => {}
96+
}
97+
}
98+
7999
let mut body: Document = doc! {
80100
Self::NAME: self.ns.coll.clone(),
81101
"query": self.query.clone(),

src/operation/find_and_modify/options.rs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ use crate::{
1010
FindOneAndDeleteOptions,
1111
FindOneAndReplaceOptions,
1212
FindOneAndUpdateOptions,
13+
Hint,
1314
ReturnDocument,
1415
UpdateModifications,
1516
},
@@ -67,6 +68,9 @@ pub(super) struct FindAndModifyOptions {
6768

6869
#[builder(default)]
6970
pub(crate) collation: Option<Collation>,
71+
72+
#[builder(default)]
73+
pub(crate) hint: Option<Hint>,
7074
}
7175

7276
impl FindAndModifyOptions {
@@ -80,6 +84,7 @@ impl FindAndModifyOptions {
8084
.projection(opts.projection)
8185
.sort(opts.sort)
8286
.write_concern(opts.write_concern)
87+
.hint(opts.hint)
8388
.build()
8489
}
8590

src/test/coll.rs

Lines changed: 132 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,22 @@ use std::time::Duration;
22

33
use futures::stream::StreamExt;
44
use lazy_static::lazy_static;
5+
use semver::VersionReq;
56

67
use crate::{
78
bson::{doc, Bson, Document},
8-
error::ErrorKind,
9+
error::{ErrorKind, Result},
910
event::command::CommandStartedEvent,
10-
options::{AggregateOptions, FindOptions, InsertManyOptions, UpdateOptions},
11+
options::{
12+
AggregateOptions,
13+
DeleteOptions,
14+
FindOneAndDeleteOptions,
15+
FindOptions,
16+
Hint,
17+
InsertManyOptions,
18+
UpdateOptions,
19+
},
20+
results::DeleteResult,
1121
test::{
1222
util::{drop_collection, CommandEvent, EventClient, TestClient},
1323
LOCK,
@@ -515,23 +525,11 @@ async fn allow_disk_use_test(options: FindOptions, expected_value: Option<bool>)
515525
.collection(function_name!());
516526
coll.find(None, options).await.unwrap();
517527

518-
let events = event_client.command_events.read().unwrap();
519-
let mut iter = events.iter().filter(|event| match event {
520-
CommandEvent::CommandStartedEvent(CommandStartedEvent { command_name, .. }) => {
521-
command_name == "find"
522-
}
523-
_ => false,
524-
});
528+
let events = event_client.get_command_started_events("find");
529+
assert_eq!(events.len(), 1);
525530

526-
let event = iter.next().unwrap();
527-
let allow_disk_use = match event {
528-
CommandEvent::CommandStartedEvent(CommandStartedEvent { command, .. }) => {
529-
command.get_bool("allowDiskUse").ok()
530-
}
531-
_ => None,
532-
};
531+
let allow_disk_use = events[0].command.get_bool("allowDiskUse").ok();
533532
assert_eq!(allow_disk_use, expected_value);
534-
assert_eq!(iter.count(), 0);
535533
}
536534

537535
#[cfg_attr(feature = "tokio-runtime", tokio::test)]
@@ -545,3 +543,120 @@ async fn ns_not_found_suppression() {
545543
coll.drop(None).await.expect("drop should not fail");
546544
coll.drop(None).await.expect("drop should not fail");
547545
}
546+
547+
async fn delete_hint_test(options: Option<DeleteOptions>, name: &str) {
548+
let _guard = LOCK.run_concurrently().await;
549+
550+
let client = EventClient::new().await;
551+
let coll = client.database(name).collection(name);
552+
let _: Result<DeleteResult> = coll.delete_many(doc! {}, options.clone()).await;
553+
554+
let events = client.get_command_started_events("delete");
555+
assert_eq!(events.len(), 1);
556+
557+
let event_hint = events[0].command.get("hint").cloned();
558+
let expected_hint = match options {
559+
Some(options) => options.hint.map(|hint| hint.to_bson()),
560+
None => None,
561+
};
562+
assert_eq!(event_hint, expected_hint);
563+
}
564+
565+
#[cfg_attr(feature = "tokio-runtime", tokio::test)]
566+
#[cfg_attr(feature = "async-std-runtime", async_std::test)]
567+
#[function_name::named]
568+
async fn delete_hint_keys_specified() {
569+
let options = DeleteOptions::builder().hint(Hint::Keys(doc! {})).build();
570+
delete_hint_test(Some(options), function_name!()).await;
571+
}
572+
573+
#[cfg_attr(feature = "tokio-runtime", tokio::test)]
574+
#[cfg_attr(feature = "async-std-runtime", async_std::test)]
575+
#[function_name::named]
576+
async fn delete_hint_string_specified() {
577+
let options = DeleteOptions::builder()
578+
.hint(Hint::Name(String::new()))
579+
.build();
580+
delete_hint_test(Some(options), function_name!()).await;
581+
}
582+
583+
#[cfg_attr(feature = "tokio-runtime", tokio::test)]
584+
#[cfg_attr(feature = "async-std-runtime", async_std::test)]
585+
#[function_name::named]
586+
async fn delete_hint_not_specified() {
587+
delete_hint_test(None, function_name!()).await;
588+
}
589+
590+
async fn find_one_and_delete_hint_test(options: Option<FindOneAndDeleteOptions>, name: &str) {
591+
let _guard = LOCK.run_concurrently().await;
592+
let client = EventClient::new().await;
593+
594+
let req = VersionReq::parse("< 4.2").unwrap();
595+
if options.is_some() && req.matches(&client.server_version) {
596+
return;
597+
}
598+
599+
let coll = client.database(name).collection(name);
600+
let _: Result<Option<Document>> = coll.find_one_and_delete(doc! {}, options.clone()).await;
601+
602+
let events = client.get_command_started_events("findAndModify");
603+
assert_eq!(events.len(), 1);
604+
605+
let event_hint = events[0].command.get("hint").cloned();
606+
let expected_hint = options.and_then(|options| options.hint.map(|hint| hint.to_bson()));
607+
assert_eq!(event_hint, expected_hint);
608+
}
609+
610+
#[cfg_attr(feature = "tokio-runtime", tokio::test)]
611+
#[cfg_attr(feature = "async-std-runtime", async_std::test)]
612+
#[function_name::named]
613+
async fn find_one_and_delete_hint_keys_specified() {
614+
let options = FindOneAndDeleteOptions::builder()
615+
.hint(Hint::Keys(doc! {}))
616+
.build();
617+
find_one_and_delete_hint_test(Some(options), function_name!()).await;
618+
}
619+
620+
#[cfg_attr(feature = "tokio-runtime", tokio::test)]
621+
#[cfg_attr(feature = "async-std-runtime", async_std::test)]
622+
#[function_name::named]
623+
async fn find_one_and_delete_hint_string_specified() {
624+
let options = FindOneAndDeleteOptions::builder()
625+
.hint(Hint::Name(String::new()))
626+
.build();
627+
find_one_and_delete_hint_test(Some(options), function_name!()).await;
628+
}
629+
630+
#[cfg_attr(feature = "tokio-runtime", tokio::test)]
631+
#[cfg_attr(feature = "async-std-runtime", async_std::test)]
632+
#[function_name::named]
633+
async fn find_one_and_delete_hint_not_specified() {
634+
find_one_and_delete_hint_test(None, function_name!()).await;
635+
}
636+
637+
#[cfg_attr(feature = "tokio-runtime", tokio::test)]
638+
#[cfg_attr(feature = "async-std-runtime", async_std::test)]
639+
#[function_name::named]
640+
async fn find_one_and_delete_hint_server_version() {
641+
let _guard = LOCK.run_concurrently().await;
642+
643+
let client = EventClient::new().await;
644+
let coll = client.database(function_name!()).collection("coll");
645+
646+
let options = FindOneAndDeleteOptions::builder()
647+
.hint(Hint::Name(String::new()))
648+
.build();
649+
let res = coll.find_one_and_delete(doc! {}, options).await;
650+
651+
let req1 = VersionReq::parse("< 4.2").unwrap();
652+
let req2 = VersionReq::parse("4.2.*").unwrap();
653+
if req1.matches(&client.server_version) {
654+
let error = res.expect_err("find one and delete should fail");
655+
assert!(matches!(error.kind.as_ref(), ErrorKind::OperationError { .. }));
656+
} else if req2.matches(&client.server_version) {
657+
let error = res.expect_err("find one and delete should fail");
658+
assert!(matches!(error.kind.as_ref(), ErrorKind::CommandError { .. }));
659+
} else {
660+
assert!(res.is_ok());
661+
}
662+
}

src/test/db.rs

Lines changed: 4 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ use crate::{
99
error::Result,
1010
options::{AggregateOptions, CreateCollectionOptions, IndexOptionDefaults},
1111
test::{
12-
util::{CommandEvent, EventClient, TestClient},
12+
util::{EventClient, TestClient},
1313
LOCK,
1414
},
1515
Database,
@@ -351,25 +351,14 @@ async fn index_option_defaults_test(defaults: Option<IndexOptionDefaults>, name:
351351
db.create_collection(name, options).await.unwrap();
352352
db.drop(None).await.unwrap();
353353

354-
let events = client.command_events.read().unwrap();
355-
let mut iter = events.iter().filter_map(|event| match event {
356-
CommandEvent::CommandStartedEvent(event) => {
357-
if event.command_name == "create" {
358-
Some(event)
359-
} else {
360-
None
361-
}
362-
}
363-
_ => None,
364-
});
354+
let events = client.get_command_started_events("create");
355+
assert_eq!(events.len(), 1);
365356

366-
let event = iter.next().unwrap();
367-
let event_defaults = match event.command.get_document("indexOptionDefaults") {
357+
let event_defaults = match events[0].command.get_document("indexOptionDefaults") {
368358
Ok(defaults) => Some(IndexOptionDefaults {
369359
storage_engine: defaults.get_document("storageEngine").unwrap().clone(),
370360
}),
371361
Err(_) => None,
372362
};
373363
assert_eq!(event_defaults, defaults);
374-
assert!(iter.next().is_none());
375364
}

src/test/util/event.rs

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -192,6 +192,24 @@ impl EventClient {
192192
String::from("single")
193193
}
194194
}
195+
196+
/// Gets all of the command started events for a specified command name.
197+
pub fn get_command_started_events(&self, command_name: &str) -> Vec<CommandStartedEvent> {
198+
let events = self.command_events.read().unwrap();
199+
events
200+
.iter()
201+
.filter_map(|event| match event {
202+
CommandEvent::CommandStartedEvent(event) => {
203+
if event.command_name == command_name {
204+
Some(event.clone())
205+
} else {
206+
None
207+
}
208+
}
209+
_ => None,
210+
})
211+
.collect()
212+
}
195213
}
196214

197215
#[cfg_attr(feature = "tokio-runtime", tokio::test)]

0 commit comments

Comments
 (0)