LCOV - code coverage report
Current view: top level - antares-api/src/batch - write.rs (source / functions) Coverage Total Hit
Test: merged.info Lines: 95.8 % 593 568
Test Date: 2026-11-02 13:21:27 Functions: 60.3 % 348 210

            Line data    Source code
       1              : // SPDX-License-Identifier: EUPL-1.2
       2              : //! Batch Entity Create, Upsert, Update and Merge (5.6.7, 5.6.8, 5.6.9,
       3              : //! 5.6.20): one [`BatchRun`] takes the array through its phases in order,
       4              : //! the per-member decision, the expansion of each member, the chronological
       5              : //! rounds, the one multi-row create, the distributed arm and the S/E outcome.
       6              : 
       7              : use super::*;
       8              : 
       9              : // ---------- POST /entityOperations/create (5.6.7) ----------
      10              : 
      11          210 : pub async fn batch_create(
      12          210 :     State(st): State<AppState>,
      13          210 :     CleanParams(params): CleanParams,
      14          210 :     headers: HeaderMap,
      15          210 :     body: Bytes,
      16          210 : ) -> Response {
      17          210 :     match batch_write(&st, &params, &headers, &body, BatchMode::Create).await {
      18          170 :         Ok(r) => r,
      19           40 :         Err(e) => e.into_response(),
      20              :     }
      21          210 : }
      22              : 
      23          202 : pub async fn batch_upsert(
      24          202 :     State(st): State<AppState>,
      25          202 :     CleanParams(params): CleanParams,
      26          202 :     headers: HeaderMap,
      27          202 :     body: Bytes,
      28          202 : ) -> Response {
      29          202 :     match batch_write(&st, &params, &headers, &body, BatchMode::Upsert).await {
      30          188 :         Ok(r) => r,
      31           14 :         Err(e) => e.into_response(),
      32              :     }
      33          202 : }
      34              : 
      35           96 : pub async fn batch_update(
      36           96 :     State(st): State<AppState>,
      37           96 :     CleanParams(params): CleanParams,
      38           96 :     headers: HeaderMap,
      39           96 :     body: Bytes,
      40           96 : ) -> Response {
      41           96 :     match batch_write(&st, &params, &headers, &body, BatchMode::Update).await {
      42           88 :         Ok(r) => r,
      43            8 :         Err(e) => e.into_response(),
      44              :     }
      45           96 : }
      46              : 
      47              : /// 5.6.20 Batch Entity Merge: each entity merged per 5.6.17 locally;
      48              : /// 204 when all succeed, 207 with S/E arrays otherwise (5.6.20.5).
      49           74 : pub async fn batch_merge(
      50           74 :     State(st): State<AppState>,
      51           74 :     CleanParams(params): CleanParams,
      52           74 :     headers: HeaderMap,
      53           74 :     body: Bytes,
      54           74 : ) -> Response {
      55           74 :     match batch_write(&st, &params, &headers, &body, BatchMode::Merge).await {
      56           58 :         Ok(r) => r,
      57           16 :         Err(e) => e.into_response(),
      58              :     }
      59           74 : }
      60              : 
      61              : /// The 6.3.7 `options` of a batch write: a comma separated list of strings,
      62              : /// so a mode is selected when it appears as ONE MEMBER of the list, never by
      63              : /// whole-string equality. 6.15.3.1 replace (default) | update for upsert;
      64              : /// 6.16.3.1 noOverwrite for update.
      65          582 : fn write_opts(params: &HashMap<String, String>, mode: BatchMode) -> WriteOpts {
      66         1164 :     let has_option = |name: &str| {
      67         1164 :         params
      68         1164 :             .get("options")
      69         1164 :             .is_some_and(|o| o.split(',').any(|s| s.trim() == name))
      70         1164 :     };
      71          582 :     WriteOpts {
      72          582 :         mode,
      73          582 :         update: has_option("update"),
      74          582 :         no_overwrite: has_option("noOverwrite"),
      75          582 :     }
      76          582 : }
      77              : 
      78          582 : async fn batch_write(
      79          582 :     st: &AppState,
      80          582 :     params: &HashMap<String, String>,
      81          582 :     headers: &HeaderMap,
      82          582 :     body: &[u8],
      83          582 :     mode: BatchMode,
      84          582 : ) -> ApiResult<Response> {
      85          582 :     let tenant = tenant_from(headers)?;
      86          582 :     check_params(params, &["options", "local"])?;
      87              :     // a batch runs only inside the in-flight budget, held until it answers.
      88          582 :     let _admitted = crate::bounds::admit_batch(body.len())
      89          582 :         .await
      90          582 :         .ok_or(ApiError::Overloaded(crate::negotiate::RETRY_AFTER_SECONDS))?;
      91          582 :     let opts = write_opts(params, mode);
      92          582 :     let mut items = parse_batch(st, &tenant, headers, body).await?;
      93          522 :     let mut run = BatchRun {
      94          522 :         st,
      95          522 :         tenant,
      96          522 :         opts,
      97          522 :         out: BatchOutcome::new(),
      98          522 :         conds: Conditions::default(),
      99          522 :         created_ids: Vec::new(),
     100          522 :         any_created: false,
     101          522 :     };
     102          522 :     run.decide(headers, &mut items).await?;
     103          510 :     let (spec, fwd_items) = batch_spec(&items);
     104          510 :     let fed_regs = match crate::federation::write_plan(
     105          510 :         st,
     106          510 :         &run.tenant,
     107          510 :         &spec,
     108          510 :         &st.loader.core(),
     109          510 :         params,
     110          510 :         headers,
     111          510 :     )
     112          510 :     .await?
     113              :     {
     114            0 :         crate::federation::WritePlan::Answered(r) => return Ok(*r),
     115          508 :         crate::federation::WritePlan::Forward(regs) => regs,
     116              :     };
     117          508 :     run.conds.unforwarded(!fed_regs.is_empty())?;
     118          506 :     let proxies: Vec<&crate::federation::FedReg> =
     119          506 :         fed_regs.iter().filter(|r| r.is_proxy()).collect();
     120          506 :     let (creates, prepped) = run.prep(items, &proxies);
     121          506 :     let prepped = run.new_comma_ids(prepped).await?;
     122          506 :     for round in rounds(prepped) {
     123          300 :         run.round(round).await?;
     124              :     }
     125          504 :     run.create(creates).await?;
     126              :     // Distributed arm (5.6.7.4/5.6.8.4/5.6.9.4/5.6.20.4): remote outcomes
     127              :     // merge into the same S/E arrays as local ones — the response body
     128              :     // carries Entity IDs and BatchEntityErrors (5.2.16/5.2.17), never
     129              :     // opaque part descriptors.
     130          504 :     if !fed_regs.is_empty() {
     131          106 :         let remote = forward_batch(
     132          106 :             st,
     133          106 :             params,
     134          106 :             headers,
     135          106 :             &run.tenant,
     136          106 :             opts,
     137          106 :             &fed_regs,
     138          106 :             &fwd_items,
     139          106 :         )
     140          106 :         .await;
     141          106 :         run.merge_remote(remote);
     142          398 :     }
     143          504 :     Ok(run.finish())
     144          582 : }
     145              : 
     146              : /// A member as the engine is shown it: its id, its pre-image and its
     147              : /// expanded body.
     148              : type Shown = (Option<String>, crate::policy::PreImage, Option<Value>);
     149              : 
     150              : /// One member of a batch, as the store writes it: the id and the expanded
     151              : /// document.
     152              : type Member = (String, Value);
     153              : 
     154              : /// One batch write in flight: what its phases read and fill.
     155              : struct BatchRun<'a> {
     156              :     st: &'a AppState,
     157              :     tenant: antares_model::TenantId,
     158              :     opts: WriteOpts,
     159              :     out: BatchOutcome,
     160              :     /// The conditions the engine set on the allowed members.
     161              :     conds: Conditions,
     162              :     /// The ids the batch created, local and forwarded, each once (5.6.8.5).
     163              :     created_ids: Vec<String>,
     164              :     any_created: bool,
     165              : }
     166              : 
     167              : impl BatchRun<'_> {
     168              :     /// ADR-0020 per member: before any item is written and before
     169              :     /// any forward, the engine decides each member as the single-Entity write
     170              :     /// would — its id, its types (stored and written), its Attributes and its
     171              :     /// expanded body. A refused member lands in E with the AccessDenied
     172              :     /// problem and the rest are applied (207); a batch refused whole is the
     173              :     /// 403 it always was. An entry that does not expand is shown by its id
     174              :     /// alone and left to the local arm to report.
     175          522 :     async fn decide(&mut self, headers: &HeaderMap, items: &mut Vec<BatchItem>) -> ApiResult<()> {
     176          522 :         let st = self.st;
     177          522 :         let Some(engine) = &st.policy else {
     178          462 :             return Ok(());
     179              :         };
     180           60 :         let shown = self.pre_images(members(items)).await?;
     181           56 :         let attrs: Vec<Vec<String>> = shown
     182           56 :             .iter()
     183          248 :             .map(|(_, _, b)| written_attrs(b.as_ref()))
     184           56 :             .collect();
     185           56 :         let ids: Vec<Vec<&str>> = shown
     186           56 :             .iter()
     187          248 :             .map(|(id, _, _)| id.as_deref().into_iter().collect())
     188           56 :             .collect();
     189           56 :         let ops: Vec<crate::policy::Operation<'_>> = shown
     190           56 :             .iter()
     191           56 :             .zip(&ids)
     192           56 :             .zip(&attrs)
     193           56 :             .map(|(((_, pre, body), ids), attrs)| crate::policy::Operation {
     194          248 :                 ids,
     195          248 :                 types: &pre.types,
     196          248 :                 attrs,
     197          248 :                 body: body.as_ref(),
     198          248 :                 stored: pre.stored.as_ref(),
     199          248 :                 ..crate::policy::Operation::new(self.opts.mode.clause())
     200          248 :             })
     201           56 :             .collect();
     202           56 :         let asking = crate::snapshots::asking_tenant(st, &self.tenant).await;
     203           56 :         let refused = crate::policy::gate_each(engine.as_ref(), &asking, headers, &ops).await;
     204           56 :         drop(ops);
     205           56 :         if let Some(why) = whole_refusal(&refused) {
     206            8 :             return Err(crate::policy::Denied(why).into());
     207           48 :         }
     208           48 :         let mut verdicts = refused.into_iter().zip(shown);
     209           48 :         let (out, conds) = (&mut self.out, &mut self.conds);
     210          238 :         items.retain(|_| match verdicts.next() {
     211           10 :             Some((Err(why), (id, _, _))) => {
     212           10 :                 out.errors.push(denied_entry(id.as_deref(), &why));
     213           10 :                 false
     214              :             }
     215          228 :             Some((Ok(granted), (id, _, _))) => {
     216          228 :                 conds.add(id.as_deref(), &granted);
     217          228 :                 true
     218              :             }
     219            0 :             None => true,
     220          238 :         });
     221           48 :         Ok(())
     222          522 :     }
     223              : 
     224              :     /// Each member with its pre-image, from one read for every member's
     225              :     /// stored Entity and, under a view, every Relationship target the batch
     226              :     /// names; a store failure fails the batch (fail-closed,
     227              :     /// F-ANT2-009), never reads as absent.
     228           60 :     async fn pre_images(
     229           60 :         &mut self,
     230           60 :         members: Vec<(Option<String>, Option<Value>)>,
     231           60 :     ) -> Result<Vec<Shown>, NgsiError> {
     232          252 :         let mut wanted: Vec<&str> = members.iter().filter_map(|(id, _)| id.as_deref()).collect();
     233           60 :         wanted.sort_unstable();
     234           60 :         wanted.dedup();
     235           60 :         let stored = if wanted.is_empty() {
     236            0 :             HashMap::new()
     237              :         } else {
     238           60 :             self.st.store.get_entities(&self.tenant, &wanted).await?
     239              :         };
     240           56 :         let shown: Vec<Shown> = members
     241           56 :             .into_iter()
     242          248 :             .map(|(id, body)| {
     243          248 :                 let pre = match &id {
     244          248 :                     Some(id) => crate::policy::image(stored.get(id).cloned(), body.as_ref()),
     245            0 :                     None => crate::policy::PreImage::default(),
     246              :                 };
     247          248 :                 (id, pre, body)
     248          248 :             })
     249           56 :             .collect();
     250           56 :         Ok(shown)
     251           60 :     }
     252              : 
     253              :     /// Expansion and validation per member, ahead of every store operation:
     254              :     /// the creates, and the members the rounds write. A member that fails
     255              :     /// lands in E; one wholly proxied has no local part.
     256          506 :     fn prep(
     257          506 :         &mut self,
     258          506 :         items: Vec<BatchItem>,
     259          506 :         proxies: &[&crate::federation::FedReg],
     260          506 :     ) -> (Vec<Member>, Vec<Member>) {
     261              :         // Creates are collected and written as ONE multi-row statement;
     262              :         // upsert/update/merge run batched per round.
     263              :         // 5.5.11.0: "All Entities and Attributes in the batch will get the same
     264              :         // modifiedAt timestamp" — the clock is read once for the whole array, not
     265              :         // per document. Stamping per document spreads a large create over several
     266              :         // milliseconds, and a Context Consumer filtering or paging on modifiedAt
     267              :         // then sees one batch as two. The per-round stamp is the separate
     268              :         // case: repeated instances of one id are sequential operations (5.5.11.2,
     269              :         // 5.5.11.5) and do carry their own instants.
     270          506 :         let create_ts = now_iso();
     271          506 :         let (mut creates, mut prepped) = (Vec::new(), Vec::new());
     272         6252 :         for (item, ctx) in items {
     273         6252 :             let id_hint = item.get("id").and_then(Value::as_str).map(str::to_owned);
     274         6252 :             let ctx = match ctx {
     275         5836 :                 Ok(c) => c,
     276          416 :                 Err(e) => {
     277          416 :                     self.out
     278          416 :                         .errors
     279          416 :                         .push(err_entry(id_hint.as_deref(), &ngsi_of(e)));
     280          416 :                     continue;
     281              :                 }
     282              :             };
     283         5836 :             let Some(item) = local_part(item, proxies, &ctx) else {
     284           92 :                 continue; // wholly proxied: no local part for this item
     285              :             };
     286         5744 :             let (prep, into) = if self.opts.mode == BatchMode::Create {
     287         5212 :                 (prep_create(&item, &ctx, &create_ts), &mut creates)
     288              :             } else {
     289          532 :                 (prep_write(&item, &ctx, self.opts.mode), &mut prepped)
     290              :             };
     291         5744 :             match prep {
     292         5716 :                 Ok(pair) => into.push(pair),
     293           28 :                 Err(e) => self.out.errors.push(err_entry(id_hint.as_deref(), &e)),
     294              :             }
     295              :         }
     296          506 :         (creates, prepped)
     297          506 :     }
     298              : 
     299              :     /// An upsert creates what is missing, so the create rule on a comma id
     300              :     /// holds for a member whose Entity is not stored; one
     301              :     /// already stored under such an id is still updated, so a source that
     302              :     /// keeps upserting a legacy comma id does not turn red.
     303          506 :     async fn new_comma_ids(&mut self, prepped: Vec<Member>) -> Result<Vec<Member>, NgsiError> {
     304          506 :         let comma: Vec<&str> = prepped
     305          506 :             .iter()
     306          526 :             .map(|(id, _)| id.as_str())
     307          526 :             .filter(|id| id.contains(','))
     308          506 :             .collect();
     309          506 :         if self.opts.mode != BatchMode::Upsert || comma.is_empty() {
     310          498 :             return Ok(prepped);
     311            8 :         }
     312            8 :         let stored = self.st.store.get_entities(&self.tenant, &comma).await?;
     313            8 :         let mut kept = Vec::with_capacity(prepped.len());
     314            8 :         for (id, doc) in prepped {
     315            8 :             match antares_jsonld::new_entity_id(&doc) {
     316            8 :                 Err(e) if !stored.contains_key(&id) => {
     317            4 :                     self.out.errors.push(err_entry(Some(&id), &e));
     318            4 :                 }
     319            4 :                 _ => kept.push((id, doc)),
     320              :             }
     321              :         }
     322            8 :         Ok(kept)
     323          506 :     }
     324              : 
     325              :     /// One round of an upsert, update or merge, every id in it unique, its
     326              :     /// store operations batched: one transaction per round instead of one
     327              :     /// per item.
     328          300 :     async fn round(&mut self, round: Vec<Member>) -> Result<(), NgsiError> {
     329          300 :         let ts = now_iso();
     330          300 :         match self.opts.mode {
     331              :             // Creates are collected apart and never reach a round; a request
     332              :             // handler answers, it does not panic.
     333              :             BatchMode::Create => {
     334            0 :                 for (id, _) in round {
     335            0 :                     self.out.errors.push(err_entry(
     336            0 :                         Some(&id),
     337            0 :                         &NgsiError::InternalError(
     338            0 :                             "batch create is handled before the rounds".into(),
     339            0 :                         ),
     340            0 :                     ));
     341            0 :                 }
     342            0 :                 Ok(())
     343              :             }
     344          180 :             BatchMode::Upsert => self.upsert(round, &ts).await,
     345          120 :             BatchMode::Update | BatchMode::Merge => self.update(round, &ts).await,
     346              :         }
     347          300 :     }
     348              : 
     349          180 :     async fn upsert(&mut self, round: Vec<Member>, ts: &str) -> Result<(), NgsiError> {
     350          180 :         let replaces = if self.opts.update {
     351           48 :             self.upsert_merge(round, ts).await?
     352              :         } else {
     353          132 :             self.replace_conditioned(round, ts).await?
     354              :         };
     355          180 :         if !replaces.is_empty() {
     356          146 :             self.replace(replaces, ts).await?;
     357           34 :         }
     358          178 :         Ok(())
     359          180 :     }
     360              : 
     361              :     /// options=update: update existing rows first (Update Attributes); ids that turn out
     362              :     /// absent (or vanish mid-flight) fall through to the replace batch —
     363              :     /// never a silent success (TOCTOU fix). The members left to replace.
     364           48 :     async fn upsert_merge(
     365           48 :         &mut self,
     366           48 :         round: Vec<Member>,
     367           48 :         ts: &str,
     368           48 :     ) -> Result<Vec<Member>, NgsiError> {
     369           74 :         let ids: Vec<String> = round.iter().map(|(id, _)| id.clone()).collect();
     370           74 :         let docs: HashMap<&str, &Value> = round.iter().map(|(id, d)| (id.as_str(), d)).collect();
     371           48 :         let conds = &self.conds;
     372           48 :         let res = self
     373           48 :             .st
     374           48 :             .store
     375           54 :             .batch_mutate(&self.tenant, &ids, |id, doc| {
     376           54 :                 crate::history::restage(id);
     377           54 :                 conds.pre(id, doc)?;
     378           50 :                 let src = member(&docs, id)?;
     379           50 :                 let before = doc.clone();
     380              :                 // 10.3.3.4: update mode runs Update Attributes (10.2.3.4),
     381              :                 // which replaces each instance it names whole
     382           50 :                 crate::entities::update_into(doc, src, ts);
     383              :                 // a member that only restates the stored state
     384              :                 // (a parked vehicle, a poll that saw nothing
     385              :                 // new) is no change (4.8, 5.8.3): handing the
     386              :                 // store the document unchanged writes no row,
     387              :                 // version, history point or notification
     388           50 :                 if same_but_stamps(&before, doc) {
     389            6 :                     *doc = before;
     390           44 :                 }
     391           50 :                 Ok::<(), NgsiError>(())
     392           54 :             })
     393           48 :             .await;
     394           48 :         let res = match res {
     395           48 :             Ok(res) => res,
     396            0 :             Err(e) => return self.step_failed(&ids, e).map(|()| Vec::new()),
     397              :         };
     398           48 :         crate::history::settle_all(&ids, &res);
     399           48 :         let mut replaces = Vec::new();
     400           74 :         for ((id, expanded), r) in round.iter().zip(res) {
     401           20 :             match r {
     402            4 :                 Some(Err(e)) => self.out.errors.push(err_entry(Some(id), &e)),
     403           50 :                 Some(Ok(())) => self.out.succeed(id),
     404           20 :                 None if self.conds.has(id) => self
     405            2 :                     .out
     406            2 :                     .errors
     407            2 :                     .push(err_entry(Some(id), &Conditions::not_found(id))),
     408           18 :                 None => replaces.push((id.clone(), expanded.clone())),
     409              :             }
     410              :         }
     411           48 :         Ok(replaces)
     412           48 :     }
     413              : 
     414              :     /// A replace-upsert: a conditioned member is replaced only inside the
     415              :     /// store's read of it, where its condition is judged. The free members,
     416              :     /// left to replace.
     417          132 :     async fn replace_conditioned(
     418          132 :         &mut self,
     419          132 :         round: Vec<Member>,
     420          132 :         ts: &str,
     421          132 :     ) -> Result<Vec<Member>, NgsiError> {
     422          132 :         let (conditioned, free): (Vec<_>, Vec<_>) =
     423          224 :             round.into_iter().partition(|(id, _)| self.conds.has(id));
     424          132 :         if conditioned.is_empty() {
     425          128 :             return Ok(free);
     426            4 :         }
     427            8 :         let ids: Vec<String> = conditioned.iter().map(|(id, _)| id.clone()).collect();
     428            4 :         let docs: HashMap<&str, &Value> =
     429            8 :             conditioned.iter().map(|(id, d)| (id.as_str(), d)).collect();
     430            4 :         let conds = &self.conds;
     431            4 :         let res = self
     432            4 :             .st
     433            4 :             .store
     434            6 :             .batch_mutate(&self.tenant, &ids, |id, doc| {
     435            6 :                 conds.pre(id, doc)?;
     436            2 :                 let src = member(&docs, id)?;
     437            2 :                 *doc = replacement(src, doc, ts);
     438            2 :                 Ok::<(), NgsiError>(())
     439            6 :             })
     440            4 :             .await;
     441            4 :         let res = match res {
     442            4 :             Ok(res) => res,
     443            0 :             Err(e) => return self.step_failed(&ids, e).map(|()| free),
     444              :         };
     445            8 :         for (id, r) in ids.iter().zip(res) {
     446            6 :             match r {
     447            2 :                 Some(Ok(())) => self.out.succeed(id),
     448            4 :                 Some(Err(e)) => self.out.errors.push(err_entry(Some(id), &e)),
     449            2 :                 None => self
     450            2 :                     .out
     451            2 :                     .errors
     452            2 :                     .push(err_entry(Some(id), &Conditions::not_found(id))),
     453              :             }
     454              :         }
     455            4 :         Ok(free)
     456          132 :     }
     457              : 
     458              :     /// The replace batch of an upsert round: absent ids are created.
     459          146 :     async fn replace(&mut self, mut replaces: Vec<Member>, ts: &str) -> Result<(), NgsiError> {
     460          234 :         for (_, doc) in replaces.iter_mut() {
     461          234 :             stamp_new(doc, ts);
     462          234 :         }
     463              :         // ids first, then MOVE the documents into the store —
     464              :         // the loop below only needs the ids, so the batch payload
     465              :         // is never deep-cloned on the ingest hot path.
     466          234 :         let ids: Vec<String> = replaces.iter().map(|(id, _)| id.clone()).collect();
     467          146 :         let flags = match self.st.store.batch_upsert(&self.tenant, replaces).await {
     468          142 :             Ok(flags) => flags,
     469            4 :             Err(e) => return self.step_failed(&ids, e),
     470              :         };
     471          230 :         for (id, created) in ids.into_iter().zip(flags) {
     472          230 :             self.out.succeed(&id);
     473          230 :             if created {
     474          112 :                 self.created(id);
     475          124 :             }
     476              :         }
     477          142 :         Ok(())
     478          146 :     }
     479              : 
     480              :     /// One round of a Batch Update or Merge.
     481          120 :     async fn update(&mut self, round: Vec<Member>, ts: &str) -> Result<(), NgsiError> {
     482          210 :         let ids: Vec<String> = round.iter().map(|(id, _)| id.clone()).collect();
     483          210 :         let docs: HashMap<&str, &Value> = round.iter().map(|(id, d)| (id.as_str(), d)).collect();
     484              :         // batch update with noOverwrite: existing attribute instances
     485              :         // are left alone; if any existed, the entity is a partial
     486              :         // failure (005_02 ⇒ 207)
     487          120 :         let mut skipped: std::collections::HashSet<String> = Default::default();
     488          120 :         let (conds, opts) = (&self.conds, self.opts);
     489          120 :         let res = self
     490          120 :             .st
     491          120 :             .store
     492          180 :             .batch_mutate(&self.tenant, &ids, |id, doc| {
     493          180 :                 crate::history::restage(id);
     494          180 :                 conds.pre(id, doc)?;
     495          176 :                 let expanded = member(&docs, id)?;
     496          176 :                 if opts.mode == BatchMode::Update && opts.no_overwrite {
     497           46 :                     if append_new_instances(doc, expanded, ts)? {
     498           20 :                         skipped.insert(id.to_owned());
     499           26 :                     }
     500          130 :                 } else if opts.mode == BatchMode::Update {
     501           62 :                     crate::entities::update_into(doc, expanded, ts);
     502           68 :                 } else {
     503           68 :                     merge_into(doc, expanded, ts);
     504           68 :                 }
     505          176 :                 Ok::<(), NgsiError>(())
     506          180 :             })
     507          120 :             .await;
     508          120 :         let res = match res {
     509          120 :             Ok(res) => res,
     510            0 :             Err(e) => return self.step_failed(&ids, e),
     511              :         };
     512          120 :         crate::history::settle_all(&ids, &res);
     513          210 :         for ((id, _), r) in round.iter().zip(res) {
     514          176 :             match r {
     515           30 :                 None => self.out.errors.push(err_entry(
     516           30 :                     Some(id),
     517           30 :                     &NgsiError::ResourceNotFound(format!("entity {id} not found")),
     518              :                 )),
     519            4 :                 Some(Err(e)) => self.out.errors.push(err_entry(Some(id), &e)),
     520          176 :                 Some(Ok(())) if skipped.contains(id) => self.out.errors.push(err_entry(
     521           20 :                     Some(id),
     522           20 :                     &NgsiError::BadRequestData(
     523           20 :                         "some attributes already existed (noOverwrite)".into(),
     524           20 :                     ),
     525              :                 )),
     526          156 :                 Some(Ok(())) => self.out.succeed(id),
     527              :             }
     528              :         }
     529          120 :         Ok(())
     530          120 :     }
     531              : 
     532              :     /// The collected creates, one multi-row statement, one transaction.
     533          504 :     async fn create(&mut self, creates: Vec<Member>) -> Result<(), NgsiError> {
     534          504 :         if creates.is_empty() {
     535          372 :             return Ok(());
     536          132 :         }
     537         5204 :         let ids: Vec<String> = creates.iter().map(|(id, _)| id.clone()).collect();
     538          132 :         let flags = match self.st.store.batch_create(&self.tenant, creates).await {
     539          132 :             Ok(flags) => flags,
     540            0 :             Err(e) => return self.step_failed(&ids, e),
     541              :         };
     542         5204 :         for (id, created) in ids.into_iter().zip(flags) {
     543         5204 :             if created {
     544         5190 :                 self.out.succeed(&id);
     545         5190 :                 self.created(id);
     546         5190 :             } else {
     547           14 :                 self.out.errors.push(err_entry(
     548           14 :                     Some(&id),
     549           14 :                     &NgsiError::AlreadyExists(format!("entity {id} already exists")),
     550           14 :                 ));
     551           14 :             }
     552              :         }
     553          132 :         Ok(())
     554          504 :     }
     555              : 
     556              :     /// A store error in one step (one transaction) of the batch. Before any
     557              :     /// member succeeded it is the request's answer, as it always was; after
     558              :     /// one did, that member's write stands, so the members of this step go to
     559              :     /// E with the error and the batch goes on to its 207 (5.6.7.4, 5.6.8.4:
     560              :     /// a BatchEntityError per Entity whose write failed). A whole-request
     561              :     /// error there told the client nothing was applied, and its retry
     562              :     /// replayed what was.
     563            4 :     fn step_failed(&mut self, ids: &[String], e: NgsiError) -> Result<(), NgsiError> {
     564            4 :         if self.out.success.is_empty() {
     565            2 :             return Err(e);
     566            2 :         }
     567            2 :         for id in ids {
     568            2 :             self.out.errors.push(err_entry(Some(id), &e));
     569            2 :         }
     570            2 :         Ok(())
     571            4 :     }
     572              : 
     573              :     /// What the forwards brought back joins S and E after the local outcome.
     574          106 :     fn merge_remote(&mut self, remote: Remote) {
     575          216 :         for (id, was_created) in remote.ok {
     576          216 :             self.out.succeed(&id);
     577          216 :             if was_created {
     578           42 :                 // 5.6.8.4: one Entity ID per Entity, the local and the forwarded
     579           42 :                 // part of one Entity included
     580           42 :                 self.created(id);
     581          174 :             }
     582              :         }
     583          106 :         self.out.errors.extend(remote.err);
     584          106 :     }
     585              : 
     586         5344 :     fn created(&mut self, id: String) {
     587         5344 :         self.any_created = true;
     588         5344 :         if !self.created_ids.contains(&id) {
     589         5326 :             self.created_ids.push(id);
     590         5326 :         }
     591         5344 :     }
     592              : 
     593              :     /// 5.6.7.5, 5.6.8.5, 5.6.9.5, 5.6.20.5: the all-ok status, else 207.
     594          504 :     fn finish(mut self) -> Response {
     595              :         // 5.6.8.5: the created-only S array is the ALL-SUCCEEDED reading ("if all
     596              :         // Entities not existing prior to this request have been successfully
     597              :         // created and the others have been successfully updated"). Once E is
     598              :         // non-empty the third bullet applies and S is "the list of Entities
     599              :         // successfully created or updated".
     600          504 :         if self.opts.mode == BatchMode::Upsert && self.any_created && self.out.errors.is_empty() {
     601           78 :             self.out.success = self.created_ids.into_iter().map(Value::String).collect();
     602          426 :         }
     603          504 :         let (status, body_on_ok) = match self.opts.mode {
     604          170 :             BatchMode::Create => (StatusCode::CREATED, true),
     605           88 :             BatchMode::Upsert if self.any_created => (StatusCode::CREATED, true),
     606              :             BatchMode::Upsert | BatchMode::Update | BatchMode::Merge => {
     607          246 :                 (StatusCode::NO_CONTENT, false)
     608              :             }
     609              :         };
     610          504 :         self.out.respond(&self.tenant, status, body_on_ok)
     611          504 :     }
     612              : }
     613              : 
     614              : /// Each entry's id and, when it expands, its expanded body: what the engine
     615              : /// is shown of it.
     616           60 : fn members(items: &[BatchItem]) -> Vec<(Option<String>, Option<Value>)> {
     617           60 :     items
     618           60 :         .iter()
     619          252 :         .map(|(item, ctx)| {
     620          252 :             let id = item.get("id").and_then(Value::as_str).map(str::to_owned);
     621          252 :             let body = match (item.as_object(), ctx.as_ref()) {
     622          252 :                 (Some(o), Ok(c)) => expand_entity(o, c, ExpandOpts::default()).ok(),
     623            0 :                 _ => None,
     624              :             };
     625          252 :             (id, body)
     626          252 :         })
     627           60 :         .collect()
     628           60 : }
     629              : 
     630              : /// The Attribute names an expanded member writes.
     631          248 : fn written_attrs(body: Option<&Value>) -> Vec<String> {
     632          248 :     body.and_then(Value::as_object)
     633          248 :         .map(|o| {
     634          248 :             o.keys()
     635          738 :                 .filter(|k| !matches!(k.as_str(), "id" | "type" | "scope" | "@context"))
     636          248 :                 .cloned()
     637          248 :                 .collect()
     638          248 :         })
     639          248 :         .unwrap_or_default()
     640          248 : }
     641              : 
     642              : /// The part of a member stored here: proxied (exclusive/redirect)
     643              : /// attributes are never stored locally. None when every attribute is.
     644         5836 : fn local_part(
     645         5836 :     item: Value,
     646         5836 :     proxies: &[&crate::federation::FedReg],
     647         5836 :     ctx: &antares_jsonld::Context,
     648         5836 : ) -> Option<Value> {
     649         5836 :     if proxies.is_empty() {
     650         5696 :         return Some(item);
     651          140 :     }
     652          140 :     let Some(o) = item.as_object() else {
     653            0 :         return Some(item);
     654              :     };
     655          140 :     let (rest, has_attrs) = crate::federation::strip_proxied(o, proxies, ctx);
     656          140 :     has_attrs.then_some(Value::Object(rest))
     657         5836 : }
     658              : 
     659              : /// A create member, expanded and stamped with the batch's one instant.
     660         5212 : fn prep_create(item: &Value, ctx: &antares_jsonld::Context, ts: &str) -> Result<Member, NgsiError> {
     661         5212 :     let obj = item
     662         5212 :         .as_object()
     663         5212 :         .ok_or_else(|| NgsiError::BadRequestData("entity must be an object".into()))?;
     664         5212 :     let mut expanded = expand_entity(obj, ctx, ExpandOpts::default())?;
     665         5208 :     let id = antares_jsonld::new_entity_id(&expanded)?.to_owned();
     666         5204 :     stamp_new(&mut expanded, ts);
     667         5204 :     Ok((id, expanded))
     668         5212 : }
     669              : 
     670              : /// An upsert, update or merge member, expanded; a merge allows the null
     671              : /// that deletes an Attribute.
     672          532 : fn prep_write(
     673          532 :     item: &Value,
     674          532 :     ctx: &antares_jsonld::Context,
     675          532 :     mode: BatchMode,
     676          532 : ) -> Result<Member, NgsiError> {
     677          532 :     let obj = item
     678          532 :         .as_object()
     679          532 :         .ok_or_else(|| NgsiError::BadRequestData("entity must be an object".into()))?;
     680          528 :     let fragment_ok = mode == BatchMode::Merge;
     681          528 :     let expanded = expand_entity(
     682          528 :         obj,
     683          528 :         ctx,
     684          528 :         ExpandOpts {
     685          528 :             fragment: false,
     686          528 :             allow_null: fragment_ok,
     687          528 :             merge: fragment_ok,
     688          528 :             temporal: false,
     689          528 :             ..Default::default()
     690          528 :         },
     691           16 :     )?;
     692          512 :     let id = antares_jsonld::expanded_id(&expanded)?.to_owned();
     693          512 :     Ok((id, expanded))
     694          532 : }
     695              : 
     696              : /// 4.6.6: duplicate instances of one Entity in a batch array "shall come
     697              : /// in chronological order" — first oldest. Sequential semantics are kept
     698              : /// by splitting into rounds: the Nth occurrence of an id lands in round
     699              : /// N, rounds execute in order, and within a round every id is unique.
     700          506 : fn rounds(prepped: Vec<Member>) -> Vec<Vec<Member>> {
     701          506 :     let mut rounds: Vec<Vec<Member>> = Vec::new();
     702          506 :     let mut occurrence: HashMap<String, usize> = HashMap::new();
     703          524 :     for (id, doc) in prepped {
     704          508 :         let n = occurrence.entry(id.clone()).or_insert(0);
     705              :         // `n` never passes `rounds.len()`: each id's count grows by one
     706              :         // and a round is added the first time a count reaches it
     707          508 :         match rounds.get_mut(*n) {
     708          208 :             Some(round) => round.push((id, doc)),
     709          300 :             None => rounds.push(vec![(id, doc)]),
     710              :         }
     711          508 :         *n = n.saturating_add(1);
     712              :     }
     713          506 :     rounds
     714          506 : }
     715              : 
     716              : /// The document a store closure writes for `id`; every id the store hands
     717              : /// it is one the round names.
     718          228 : fn member<'d>(docs: &HashMap<&str, &'d Value>, id: &str) -> Result<&'d Value, NgsiError> {
     719          228 :     docs.get(id)
     720          228 :         .copied()
     721          228 :         .ok_or_else(|| NgsiError::InternalError(format!("batch member {id} lost its document")))
     722          228 : }
     723              : 
     724              : /// `src` stamped as the new state of the stored `doc`. 4.8: a replaced
     725              : /// Entity keeps the instant it was created.
     726            2 : fn replacement(src: &Value, doc: &Value, ts: &str) -> Value {
     727            2 :     let mut new = src.clone();
     728            2 :     stamp_new(&mut new, ts);
     729            2 :     if let (Some(o), Some(created)) = (new.as_object_mut(), doc.get("createdAt")) {
     730            2 :         o.insert("createdAt".into(), created.clone());
     731            2 :     }
     732            2 :     new
     733            2 : }
     734              : 
     735              : /// 5.6.9 with noOverwrite, which is instance-level: an incoming instance
     736              : /// whose datasetId the stored Attribute already holds is skipped, every
     737              : /// other one appended. Whether any instance was skipped.
     738           46 : fn append_new_instances(doc: &mut Value, expanded: &Value, ts: &str) -> Result<bool, NgsiError> {
     739           46 :     let mut skipped = false;
     740           46 :     let target = antares_store::stored_object(doc)?;
     741          206 :     for (k, v) in antares_jsonld::expanded_object(expanded)? {
     742          108 :         if matches!(
     743          206 :             k.as_str(),
     744          206 :             "id" | "type" | "scope" | "createdAt" | "modifiedAt"
     745              :         ) {
     746          108 :             continue;
     747           98 :         }
     748              :         // stamped as every other write path stamps a new instance
     749           98 :         let mut incoming = v.clone();
     750           98 :         crate::stamp::stamp_instances(&mut incoming, ts);
     751           98 :         let incoming: Vec<Value> = match incoming {
     752           98 :             Value::Array(a) => a,
     753            0 :             _ => Vec::new(),
     754              :         };
     755           98 :         match target.get_mut(k).and_then(Value::as_array_mut) {
     756           38 :             None => {
     757           38 :                 target.insert(k.clone(), Value::Array(incoming));
     758           38 :             }
     759           60 :             Some(cur) => {
     760           68 :                 for ni in incoming {
     761           68 :                     let ds = ni.get("datasetId").and_then(Value::as_str);
     762           68 :                     if cur
     763           68 :                         .iter()
     764           76 :                         .any(|ci| ci.get("datasetId").and_then(Value::as_str) == ds)
     765           52 :                     {
     766           52 :                         skipped = true;
     767           52 :                     } else {
     768           16 :                         cur.push(ni);
     769           16 :                     }
     770              :                 }
     771              :             }
     772              :         }
     773              :     }
     774           46 :     target.insert("modifiedAt".into(), Value::String(ts.to_owned()));
     775           46 :     Ok(skipped)
     776           46 : }
     777              : 
     778          416 : fn ngsi_of(e: ApiError) -> NgsiError {
     779          416 :     match e {
     780          416 :         ApiError::Ngsi(n) => n,
     781            0 :         ApiError::Bare(code) => NgsiError::BadRequestData(format!("HTTP {code}")),
     782            0 :         ApiError::NotAcceptable(_) => NgsiError::BadRequestData("HTTP 406".into()),
     783              :         // One item of a batch cannot answer 503: the array's other entries
     784              :         // may have succeeded, and 5.6.7.4 gives each failure its own
     785              :         // ProblemDetails. The item carries the same detail the whole-request
     786              :         // answer would, so the cause is not lost in the collapse.
     787              :         ApiError::Overloaded(_) => {
     788            0 :             NgsiError::InternalError(antares_model::error::DB_OVERLOADED.into())
     789              :         }
     790              :         // The policy gate runs once per request, before the array is
     791              :         // touched, so a refusal is the whole array's 403 and never one
     792              :         // entry's — this arm is exhaustiveness, not a path. If a per-item
     793              :         // gate is ever added, 422 is the nearest thing Table 6.3.2-1 has:
     794              :         // the entry was refused, and the reason travels with it.
     795            0 :         ApiError::Denied(why) => NgsiError::OperationNotSupported(why),
     796              :     }
     797          416 : }
     798              : 
     799              : /// `a` and `b` hold the same Entity apart from the `modifiedAt` stamps a
     800              : /// merge leaves on the Entity and on each Attribute instance it touched.
     801              : /// Only those two levels are skipped: a `modifiedAt` key inside a value
     802              : /// is data and compares like any other.
     803           66 : pub(super) fn same_but_stamps(a: &Value, b: &Value) -> bool {
     804          316 :     fn without(o: &serde_json::Map<String, Value>) -> impl Iterator<Item = (&String, &Value)> {
     805         1436 :         o.iter().filter(|(k, _)| k.as_str() != "modifiedAt")
     806          316 :     }
     807          116 :     fn same_obj(
     808          116 :         a: &serde_json::Map<String, Value>,
     809          116 :         b: &serde_json::Map<String, Value>,
     810          116 :         inner: fn(&Value, &Value) -> bool,
     811          116 :     ) -> bool {
     812          116 :         without(a).count() == without(b).count()
     813          236 :             && without(a).all(|(k, v)| b.get(k).is_some_and(|w| inner(v, w)))
     814          116 :     }
     815          104 :     fn instances(a: &Value, b: &Value) -> bool {
     816          104 :         match (a.as_array(), b.as_array()) {
     817           60 :             (Some(x), Some(y)) => {
     818           60 :                 x.len() == y.len()
     819           60 :                     && x.iter()
     820           60 :                         .zip(y)
     821           60 :                         .all(|(i, j)| match (i.as_object(), j.as_object()) {
     822          132 :                             (Some(i), Some(j)) => same_obj(i, j, |v, w| v == w),
     823           10 :                             _ => i == j,
     824           60 :                         })
     825              :             }
     826           44 :             _ => a == b,
     827              :         }
     828          104 :     }
     829           66 :     match (a.as_object(), b.as_object()) {
     830           66 :         (Some(a), Some(b)) => same_obj(a, b, instances),
     831            0 :         _ => a == b,
     832              :     }
     833           66 : }
        

Generated by: LCOV version 2.0-1