Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 23 additions & 0 deletions changelog.d/ftdc-diagnostic-data-archive.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
### The Rust MongoDB server writes its FTDC samples to `diagnostic.data`

Every full-time diagnostic data capture sample is now written to
`<dbpath>/diagnostic.data` in mongod's own archive format, so mongod's FTDC
decoders read it: `metrics.<UTC time>-00000` files that open with the
`buildInfo` / `getCmdLineOpts` / `hostInfo` metadata, then zlib-compressed
metric chunks of a reference sample and its delta-encoded successors, with
`metrics.interim` carrying the chunk in progress into the next start.

#### Fixed

- `diagnosticDataCollectionDirectoryPath` answers the directory, and a change
to it is refused 220 `FTDCPathAlreadySet`, as on mongod 8.2.11.
- `diagnosticDataCollectionDirectorySizeMB`, `...FileSizeMB`,
`...SamplesPerChunk` and `...SamplesPerInterimUpdate` are registered with
mongod's minimums and its file-versus-directory size check; files rotate at
the file size and the oldest are removed past the directory size.

Fixes RM-417 (tests/backlog/test_rm_219_diag_query_settings.py::test_samples_are_written_to_diagnostic_data)

#### Filed

- RM-690: the archive writes no `type: 2` periodic metadata.
11 changes: 11 additions & 0 deletions changelog.d/rm-418-debug-query-shape.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
### Rust MongoDB server: `$querySettings` gives `debugQueryShape`

`{$querySettings: {showDebugQueryShape: true}}` now adds each setting's
`debugQueryShape` (its query's shape with each literal a debug type
string, `{x: {$eq: "?number"}}`), as mongod 8.2 does. Before, the option
was parsed and ignored.

#### Fixed

- `showDebugQueryShape` gave no `debugQueryShape`.
Fixes RM-418 in part (tests/backlog/test_rm_219_diag_query_settings.py::test_show_debug_query_shape)
15 changes: 15 additions & 0 deletions changelog.d/rm-540-applyops-u.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
### Rust MongoDB server: `applyOps` `u` entries answer as mongod does

`applyOps` now leaves a `u` replacement of a missing document unapplied when
the entry has no `b: true`, and refuses documents past 16 MB with mongod
8.2.11's codes.

#### Fixed

- A `u` entry replacing a document that does not exist, without `b: true`,
is `false` in `results` (the command fails 8); it was inserted.
- A `u` upsert of a document past 16 MB is 17420 "Document to upsert is
larger than 16777216" (was 10334), and an `i` entry replacing an existing
document with one past 16 MB is 17419 (was 10334).

Fixes RM-540 (tests/backlog/test_rm_219_diag_query_settings.py::test_apply_ops_update_entries)
9 changes: 9 additions & 0 deletions changelog.d/rm-541-int-param-double-bounds.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
### Rust MongoDB server: an int parameter bounds a double before truncating it

`setParameter` now refuses `2147483647.9` and `-2147483648.5` for an int
parameter with mongod 8.2.11's "Out of bounds coercing to integral value",
instead of truncating them into range and storing them.

#### Fixed

- Fixes RM-541 (tests/backlog/test_rm_found_2026_11_10_rm417.py::test_an_int_parameter_bounds_the_double_before_truncating)
17 changes: 17 additions & 0 deletions changelog.d/rm-543-sbe-find-in-projection.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
### Rust MongoDB server: an `$in` and a simple projection explain as SBE under `queryFramework: "sbe"`

A `find` that a query setting puts in SBE now explains as the slot program
mongod 8.2.11 runs when its filter holds an `$in` or it projects top-level
fields. Before, it was explained in the classic shape.

#### Fixed

- An `$in` is `isMember` against an `InList` that the plan's environment
binds, holding the values in the command's order. A simple projection is
`makeBsonObj` (closed for an inclusion, open for an exclusion), or
`newBsonObj` when it keeps one field. It sits above a skip and below a
limit. The `queryPlan`, the slot program and `executionStats` match
mongod's. A sorted or index-served `find` is still explained classically
(RM-543 stays open for those).

Fixes RM-543 in part (tests/backlog/test_rm_found_2026_11_10_rm418.py::test_sbe_find_in_and_projection_are_mongods).
2 changes: 2 additions & 0 deletions crates/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 3 additions & 0 deletions crates/secantus-commands/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,8 @@ md-5 = "0.10"
base64 = "0.22"
# `$queryStats`' `transformIdentifiers`: each identifier as its HMAC-SHA-256.
hmac = "0.12"
# The FTDC archive's metric chunks are zlib, as mongod writes them.
flate2 = "1"

