Skip to content

missing=true in conditional criteria - #2236

Merged
smunini merged 3 commits into
HeliosSoftware:mainfrom
bomanaps:feat/2003-conditional-missing-criteria
Oct 25, 2026
Merged

smunini merged 3 commits into
HeliosSoftware:mainfrom
bomanaps:feat/2003-conditional-missing-criteria

Conversation

@bomanaps

Copy link
Copy Markdown
Contributor

Evaluate :missing=true in MongoDB transaction and batch conditional criteria (ifNoneExist, conditional update/delete) inside the transaction's session with the same semantics as search, instead of rejecting the bundle fixes #2003.

@bomanaps
bomanaps force-pushed the feat/2003-conditional-missing-criteria branch from b873e6a to fcf648b Compare October 25, 2026 11:41
@bomanaps
bomanaps force-pushed the feat/2003-conditional-missing-criteria branch from fcf648b to 3e606f3 Compare October 25, 2026 12:26
…eliosSoftware#2003)

Conditional criteria (ifNoneExist, conditional update/delete) computed the
absent-id set for the whole resource type before any other criterion
narrowed it. At 310k Patients, `identifier=X&gender:missing=true` took
6-13 s per entry on the metadata route. On the value route it failed with
TooManyResults even though identifier matched one resource. The id sets
also had no path below the 16 MB document limit.

The check now follows search (HeliosSoftware#2004):

- The criteria are classified once per entry, after the same freshness
  probe search runs (HeliosSoftware#1999, HeliosSoftware#2007).
- In the index-driven pager, :missing=true is applied to each 128-id
  candidate batch after the other criteria.
- With nothing else to drive from (only _id, _lastUpdated and
  :missing=true), an unsorted cursor is read in 512-id batches and stops
  at the second match. Metadata-answered criteria also narrow the cursor.
- metadata_missing_ids hints idx_resources_identity when it is given
  candidates. Otherwise the planner walked idx_resources_missing_params,
  reading every absent key of the type for each batch (HeliosSoftware#2233).

At 310k Patients (release, mongo 7.0 rs):

- identifier+:missing=true: 6.4-13 s -> 3 ms with metadata; with
  metadata off it no longer fails (2 ms).
- broad :missing=true alone: 11.9 s -> 23 ms.
- rare :missing=true alone: 2 ms with metadata; 1.2 s with metadata off
  (a full type pass).
- Criteria without :missing and the GET shapes are unchanged.
@smunini

smunini commented Oct 25, 2026

Copy link
Copy Markdown
Contributor

Thanks @bomanaps. The semantics are right: the routes match search, writes earlier in the same Bundle are visible, and the freshness, opt-out and index-build gates from #2181/#2209/#2001/#2211 all apply. Without this PR both of its tests fail with unsupported modifier 'missing=true'; with it they pass.

Benchmarking turned up a scaling problem, so I pushed a fix to this branch. Your rebased commit is kept, followed by a merge of current main (28cf9b3) and the fix (2563f80).

The problem

absent_ids_in_bundle_transaction built the absent-id set for the whole resource type before any other criterion narrowed it, then intersected it with the candidates. Search stopped working that way in #2004. As a result:

  • Metadata route: a fetch plus a $lookup for every absent resource of the type, on every conditional entry.
  • Value route (metadata off or stale): it lists every live id, so past 300k resources it fails with TooManyResults, even when identifier matches exactly one.
  • No ceiling: the metadata route had no cap, so its $in / distinct would eventually hit MongoDB's 16 MB document limit.

The fix

  • The :missing=true criteria are classified once per entry, after the same freshness probe search runs.
  • They are applied to each candidate batch after the other criteria: the 128-id batches of the index pager.
  • When nothing else can drive (only _id / _lastUpdated alongside :missing=true), an unsorted cursor is read in 512-id batches and stops at the second match, which is all ifNoneExist and conditional update/delete need. When metadata answers the parameter, the cursor itself is narrowed by the missing_params condition.
  • Hint in a shared function: metadata_missing_ids now hints idx_resources_identity when it is given candidates. The profiler showed that, without the hint, checking a 512-id batch walked idx_resources_missing_params at about 113k keys per batch (the MongoDB: broad :missing=true on missing_params metadata scans every index key and runs a $lookup per match (slower than the value route) #2233 shape). Search calls the same function; the GET shapes below are unchanged.
  • New test: mongodb_missing_conditional_criteria_cross_candidate_batches puts the absent resource after 600 present ones, past the first batch. It covers 1 match, 2+ matches (412) and 0 matches, with metadata on and off.
  • Docs: the paragraph in docs/mongodb/search-indexes.md now describes per-batch evaluation.

Numbers

310k Patients, half without gender. Release build, mongo 7.0 replica set. Each row is a single-entry transaction whose ifNoneExist matches one existing resource; main and this branch were run alternately.

ifNoneExist criteria as submitted this branch GET, same criteria
identifier=X&gender:missing=true, metadata on 6.4–13 s 3 ms 1.3–1.7 ms
identifier=X&gender:missing=true, metadata off fails with found 300001, maximum is 300000 2 ms 0.7 ms
_id=X&gender:missing=true 11.3 s 2–3 ms –
gender:missing=true alone, ~155k match (412) 11.9 s, or the 300k error with metadata off 23 ms, 4 ms with metadata off –
birthdate:missing=true alone, 1 match – 2 ms, 1.2 s with metadata off –
criteria without :missing=true – same as main –
GET active=true&gender:missing=true, _count=50 – 5 ms / 11 ms (main: 5 ms / 10 ms) –

At 50k Patients the submitted version already took about 900 ms per entry.

The remaining slow case is a criterion with only :missing=true that matches almost nothing, with metadata off. It has to read the whole type, at about 1.2 s for 310k, which is roughly what the same GET costs. It now finishes instead of erroring.

Checks

  • Clippy with the CI flags: clean. Lib tests (backends::mongodb): 505/505.
  • Full mongodb_tests on the fix: 529 passed, 0 failed. Locally it had to be split in two because of the known createIndexes stall with ~320 databases in one container, which happens on main too.
  • On the pushed tree: missing_index 25/25, and the transaction / conditional / ifNoneExist tests 78/78.

@smunini
smunini merged commit c26c24b into HeliosSoftware:main Oct 25, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

mongodb transactions: support :missing=true in conditional criteria (ifNoneExist, conditional update/delete)

2 participants