# `dbHash` names the machine as mongod does (`hostname:port`).
[target.'cfg(unix)'.dependencies]
Expand All @@ -56,3 +58,4 @@ libc = "0.2"
hmac = "0.12"
sha2 = "0.10"
pbkdf2 = "0.12"
tempfile = "3"
25 changes: 23 additions & 2 deletions crates/secantus-commands/src/apply_ops.rs
Original file line number Diff line number Diff line change
Expand Up @@ -463,7 +463,10 @@ fn apply_entry(
let post = match (diff, current) {
(Some(diff), Some(pre)) => apply_diff(pre, diff)?,
(Some(diff), None) if upsert => apply_diff(doc! {"_id": id.clone()}, diff)?,
(None, Some(_)) | (None, None) if upsert || o.get("_id") == Some(&id) => {
// A replacement of a missing document is applied only as an
// upsert: mongod 8.2.11 leaves it `false` in `results`.
(None, Some(_)) if o.get("_id") == Some(&id) => o.clone(),
(None, None) if upsert => {
if o.get("_id") != Some(&id) {
return Err(error(13596, "in Collection::updateDocument _id mismatch"));
}
Expand Down Expand Up @@ -526,6 +529,9 @@ fn apply_entry(
}
}

/// The largest document a client may write (mongod's `BSONObjMaxUserSize`).
const BSON_USER_MAX: usize = 16 * 1024 * 1024;

fn is_timeseries(storage: &dyn crate::Storage, db: &str, coll: &str) -> bool {
storage
.get_collection_options(db, coll)
Expand Down Expand Up @@ -581,11 +587,26 @@ fn write_document(
let filter = doc! {"_id": id.clone()};
let outcome = storage
.update_matching(db, coll, &filter, &doc, false, false)
.map_err(crate::util::command_error)?;
.map_err(|e| {
// A replacement past 16 MiB is 17419 under oplog application on
// mongod 8.2.11, with the update path's message.
let e = crate::util::command_error(e);
if e.code == 10334 && e.errmsg.starts_with("Resulting document after update") {
error(17419, e.errmsg)
} else {
e
}
})?;
if outcome.matched > 0 {
return Ok(Outcome::Applied);
}
let bytes = crate::bsondoc::to_vec(&doc).map_err(|e| error(1, e.to_string()))?;
if !insert_entry && bytes.len() > BSON_USER_MAX {
return Err(error(
17420,
format!("Document to upsert is larger than {BSON_USER_MAX}"),
));
}
let (_, errors) = if insert_entry {
storage.insert_from_oplog_application(db, coll, vec![bytes], true)
} else {
Expand Down
10 changes: 5 additions & 5 deletions crates/secantus-commands/src/explain/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1957,8 +1957,9 @@ fn reply_for(r: &Request, verbosity: Verbosity, ctx: &mut CommandContext) -> Han
}
}
// A `find` mongod runs in SBE (RM-418): over a forward collection scan
// with no sort, projection, hint or collation, its scan, filter, skip
// and limit; any other is still explained in the classic shape.
// with no sort, hint or collation, its scan, filter, skip, limit and a
// simple projection (RM-543); any other is still explained in the
// classic shape.
if r.pipeline.is_empty() && front_runs_in_sbe(storage, &db, s, collation.as_ref(), &[]) {
let built = if !t.exists {
Some(sbe::eof_plan())
Expand All @@ -1971,14 +1972,13 @@ fn reply_for(r: &Request, verbosity: Verbosity, ctx: &mut CommandContext) -> Han
}
);
let simple = s.sort.as_ref().is_none_or(Document::is_empty)
&& s.projection.as_ref().is_none_or(Document::is_empty)
&& s.hint.is_none()
&& collation.is_none()
&& s.min_max.is_none()
&& !s.return_key
&& !s.show_record_id;
let parsed = parsed::parsed_query(&s.filter);
let filter = sbe::Filter::parse(&parsed);
let filter = sbe::Filter::parse_find(&parsed, &s.filter);
let on = sbe::Front {
parsed,
filter,
Expand All @@ -1988,7 +1988,7 @@ fn reply_for(r: &Request, verbosity: Verbosity, ctx: &mut CommandContext) -> Han
let filtered = s.filter.is_empty() || on.filter.is_some();
let uuid = crate::admin::collection_uuid_text(storage, &db, &s.coll);
(scan && simple && filtered)
.then(|| sbe::find_plan(&uuid, &on))
.then(|| sbe::find_plan(&uuid, &on, s.projection.as_ref()))
.flatten()
};
if let Some(built) = built {
Expand Down
72 changes: 72 additions & 0 deletions crates/secantus-commands/src/explain/sbe/expr.rs
Original file line number Diff line number Diff line change
Expand Up @@ -54,13 +54,49 @@ pub(crate) enum Expr {
TypeMatch(Box<Expr>, u32),
/// `ENumericConvert` to int32: `convert ( e, int32)`.
ConvertInt32(Box<Expr>),
/// A slot the plan's environment binds to an `InList` (an `$in`'s
/// values, in the order the command gave them): prints `sN`.
InList(Slot, Vec<Bson>),
/// `makeBsonObj`'s spec: the fields it keeps (`Closed`, the rest
/// dropped) or drops (`Open`, the rest kept), in the projection's order.
ObjSpec { fields: Vec<String>, open: bool },
}

/// The mask `typeMatch` and `fillType` take for null and undefined:
/// `getBSONTypeMask(jstNULL) | getBSONTypeMask(Undefined)`.
pub(crate) const NULLISH_MASK: u32 = 1088;

impl Expr {
/// The `InList`s the expression reads, in the order it prints them.
pub fn in_lists(&self) -> Vec<(Slot, Vec<Bson>)> {
let mut out = Vec::new();
self.collect_lists(&mut out);
out
}

fn collect_lists(&self, out: &mut Vec<(Slot, Vec<Bson>)>) {
match self {
Expr::InList(s, values) => out.push((*s, values.clone())),
Expr::Func(_, args) => args.iter().for_each(|a| a.collect_lists(out)),
Expr::Bin(_, l, r) => {
l.collect_lists(out);
r.collect_lists(out);
}
Expr::If(c, t, e) => {
c.collect_lists(out);
t.collect_lists(out);
e.collect_lists(out);
}
Expr::Not(e) | Expr::TypeMatch(e, _) | Expr::ConvertInt32(e) => e.collect_lists(out),
Expr::Lambda { body, .. } => body.collect_lists(out),
Expr::Slot(_)
| Expr::Local { .. }
| Expr::Const(_)
| Expr::Int64(_)
| Expr::ObjSpec { .. } => {}
}
}

pub fn null() -> Expr {
Expr::Const(Some(Bson::Null))
}
Expand Down Expand Up @@ -326,6 +362,9 @@ impl Expr {
}
_ => None,
},
Expr::InList(_, values) => Some(Bson::Array(values.clone())),
// Read only by the `makeBsonObj` it is the spec of.
Expr::ObjSpec { .. } => None,
Expr::Func(name, args) => self.call(name, args, env),
}
}
Expand Down Expand Up @@ -375,6 +414,39 @@ impl Expr {
let vals: Vec<Value> = args.iter().map(|a| a.eval(env)).collect();
new_obj(&vals)
}
// `isMember(v, inList)`: `v` equals one of the list's values
// under BSON comparison; Nothing is a member of nothing.
"isMember" => {
let v = args[0].eval(env);
let list = args[1].eval(env);
match (v, list) {
(Some(v), Some(Bson::Array(list))) => {
Some(Bson::Boolean(list.iter().any(|m| {
compare(&Some(m.clone()), &Some(v.clone())) == Ordering::Equal
})))
}
(None, Some(_)) => Some(Bson::Boolean(false)),
_ => None,
}
}
// `makeBsonObj(spec, root)`: the root's fields, kept or dropped
// by the spec, in the root's order; Nothing over a non-object
// (`RetNothing`) for a closed spec, the input for an open one
// (`RetInput`).
"makeBsonObj" => {
let Expr::ObjSpec { fields, open } = &args[0] else {
return None;
};
match args[1].eval(env) {
Some(Bson::Document(d)) => Some(Bson::Document(
d.into_iter()
.filter(|(k, _)| fields.contains(k) != *open)
.collect(),
)),
other if *open => other,
_ => None,
}
}
"isNumber" => args[0].eval(env).map(|v| Bson::Boolean(is_number(&v))),
"fillType" => {
let v = args[0].eval(env);
Expand Down
Loading
Loading