LCOV - code coverage report
Current view: top level - antares-store/src - lib.rs (source / functions) Coverage Total Hit
Test: merged.info Lines: 76.1 % 469 357
Test Date: 2026-09-21 10:31:06 Functions: 37.0 % 1227 454

            Line data    Source code
       1              : // SPDX-License-Identifier: EUPL-1.2
       2              : //! The storage seam: two driver traits every backend implements, plus the
       3              : //! backend-neutral types they speak (resource kinds, filter shapes, the
       4              : //! store-mode enum). This crate names no backend — redb, sqlx and the
       5              : //! browser's OPFS all live behind these traits in their own crates.
       6              : //!
       7              : //! Current-state and temporal storage are SEPARATE interfaces on purpose:
       8              : //! a deployment may run postgres current-state with no temporal store at
       9              : //! all, or memory current-state with a database-backed history. A driver
      10              : //! that does not support an operation answers with an error (or a benign
      11              : //! no-op for internal bookkeeping), never a panic — `NoTemporal` is the
      12              : //! canonical instance.
      13              : #![cfg_attr(not(test), warn(clippy::expect_used))]
      14              : #![deny(missing_docs)]
      15              : 
      16              : #[cfg(feature = "test-kit")]
      17              : /// The shared driver contract every backend runs as a test. It asserts, so
      18              : /// it panics on a store that breaks an invariant — that is the point.
      19              : #[allow(clippy::expect_used)]
      20              : pub mod contract;
      21              : pub mod filter;
      22              : 
      23              : use antares_model::{NgsiError, TenantId};
      24              : use serde_json::Value;
      25              : 
      26              : /// A stored document as the object it is. Every document a driver holds is a
      27              : /// JSON object — `contract` asserts the shape on every backend — so a value
      28              : /// that is not one means the driver returned something the contract forbids
      29              : /// underneath a live request. That fails the one request; it never takes the
      30              : /// process down, which is what an unwrap here would do.
      31         4392 : pub fn stored_object(doc: &mut Value) -> Result<&mut serde_json::Map<String, Value>, NgsiError> {
      32         4392 :     doc.as_object_mut()
      33         4392 :         .ok_or_else(|| NgsiError::InternalError("stored document is not a JSON object".into()))
      34         4392 : }
      35              : 
      36              : /// What a [`ChangeHook`] call returns: the hook records temporal history
      37              : /// through the temporal driver, which is asynchronous, so the driver that
      38              : /// fires the hook awaits it before its own write returns.
      39              : pub type HookFuture<'a> = std::pin::Pin<Box<dyn std::future::Future<Output = ()> + Send + 'a>>;
      40              : 
      41              : /// Called with (tenant, before, after) on every entity write — the local-mode
      42              : /// change feed: create ⇒ (None, Some), delete ⇒ (Some, None). Shared rather
      43              : /// than owned: a driver clones it out of its lock before awaiting, since a
      44              : /// lock guard cannot be held across an await.
      45              : pub type ChangeHook = std::sync::Arc<
      46              :     dyn for<'a> Fn(&'a TenantId, Option<Value>, Option<Value>) -> HookFuture<'a> + Send + Sync,
      47              : >;
      48              : 
      49              : /// Which resource family an operation touches.
      50              : #[derive(Clone, Copy, Debug, PartialEq, Eq)]
      51              : pub enum Kind {
      52              :     /// 5.2.4 Entity current-state documents.
      53              :     Entity,
      54              :     /// 5.2.12 Subscription documents.
      55              :     Subscription,
      56              :     /// 5.2.9 Context Source Registration documents.
      57              :     Registration,
      58              :     /// Context Source Registration Subscription documents (5.11).
      59              :     CSourceSubscription,
      60              :     /// Temporal Representation documents (5.2.5).
      61              :     Temporal,
      62              :     /// 5.16 Snapshot status documents (+ the internal synth-tenant index).
      63              :     Snapshot,
      64              :     /// 5.14 EntityMap API documents.
      65              :     EntityMap,
      66              :     /// 5.8.1.4 distributed-subscription mappings (remote ids per CSR).
      67              :     DistSub,
      68              :     /// Notifications a delivery policy gave up on (dead letters), kept
      69              :     /// under the subscription's tenant for replay.
      70              :     DeadLetter,
      71              : }
      72              : 
      73              : /// The four store backends, decided ONCE at startup and threaded as a value —
      74              : /// never re-derived from strings or runtime probes, so a section gated on the
      75              : /// wrong mode is unrepresentable.
      76              : #[derive(Clone, Copy, Debug, PartialEq, Eq, Default)]
      77              : pub enum StoreMode {
      78              :     /// In-process maps; nothing survives a restart.
      79              :     #[default]
      80              :     Memory,
      81              :     /// Embedded single-file store on local disk.
      82              :     File,
      83              :     /// PostgreSQL with PostGIS.
      84              :     Postgres,
      85              :     /// PostgreSQL with PostGIS and TimescaleDB hypertables for history.
      86              :     Timescale,
      87              : }
      88              : 
      89              : impl StoreMode {
      90              :     /// Every backend this workspace knows, in the order a listing shows
      91              :     /// them. One source of truth: `FromStr`, the unknown-mode message and
      92              :     /// the broker's built-with shelf all read it, so a backend added to the
      93              :     /// enum cannot go missing from any of them.
      94              :     pub const ALL: [StoreMode; 4] = [
      95              :         StoreMode::Memory,
      96              :         StoreMode::File,
      97              :         StoreMode::Postgres,
      98              :         StoreMode::Timescale,
      99              :     ];
     100              : 
     101              :     /// The accepted mode names, `memory|file|postgres|timescale`.
     102            4 :     pub fn names() -> String {
     103            4 :         StoreMode::ALL
     104            4 :             .iter()
     105           16 :             .map(|m| m.as_str())
     106            4 :             .collect::<Vec<_>>()
     107            4 :             .join("|")
     108            4 :     }
     109              : 
     110              :     /// The mode name as accepted by `FromStr` (`memory|file|postgres|timescale`).
     111          432 :     pub fn as_str(self) -> &'static str {
     112          432 :         match self {
     113          151 :             StoreMode::Memory => "memory",
     114          107 :             StoreMode::File => "file",
     115           94 :             StoreMode::Postgres => "postgres",
     116           80 :             StoreMode::Timescale => "timescale",
     117              :         }
     118          432 :     }
     119              :     /// Shared-database modes — the only ones that can back multiple instances.
     120           39 :     pub fn is_pg(self) -> bool {
     121           39 :         matches!(self, StoreMode::Postgres | StoreMode::Timescale)
     122           39 :     }
     123              : }
     124              : 
     125              : impl std::str::FromStr for StoreMode {
     126              :     type Err = String;
     127           83 :     fn from_str(s: &str) -> Result<Self, Self::Err> {
     128           83 :         StoreMode::ALL
     129           83 :             .into_iter()
     130          156 :             .find(|m| m.as_str() == s)
     131           83 :             .ok_or_else(|| format!("unknown store mode {s} ({})", StoreMode::names()))
     132           83 :     }
     133              : }
     134              : 
     135              : impl std::fmt::Display for StoreMode {
     136            4 :     fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
     137            4 :         f.write_str(self.as_str())
     138            4 :     }
     139              : }
     140              : 
     141              : /// What one tenant holds, per current-state document kind.
     142              : #[derive(Debug, Clone, Default, PartialEq, Eq)]
     143              : pub struct TenantStats {
     144              :     /// The tenant name.
     145              :     pub tenant: String,
     146              :     /// When the tenant row was created, if the backend records it.
     147              :     pub created_at: Option<String>,
     148              :     /// 5.2.4 entities.
     149              :     pub entities: u64,
     150              :     /// 5.2.12 subscriptions.
     151              :     pub subscriptions: u64,
     152              :     /// 5.2.9 context source registrations.
     153              :     pub registrations: u64,
     154              :     /// Context source registration subscriptions.
     155              :     pub csource_subscriptions: u64,
     156              :     /// 5.16 snapshots.
     157              :     pub snapshots: u64,
     158              :     /// Entity maps (5.14).
     159              :     pub entity_maps: u64,
     160              :     /// Distributed subscriptions this tenant placed at other brokers.
     161              :     pub dist_subs: u64,
     162              : }
     163              : 
     164              : /// The read-modify-write closure as the object-safe seam carries it. The
     165              : /// typed `Result<T, E>` travels through [`CurrentStateDriverExt::mutate`]'s
     166              : /// side slot; the boxed closure only signals commit (`Ok`) vs reject
     167              : /// (`Err`) to the driver.
     168              : pub type MutateFn<'a> = Box<dyn FnOnce(&mut Value) -> Result<(), ()> + Send + 'a>;
     169              : /// Per-id variant for batch mutation; the driver calls it once per PRESENT
     170              : /// id, in input order — the ext trait's error slot depends on that order.
     171              : pub type BatchMutateFn<'a> = Box<dyn FnMut(&str, &mut Value) -> Result<(), ()> + Send + 'a>;
     172              : /// The delivery stamp expressed as a `mutate`. This is the rule itself —
     173              : /// what `timesSent`, `lastNotification`, `lastSuccess` and `status` become
     174              : /// after one attempt (5.2.14.2) — so a backend that reimplements
     175              : /// [`CurrentStateDriver::record_delivery`] in its own query language is
     176              : /// reimplementing THIS, and the two must agree.
     177          390 : pub async fn record_delivery_via_mutate(
     178          390 :     d: &(impl CurrentStateDriver + ?Sized),
     179          390 :     tenant: &TenantId,
     180          390 :     kind: Kind,
     181          390 :     id: &str,
     182          390 :     now: &str,
     183          390 : ) -> Result<Option<Delivery>, NgsiError> {
     184          390 :     let mut out: Option<Delivery> = None;
     185          390 :     d.mutate::<(), ()>(tenant, kind, id, |doc| {
     186          382 :         let mut prev_success = None;
     187          382 :         if let Some(o) = doc.as_object_mut() {
     188          382 :             o.remove("status");
     189          382 :         }
     190          382 :         if let Some(n) = doc
     191          382 :             .as_object_mut()
     192          382 :             .and_then(|o| o.get_mut("notification"))
     193          382 :             .and_then(Value::as_object_mut)
     194          382 :         {
     195          382 :             let sent = n.get("timesSent").and_then(Value::as_i64).unwrap_or(0);
     196          382 :             n.insert("timesSent".into(), serde_json::json!(sent + 1));
     197          382 :             n.insert("lastNotification".into(), Value::String(now.to_owned()));
     198          382 :             prev_success = n.insert("lastSuccess".into(), Value::String(now.to_owned()));
     199          382 :             n.insert("status".into(), Value::String("ok".into()));
     200          382 :         }
     201          382 :         out = Some(Delivery {
     202          382 :             doc: doc.clone(),
     203          382 :             prev_success,
     204          382 :         });
     205          382 :         Ok(())
     206          382 :     })
     207          390 :     .await?;
     208          390 :     Ok(out)
     209          390 : }
     210              : 
     211              : /// The forward stamp expressed as a `mutate`. This is the Table 5.2.9-2
     212              : /// rule itself — what `timesSent`, `timesFailed`, `lastSuccess`,
     213              : /// `lastFailure` and `status` become after one distributed operation — so a
     214              : /// backend that reimplements [`CurrentStateDriver::record_forward`] in its
     215              : /// own query language is reimplementing THIS, and the two must agree.
     216              : ///
     217              : /// `timesSent` moves on EVERY attempt: the table defines it as the number of
     218              : /// times the registration "triggered a distributed operation, including
     219              : /// failed attempts". A member is written when it first has meaning, which is
     220              : /// what cardinality 0..1 and "created on first successful operation" ask for:
     221              : /// a registration that has only ever succeeded carries no `timesFailed` and
     222              : /// no `lastFailure`, and one that has only ever failed carries no
     223              : /// `lastSuccess`. `status` always names the LAST attempt.
     224          435 : pub async fn record_forward_via_mutate(
     225          435 :     d: &(impl CurrentStateDriver + ?Sized),
     226          435 :     tenant: &TenantId,
     227          435 :     id: &str,
     228          435 :     now: &str,
     229          435 :     ok: bool,
     230          435 : ) -> Result<Option<Value>, NgsiError> {
     231          435 :     let mut out: Option<Value> = None;
     232          435 :     d.mutate::<(), ()>(tenant, Kind::Registration, id, |doc| {
     233          409 :         if let Some(o) = doc.as_object_mut() {
     234          409 :             let sent = o.get("timesSent").and_then(Value::as_i64).unwrap_or(0);
     235          409 :             o.insert("timesSent".into(), serde_json::json!(sent + 1));
     236          409 :             if ok {
     237          370 :                 o.insert("lastSuccess".into(), Value::String(now.to_owned()));
     238          370 :                 o.insert("status".into(), Value::String("ok".into()));
     239          370 :             } else {
     240           39 :                 let failed = o.get("timesFailed").and_then(Value::as_i64).unwrap_or(0);
     241           39 :                 o.insert("timesFailed".into(), serde_json::json!(failed + 1));
     242           39 :                 o.insert("lastFailure".into(), Value::String(now.to_owned()));
     243           39 :                 o.insert("status".into(), Value::String("failed".into()));
     244           39 :             }
     245            0 :         }
     246          409 :         out = Some(doc.clone());
     247          409 :         Ok(())
     248          409 :     })
     249          435 :     .await?;
     250          435 :     Ok(out)
     251          435 : }
     252              : 
     253              : /// What one delivery attempt wrote: the stored subscription as it now
     254              : /// stands (the mirror is fed from it) and the `lastSuccess` that was there
     255              : /// before, which a failed attempt puts back.
     256              : #[derive(Debug, Clone)]
     257              : pub struct Delivery {
     258              :     /// The subscription as it now stands, the shape the mirror is fed.
     259              :     pub doc: Value,
     260              :     /// The `notification.lastSuccess` this attempt overwrote, absent when
     261              :     /// the subscription had never succeeded.
     262              :     pub prev_success: Option<Value>,
     263              : }
     264              : 
     265              : /// ADR-0021: the Tenant a stored `@context` row belongs to, or `None` when it
     266              : /// belongs to none.
     267              : ///
     268              : /// 5.13.1 "Cached" is a copy of a public document the broker downloaded for
     269              : /// whoever named its URL, so every Tenant reaches it. "Hosted" and
     270              : /// "ImplicitlyCreated" are documents a client stored THROUGH a Tenant, and
     271              : /// 5.5.7 makes their term mappings decide what that Tenant's payloads mean;
     272              : /// the owner travels in the row. A row written before the member existed
     273              : /// reads as the default Tenant's, and so does a row whose `kind` is missing
     274              : /// altogether: no writer produces one, and an unrecognised row is safer read
     275              : /// as somebody's than as everybody's.
     276         8914 : pub fn context_row_owner(row: &Value) -> Option<&str> {
     277         8914 :     (row["kind"].as_str() != Some("Cached"))
     278         8914 :         .then(|| row["owner"].as_str().unwrap_or(TenantId::DEFAULT))
     279         8914 : }
     280              : 
     281              : /// Whether a call acting for `tenant` — `None` for no Tenant at all — reaches
     282              : /// the row. The rule [`context_row_owner`] states, in the form the stores and
     283              : /// the API both need.
     284         8874 : pub fn context_row_visible(row: &Value, tenant: Option<&TenantId>) -> bool {
     285         8874 :     match context_row_owner(row) {
     286         7290 :         None => true,
     287         1584 :         Some(owner) => tenant.is_some_and(|t| t.as_str() == owner),
     288              :     }
     289         8874 : }
     290              : 
     291              : /// Current-state storage: everything except the temporal evolution.
     292              : ///
     293              : /// Contract carried over from the enum seam it replaces: every mutate is
     294              : /// one transaction under the row lock, and a missing row is `None`, never
     295              : /// an insert (a bookkeeping writeback racing a DELETE must not resurrect
     296              : /// the row). Backends map their internal failures to
     297              : /// `NgsiError::InternalError` with a GENERIC client-visible detail.
     298              : #[async_trait::async_trait]
     299              : pub trait CurrentStateDriver: Send + Sync {
     300              :     /// Readiness RIGHT NOW — a lost database flips /q/ready to 503.
     301              :     async fn ping(&self) -> Result<(), NgsiError>;
     302              :     /// (Queued writers, peak) of a single-writer commit section, if the
     303              :     /// backend has one.
     304            0 :     fn commit_queue(&self) -> Option<(usize, usize)> {
     305            0 :         None
     306            0 :     }
     307              :     /// Drain: finish in-flight work and disconnect cleanly.
     308              :     async fn close(&self);
     309              :     /// What this driver runs on, for `/q/health`: engine, server version,
     310              :     /// extensions — whatever an operator needs to tell two deployments of
     311              :     /// the same backend name apart. Read from state captured at startup,
     312              :     /// never by querying on the request: health is polled. An empty object
     313              :     /// (the default) means the driver has nothing to add to its name.
     314            0 :     fn version_info(&self) -> Value {
     315            0 :         Value::Object(serde_json::Map::new())
     316            0 :     }
     317              :     /// Installs the (tenant, before, after) hook called on every entity write.
     318              :     fn set_change_hook(&self, h: ChangeHook);
     319              :     /// Turn the same-transaction outbox producer on (bus=nats).
     320            0 :     fn set_outbox(&self, on: bool) {
     321            0 :         let _ = on;
     322            0 :     }
     323              :     /// Outbox drain: oldest-first page of pending rows `(seq, tenant, event)`.
     324            0 :     async fn outbox_peek(&self, limit: i64) -> Result<Vec<(i64, String, Value)>, NgsiError> {
     325              :         let _ = limit;
     326              :         Ok(Vec::new())
     327            0 :     }
     328              :     /// Outbox drain: delete EXACTLY the published rows.
     329            0 :     async fn outbox_ack(&self, seqs: &[i64]) -> Result<u64, NgsiError> {
     330            0 :         let _ = seqs;
     331              :         Ok(0)
     332            0 :     }
     333              :     /// Outbox drain: keep the rows of events the bus could not carry whole
     334              :     /// and take them out of the drain's page. The published message holds a
     335              :     /// claim-check reference; the row holds the bodies it stands for.
     336            0 :     async fn outbox_retain(&self, tenant: &TenantId, seqs: &[i64]) -> Result<u64, NgsiError> {
     337              :         let _ = (tenant, seqs);
     338              :         Ok(0)
     339            0 :     }
     340              :     /// The whole event behind a claim-check reference, or `None` once it has
     341              :     /// been reaped. Scoped to the tenant that wrote it.
     342            0 :     async fn outbox_event(&self, seq: i64, tenant: &TenantId) -> Result<Option<Value>, NgsiError> {
     343              :         let _ = (seq, tenant);
     344              :         Ok(None)
     345            0 :     }
     346              : 
     347              :     /// Insert a document; `false` if the id already exists (nothing written).
     348              :     async fn create(
     349              :         &self,
     350              :         tenant: &TenantId,
     351              :         kind: Kind,
     352              :         id: &str,
     353              :         doc: Value,
     354              :     ) -> Result<bool, NgsiError>;
     355              :     /// Batch create (entities only); created-flags in input order.
     356              :     async fn batch_create(
     357              :         &self,
     358              :         tenant: &TenantId,
     359              :         items: Vec<(String, Value)>,
     360              :     ) -> Result<Vec<bool>, NgsiError>;
     361              :     /// Batch delete (entities only); deleted-flags in input order, a
     362              :     /// duplicate id deletes once and reads absent the second time.
     363              :     async fn batch_delete(&self, tenant: &TenantId, ids: &[String])
     364              :         -> Result<Vec<bool>, NgsiError>;
     365              :     /// Batch upsert with REPLACE semantics (entities only); created-flags in
     366              :     /// input order.
     367              :     async fn batch_upsert(
     368              :         &self,
     369              :         tenant: &TenantId,
     370              :         items: Vec<(String, Value)>,
     371              :     ) -> Result<Vec<bool>, NgsiError>;
     372              :     /// Insert or replace a document. `true` means a document was ALREADY
     373              :     /// there and this call replaced it; `false` means this call created it.
     374              :     /// The polarity is the opposite of [`Self::batch_upsert`], which answers
     375              :     /// created-flags — the batch path needs them to split 201 from 204
     376              :     /// (5.6.8) while the single path ignores the value.
     377              :     async fn upsert(
     378              :         &self,
     379              :         tenant: &TenantId,
     380              :         kind: Kind,
     381              :         id: &str,
     382              :         doc: Value,
     383              :     ) -> Result<bool, NgsiError>;
     384              :     /// Read one document; `None` if absent.
     385              :     async fn get(
     386              :         &self,
     387              :         tenant: &TenantId,
     388              :         kind: Kind,
     389              :         id: &str,
     390              :     ) -> Result<Option<Value>, NgsiError>;
     391              :     /// Delete one document; `false` if it was absent.
     392              :     async fn delete(&self, tenant: &TenantId, kind: Kind, id: &str) -> Result<bool, NgsiError>;
     393              :     /// Delete one Entity, but only if `keep` accepts the stored document.
     394              :     /// `false` = absent OR refused; the caller cannot tell the two apart and
     395              :     /// does not need to, because 5.6.6.4 gives the same answer to both: an
     396              :     /// Entity the selector excludes "is not known" for this operation.
     397              :     ///
     398              :     /// The read and the delete happen under one lock. Required, deliberately:
     399              :     /// the obvious default is `get` then `delete`, and that pair is the race
     400              :     /// this method exists to remove — between the two the Entity can be
     401              :     /// deleted and recreated under the same id, and the delete then lands on
     402              :     /// a document the caller never inspected. A backend inheriting a default
     403              :     /// would keep the race with no compile error to say so.
     404              :     async fn delete_entity_if(
     405              :         &self,
     406              :         tenant: &TenantId,
     407              :         id: &str,
     408              :         keep: &(dyn for<'v> Fn(&'v Value) -> bool + Sync),
     409              :     ) -> Result<bool, NgsiError>;
     410              :     /// Every document of this kind in the tenant.
     411              :     ///
     412              :     /// A backend may refuse a tenant that holds too many to materialize
     413              :     /// (5.5.6 TooManyResults). That is right for a client query and wrong
     414              :     /// for a reader that must see ALL of them — use [`Self::list_page`].
     415              :     async fn list(&self, tenant: &TenantId, kind: Kind) -> Result<Vec<Value>, NgsiError>;
     416              :     /// One id-ordered page of documents, for the internal readers that must
     417              :     /// see every one of them and so cannot be refused: ids strictly greater
     418              :     /// than `after`, at most `limit`. A short page means the end.
     419              :     ///
     420              :     /// Keyset, not offset: the caller walks a tenant that is being written
     421              :     /// to underneath it, where OFFSET skips and repeats rows. The peak cost
     422              :     /// is one page, not the whole tenant, which is what the row ceiling on
     423              :     /// `list` was protecting — so this carries no ceiling of its own, and a
     424              :     /// backend may not refuse it for volume.
     425              :     ///
     426              :     /// Required, deliberately: the obvious default is `list` sliced, and
     427              :     /// `list` is the read that may refuse. A backend inheriting that would
     428              :     /// silently reacquire the outage this method exists to prevent, with no
     429              :     /// compile error to say so.
     430              :     async fn list_page(
     431              :         &self,
     432              :         tenant: &TenantId,
     433              :         kind: Kind,
     434              :         after: Option<&str>,
     435              :         limit: usize,
     436              :     ) -> Result<Vec<Value>, NgsiError>;
     437              :     /// One id-ordered window of documents and the size of the whole set:
     438              :     /// elements `offset..offset + limit`, plus the count 6.3.10's `count`
     439              :     /// parameter reports. `limit` 0 is a legal request for the count alone.
     440              :     ///
     441              :     /// 5.5.9.1: "the query resolution mechanisms of the NGSI-LD System shall
     442              :     /// ensure that only up to a maximum of L NGSI-LD Elements are RETRIEVED
     443              :     /// and returned to the NGSI-LD client". Reading a whole tenant to serve
     444              :     /// one page of it is what that sentence rules out, and it is why this
     445              :     /// carries no row ceiling: the window bounds the result by construction,
     446              :     /// so the only thing a ceiling could refuse is a page the client is
     447              :     /// entitled to.
     448              :     ///
     449              :     /// Offset, not the keyset of [`Self::list_page`], because this serves
     450              :     /// 5.5.9.2's `limit`/`offset`, which lets a client "jump to a desired
     451              :     /// set of elements". A cursor cannot answer that; the two reads exist
     452              :     /// for different callers and neither replaces the other.
     453              :     ///
     454              :     /// Required for the same reason `list_page` is: the obvious default
     455              :     /// slices `list`, and `list` is the read that may refuse.
     456              :     async fn list_slice(
     457              :         &self,
     458              :         tenant: &TenantId,
     459              :         kind: Kind,
     460              :         offset: usize,
     461              :         limit: usize,
     462              :     ) -> Result<(Vec<Value>, usize), NgsiError>;
     463              :     /// Registrations that can match these ids/types (a backend may narrow;
     464              :     /// returning the full tenant list is always correct).
     465              :     async fn matching_registrations(
     466              :         &self,
     467              :         tenant: &TenantId,
     468              :         ids: Option<&[String]>,
     469              :         types: Option<&[String]>,
     470              :     ) -> Result<Vec<Value>, NgsiError>;
     471              :     /// Query Entities with the filter pushed down where the backend can
     472              :     /// take it; the caller re-checks unless the outcome says `decided`.
     473              :     async fn query_entities(
     474              :         &self,
     475              :         tenant: &TenantId,
     476              :         f: &filter::EntityFilter<'_>,
     477              :     ) -> Result<filter::QueryOutcome, NgsiError>;
     478              :     /// Read-modify-write under the row lock; `None` = absent (never an
     479              :     /// insert), `Some(Err)` = the closure rejected, nothing committed.
     480              :     async fn mutate_boxed<'a>(
     481              :         &self,
     482              :         tenant: &TenantId,
     483              :         kind: Kind,
     484              :         id: &str,
     485              :         f: MutateFn<'a>,
     486              :     ) -> Result<Option<Result<(), ()>>, NgsiError>;
     487              :     /// Batch read-modify-write (entities only); results align with `ids`.
     488              :     async fn batch_mutate_boxed<'a>(
     489              :         &self,
     490              :         tenant: &TenantId,
     491              :         ids: &[String],
     492              :         f: BatchMutateFn<'a>,
     493              :     ) -> Result<Vec<Option<Result<(), ()>>>, NgsiError>;
     494              :     /// 5.2.14.2 delivery bookkeeping: stamp one delivery attempt on a
     495              :     /// subscription and hand back the stored document. `timesSent` moves by
     496              :     /// one, `lastNotification` and `lastSuccess` take `now`, and the previous
     497              :     /// `lastSuccess` comes back so a failed attempt can roll it back.
     498              :     /// `None` means the row is gone — the subscription was deleted between
     499              :     /// matching and delivery, and nothing may be sent (5.8.6).
     500              :     ///
     501              :     /// The default expresses it as a `mutate`, which is correct everywhere.
     502              :     /// A backend whose `mutate` locks the row across a network round trip
     503              :     /// should override it with one statement: at fan-out every delivery on
     504              :     /// one subscription contends for that row, so the lock hold time — not
     505              :     /// the statement count — is what serializes delivery.
     506              :     async fn record_delivery(
     507              :         &self,
     508              :         tenant: &TenantId,
     509              :         kind: Kind,
     510              :         id: &str,
     511              :         now: &str,
     512            0 :     ) -> Result<Option<Delivery>, NgsiError> {
     513              :         record_delivery_via_mutate(self, tenant, kind, id, now).await
     514            0 :     }
     515              :     /// Reap expired docs/instances (backends with their own maintenance job
     516              :     /// return 0).
     517            0 :     async fn sweep_expired(&self) -> usize {
     518              :         0
     519            0 :     }
     520              :     /// Table 5.2.9-2 forward bookkeeping: stamp one distributed operation on
     521              :     /// a registration and hand back the stored document. `None` means the row
     522              :     /// is gone — the registration was deleted while its forward was in
     523              :     /// flight, and a bookkeeping writeback must never resurrect it.
     524              :     ///
     525              :     /// The default expresses it as a `mutate`, which is correct everywhere.
     526              :     // ponytail: one read-modify-write per forward, so a registration every
     527              :     // request fans out to is one contended row; a backend whose `mutate`
     528              :     // locks that row across a network round trip can collapse it into one
     529              :     // statement the way `record_delivery` does, once a profile asks.
     530              :     async fn record_forward(
     531              :         &self,
     532              :         tenant: &TenantId,
     533              :         id: &str,
     534              :         now: &str,
     535              :         ok: bool,
     536          435 :     ) -> Result<Option<Value>, NgsiError> {
     537              :         record_forward_via_mutate(self, tenant, id, now, ok).await
     538          435 :     }
     539              :     /// 5.5.10: the default Tenant implicitly exists; others once created.
     540              :     async fn tenant_exists(&self, tenant: &TenantId) -> Result<bool, NgsiError>;
     541              :     /// The iteration domain of the interval sweep and of every mirror
     542              :     /// hydration: a tenant holding a Subscription, a Context Source
     543              :     /// Registration Subscription OR a Registration SHALL appear. A superset
     544              :     /// is allowed — the callers list per tenant afterwards and an empty list
     545              :     /// costs nothing — so a backend that cannot narrow the set cheaply may
     546              :     /// return every tenant it knows. A SUBSET is a silent outage: a tenant
     547              :     /// missing here never fires a periodic notification and never reaches
     548              :     /// the mirror.
     549              :     ///
     550              :     /// Registrations are in the domain because one of the hydrations fills
     551              :     /// the REGISTRATION mirror, and the federation path reads that mirror
     552              :     /// alone whenever it is installed. A domain that stopped at
     553              :     /// subscription-holding tenants left a tenant with registrations and no
     554              :     /// subscription forwarding to no Context Source at all.
     555              :     async fn subscription_tenants(&self) -> Result<Vec<String>, NgsiError>;
     556              :     /// Every tenant the backend knows, sorted. The default Tenant is listed
     557              :     /// even when empty (5.5.10: it always exists). Names only: at the
     558              :     /// 10 000-tenant target (ADR-0001) an inventory carrying per-kind counts
     559              :     /// would cost a count per kind per tenant, so the counts are paid per
     560              :     /// lookup in `tenant_stats_one`.
     561            0 :     async fn tenant_ids(&self) -> Result<Vec<String>, NgsiError> {
     562              :         Err(NgsiError::OperationNotSupported("tenant inventory".into()))
     563            0 :     }
     564              :     /// What one tenant holds; `None` when it does not exist.
     565            0 :     async fn tenant_stats_one(&self, tenant: &TenantId) -> Result<Option<TenantStats>, NgsiError> {
     566            0 :         let _ = tenant;
     567              :         Err(NgsiError::OperationNotSupported("tenant inventory".into()))
     568            0 :     }
     569              :     /// Remove every current-state document of one tenant; `false` when the
     570              :     /// tenant did not exist. The default Tenant is emptied but keeps
     571              :     /// existing.
     572            0 :     async fn purge_tenant(&self, tenant: &TenantId) -> Result<bool, NgsiError> {
     573            0 :         let _ = tenant;
     574              :         Err(NgsiError::OperationNotSupported("tenant purge".into()))
     575            0 :     }
     576              :     /// 5.13 @context documents. `tenant` is the Tenant the call acts for, and
     577              :     /// `None` means no Tenant at all — the boot warm and the `Cached`
     578              :     /// write-through, which touch documents downloaded from public URLs.
     579              :     ///
     580              :     /// A backend enforces ADR-0021 on it: a `Cached` row belongs to no Tenant
     581              :     /// and is reachable with any `tenant`, and every other kind is reachable
     582              :     /// only by the Tenant named in its `owner` member. A backend that ignores
     583              :     /// the parameter fails the driver contract.
     584              :     async fn context_put(
     585              :         &self,
     586              :         tenant: Option<&TenantId>,
     587              :         id: &str,
     588              :         doc: Value,
     589              :     ) -> Result<(), NgsiError>;
     590              :     /// Read a stored @context document by id; `None` if absent or owned by
     591              :     /// another Tenant.
     592              :     async fn context_get(
     593              :         &self,
     594              :         tenant: Option<&TenantId>,
     595              :         id: &str,
     596              :     ) -> Result<Option<Value>, NgsiError>;
     597              :     /// Delete a stored @context document; `false` if it was absent or owned
     598              :     /// by another Tenant.
     599              :     async fn context_delete(&self, tenant: Option<&TenantId>, id: &str) -> Result<bool, NgsiError>;
     600              :     /// Every stored @context document.
     601              :     /// Every stored `@context` row WITHOUT its `body` member — the url,
     602              :     /// localId, kind, owner and usage counters, and not the document.
     603              :     ///
     604              :     /// There is deliberately no read that returns every row WITH its body.
     605              :     /// A body is accepted up to `MAX_CONTEXT_BYTES` (5 MiB) and only the
     606              :     /// `Cached` rows are capped in number, so one such read materializes
     607              :     /// gigabytes — on the boot path, where it decides whether the broker
     608              :     /// starts at all. The callers that need one body ask for it by id
     609              :     /// ([`Self::context_get`]); nothing needs them all at once.
     610              :     async fn context_list_meta(&self, tenant: Option<&TenantId>) -> Result<Vec<Value>, NgsiError>;
     611              : }
     612              : 
     613              : /// The typed-slot lock, taken and released inside one closure call. A
     614              : /// poisoned slot is read through: the panic that poisoned it is already on
     615              : /// its way out of the driver, and the value behind it is whatever the
     616              : /// closure managed to write.
     617         1863 : fn lock<T>(m: &std::sync::Mutex<T>) -> std::sync::MutexGuard<'_, T> {
     618         1863 :     m.lock().unwrap_or_else(|p| p.into_inner())
     619         1863 : }
     620              : 
     621              : /// The same, for taking the slot's value once the driver has answered.
     622         1905 : fn into_inner<T>(m: std::sync::Mutex<T>) -> T {
     623         1905 :     m.into_inner().unwrap_or_else(|p| p.into_inner())
     624         1905 : }
     625              : 
     626              : /// Typed sugar over the boxed mutate seam — call sites keep their
     627              : /// `Result<T, E>` closures; the value crosses the object boundary in a
     628              : /// side slot.
     629              : pub trait CurrentStateDriverExt {
     630              :     /// Typed read-modify-write: `None` = absent, `Some(Err(e))` = the closure
     631              :     /// rejected and nothing was committed.
     632              :     fn mutate<T: Send, E: Send>(
     633              :         &self,
     634              :         tenant: &TenantId,
     635              :         kind: Kind,
     636              :         id: &str,
     637              :         f: impl FnOnce(&mut Value) -> Result<T, E> + Send,
     638              :     ) -> impl std::future::Future<Output = Result<Option<Result<T, E>>, NgsiError>> + Send;
     639              :     /// Typed batch read-modify-write (entities only); results align with `ids`.
     640              :     fn batch_mutate<E: Send>(
     641              :         &self,
     642              :         tenant: &TenantId,
     643              :         ids: &[String],
     644              :         f: impl FnMut(&str, &mut Value) -> Result<(), E> + Send,
     645              :     ) -> impl std::future::Future<Output = Result<Vec<Option<Result<(), E>>>, NgsiError>> + Send;
     646              : }
     647              : 
     648              : impl<S: CurrentStateDriver + ?Sized> CurrentStateDriverExt for S {
     649         4573 :     async fn mutate<T: Send, E: Send>(
     650         4573 :         &self,
     651         4573 :         tenant: &TenantId,
     652         4573 :         kind: Kind,
     653         4573 :         id: &str,
     654         4573 :         f: impl FnOnce(&mut Value) -> Result<T, E> + Send,
     655         4573 :     ) -> Result<Option<Result<T, E>>, NgsiError> {
     656         4573 :         let slot = std::sync::Mutex::new(None);
     657         4573 :         let r = self
     658         4573 :             .mutate_boxed(
     659         4573 :                 tenant,
     660         4573 :                 kind,
     661         4573 :                 id,
     662         4573 :                 Box::new(|v| {
     663         1771 :                     let r = f(v);
     664         1771 :                     let flag = if r.is_ok() { Ok(()) } else { Err(()) };
     665         1771 :                     *lock(&slot) = Some(r);
     666         1771 :                     flag
     667         1771 :                 }),
     668              :             )
     669         4573 :             .await?;
     670         4573 :         Ok(r.and_then(|_| into_inner(slot)))
     671         4573 :     }
     672              : 
     673           42 :     async fn batch_mutate<E: Send>(
     674           42 :         &self,
     675           42 :         tenant: &TenantId,
     676           42 :         ids: &[String],
     677           42 :         mut f: impl FnMut(&str, &mut Value) -> Result<(), E> + Send,
     678           42 :     ) -> Result<Vec<Option<Result<(), E>>>, NgsiError> {
     679              :         // Errors land in the queue in closure-call order, which the trait
     680              :         // contract fixes to input order over present ids.
     681           42 :         let errs = std::sync::Mutex::new(std::collections::VecDeque::new());
     682           42 :         let r = self
     683           42 :             .batch_mutate_boxed(
     684           42 :                 tenant,
     685           42 :                 ids,
     686         4052 :                 Box::new(|id, v| match f(id, v) {
     687         4052 :                     Ok(()) => Ok(()),
     688            0 :                     Err(e) => {
     689            0 :                         lock(&errs).push_back(e);
     690            0 :                         Err(())
     691              :                     }
     692         4052 :                 }),
     693              :             )
     694           42 :             .await?;
     695           42 :         let mut errs = into_inner(errs);
     696           42 :         let mut out = Vec::with_capacity(r.len());
     697         4080 :         for slot in r {
     698         4080 :             out.push(match slot {
     699           28 :                 None => None,
     700         4052 :                 Some(Ok(())) => Some(Ok(())),
     701              :                 // one queued error per rejected id is the trait contract; a
     702              :                 // driver that reports more rejections than the closure raised
     703              :                 // has broken it, and the batch fails rather than inventing an
     704              :                 // error for the caller to read.
     705            0 :                 Some(Err(())) => Some(Err(errs.pop_front().ok_or_else(|| {
     706            0 :                     NgsiError::InternalError(
     707            0 :                         "batch driver reported more rejections than were raised".into(),
     708            0 :                     )
     709            0 :                 })?)),
     710              :             });
     711              :         }
     712           42 :         Ok(out)
     713           42 :     }
     714              : }
     715              : 
     716              : /// What a temporal event records.
     717              : #[derive(Clone, Copy, Debug, PartialEq, Eq)]
     718              : pub enum TemporalOp {
     719              :     /// An Attribute the entity did not carry before (one event per instance).
     720              :     AttrCreated,
     721              :     /// A changed instance of an existing Attribute (4.5.6 append).
     722              :     AttrModified,
     723              :     /// 4.5.6: the Scope changed through the Core API — recorded as a scope
     724              :     /// instance whose observedAt copies modifiedAt.
     725              :     ScopeChanged,
     726              : }
     727              : 
     728              : /// One change the write path hands to the temporal seam. Events are
     729              : /// produced per attribute INSTANCE (the gate chain and a columnar writer
     730              : /// both work per instance) and drained per request, in order.
     731              : #[derive(Clone, Debug)]
     732              : pub struct TemporalEvent {
     733              :     /// What the event records.
     734              :     pub op: TemporalOp,
     735              :     /// Owning tenant.
     736              :     pub tenant: TenantId,
     737              :     /// Id of the entity whose history this instance belongs to.
     738              :     pub entity_id: String,
     739              :     /// The entity's meta shell (id, type, createdAt, modifiedAt, scope) as
     740              :     /// it stood after the write — what `temporal_append` creates on first
     741              :     /// touch.
     742              :     pub shell: Value,
     743              :     /// Expanded Attribute name, or `scope`.
     744              :     pub attr: String,
     745              :     /// The instance snapshot: value, datasetId, observedAt, instanceId…
     746              :     pub instance: Value,
     747              : }
     748              : 
     749              : /// Temporal storage: the entity history (Temporal Evolution) plus the raw
     750              : /// temporal documents the 5.6.13-5.6.16 edit paths operate on.
     751              : ///
     752              : /// Internal bookkeeping (snapshot copies, delete cascades) degrades to
     753              : /// benign no-ops on a driver without temporal support; the CLIENT-facing
     754              : /// operations answer `OperationNotSupported` (422 per CIM 009
     755              : /// Table 6.3.2-1) instead.
     756              : #[async_trait::async_trait]
     757              : pub trait TemporalDriver: Send + Sync {
     758              :     /// The drain: one call carries a whole request's events, in production
     759              :     /// order. The default folds consecutive events of one entity into a
     760              :     /// single `temporal_append` (scope changes go through `mutate`, as
     761              :     /// 4.5.6 shapes them); a bulk writer overrides this and sees the batch.
     762         5334 :     async fn event_list(&self, evs: &[TemporalEvent]) -> Result<(), NgsiError> {
     763              :         let mut i = 0;
     764              :         while i < evs.len() {
     765              :             let (tenant, id) = (&evs[i].tenant, evs[i].entity_id.as_str());
     766              :             let mut additions = serde_json::Map::new();
     767              :             let mut shell = &evs[i].shell;
     768              :             let mut j = i;
     769              :             while j < evs.len() && evs[j].tenant == *tenant && evs[j].entity_id == id {
     770              :                 let ev = &evs[j];
     771              :                 shell = &ev.shell;
     772              :                 if ev.op == TemporalOp::ScopeChanged {
     773              :                     let inst = ev.instance.clone();
     774            4 :                     self.mutate(tenant, id, |doc| {
     775            4 :                         let target = doc.as_object_mut().ok_or(())?;
     776            4 :                         match target.get_mut("scope").and_then(Value::as_array_mut) {
     777            2 :                             Some(arr) if arr.first().is_some_and(Value::is_object) => {
     778            0 :                                 arr.push(inst);
     779            0 :                             }
     780            4 :                             _ => {
     781            4 :                                 target.insert("scope".into(), Value::Array(vec![inst]));
     782            4 :                             }
     783              :                         }
     784            4 :                         Ok::<(), ()>(())
     785            4 :                     })
     786              :                     .await?;
     787              :                 } else {
     788              :                     if let Some(arr) = additions
     789              :                         .entry(ev.attr.clone())
     790         5608 :                         .or_insert_with(|| Value::Array(Vec::new()))
     791              :                         .as_array_mut()
     792              :                     {
     793              :                         arr.push(ev.instance.clone());
     794              :                     }
     795              :                 }
     796              :                 j += 1;
     797              :             }
     798              :             if !additions.is_empty() {
     799              :                 self.temporal_append(tenant, id, shell, &Value::Object(additions))
     800              :                     .await?;
     801              :             }
     802              :             i = j;
     803              :         }
     804              :         Ok(())
     805         5334 :     }
     806              :     /// `false` = this deployment records no history (`NoTemporal`); the
     807              :     /// write path skips recording entirely.
     808        15402 :     fn supported(&self) -> bool {
     809        15402 :         true
     810        15402 :     }
     811              :     /// Stored attribute instances of one tenant (inventory); a driver
     812              :     /// without history reports 0.
     813            0 :     async fn attr_instance_count(&self, tenant: &TenantId) -> Result<u64, NgsiError> {
     814            0 :         let _ = tenant;
     815              :         Ok(0)
     816            0 :     }
     817              :     /// Remove the whole history of one tenant; nothing to do without history.
     818            0 :     async fn purge_tenant(&self, tenant: &TenantId) -> Result<(), NgsiError> {
     819            0 :         let _ = tenant;
     820              :         Ok(())
     821            0 :     }
     822              :     /// Readiness of the temporal backend; the default is always ready.
     823            0 :     async fn ping(&self) -> Result<(), NgsiError> {
     824              :         Ok(())
     825            0 :     }
     826              :     /// What this temporal driver runs on, for `/q/health`; the same
     827              :     /// contract as [`CurrentStateDriver::version_info`].
     828            6 :     fn version_info(&self) -> Value {
     829            6 :         Value::Object(serde_json::Map::new())
     830            6 :     }
     831              :     /// Drain: finish in-flight work and disconnect cleanly. A temporal
     832              :     /// driver configured as a backend of its own holds its own pool, and the
     833              :     /// shutdown path closes it here. May be called more than once — when one
     834              :     /// instance serves both seams it is closed through each of them — so an
     835              :     /// implementation makes it idempotent. The default has nothing to close.
     836            6 :     async fn close(&self) {}
     837              :     /// Auto-recording fast path: append instances, creating the meta shell
     838              :     /// on first touch — and only for an entity that still exists (5.6.6
     839              :     /// deletes history; an append overlapping the delete must not recreate
     840              :     /// it).
     841              :     async fn temporal_append(
     842              :         &self,
     843              :         tenant: &TenantId,
     844              :         id: &str,
     845              :         shell: &Value,
     846              :         additions: &Value,
     847              :     ) -> Result<(), NgsiError>;
     848              :     /// Would `query_temporal` page EXACTLY under this `q` and range — the
     849              :     /// driver's own entity verdict equal to the evaluator's on every row,
     850              :     /// so a page it cuts is the page the caller would have cut? A driver
     851              :     /// that pages after an in-process evaluation, or not at all, answers
     852              :     /// `false`, and the caller pages the merged result itself (5.7.4.4).
     853            0 :     fn q_pushdown_exact(
     854            0 :         &self,
     855            0 :         _q: &antares_ql::QNode,
     856            0 :         _range: Option<&filter::InstanceRange<'_>>,
     857            0 :         _expand: &dyn Fn(&str) -> String,
     858            0 :     ) -> bool {
     859            0 :         false
     860            0 :     }
     861              :     /// Query Temporal Evolution (5.7.4) with pushdown where possible.
     862              :     async fn query_temporal(
     863              :         &self,
     864              :         tenant: &TenantId,
     865              :         f: &filter::TemporalFilter<'_>,
     866              :     ) -> Result<filter::TemporalOutcome, NgsiError>;
     867              :     /// Retrieve Temporal Evolution (5.7.3) with instance pruning.
     868              :     async fn get_temporal(
     869              :         &self,
     870              :         tenant: &TenantId,
     871              :         id: &str,
     872              :         f: &filter::TemporalFilter<'_>,
     873              :     ) -> Result<Option<Value>, NgsiError>;
     874              :     /// Raw temporal document access (the 5.6.11-5.6.16 edit/delete paths
     875              :     /// and internal copies).
     876              :     async fn get(&self, tenant: &TenantId, id: &str) -> Result<Option<Value>, NgsiError>;
     877              :     /// Insert a temporal document; `false` if the id already exists.
     878              :     async fn create(&self, tenant: &TenantId, id: &str, doc: Value) -> Result<bool, NgsiError>;
     879              :     /// Insert or replace a temporal document. `true` means it was ALREADY
     880              :     /// there and this call replaced it; `false` means this call created it,
     881              :     /// the same polarity as the current-state seam.
     882              :     async fn upsert(&self, tenant: &TenantId, id: &str, doc: Value) -> Result<bool, NgsiError>;
     883              :     /// Delete an entity's whole history; `false` if it had none.
     884              :     async fn delete(&self, tenant: &TenantId, id: &str) -> Result<bool, NgsiError>;
     885              :     /// Every temporal document in the tenant.
     886              :     async fn list(&self, tenant: &TenantId) -> Result<Vec<Value>, NgsiError>;
     887              :     /// Read-modify-write of one temporal document under its row lock;
     888              :     /// `None` = absent (never an insert), `Some(Err)` = rejected, not committed.
     889              :     async fn mutate_boxed<'a>(
     890              :         &self,
     891              :         tenant: &TenantId,
     892              :         id: &str,
     893              :         f: MutateFn<'a>,
     894              :     ) -> Result<Option<Result<(), ()>>, NgsiError>;
     895              : }
     896              : 
     897              : /// Typed mutate sugar for the temporal seam, same slot trick as
     898              : /// [`CurrentStateDriverExt`].
     899              : pub trait TemporalDriverExt {
     900              :     /// Typed read-modify-write of one temporal document: `None` = absent,
     901              :     /// `Some(Err(e))` = the closure rejected and nothing was committed.
     902              :     fn mutate<T: Send, E: Send>(
     903              :         &self,
     904              :         tenant: &TenantId,
     905              :         id: &str,
     906              :         f: impl FnOnce(&mut Value) -> Result<T, E> + Send,
     907              :     ) -> impl std::future::Future<Output = Result<Option<Result<T, E>>, NgsiError>> + Send;
     908              : }
     909              : 
     910              : impl<S: TemporalDriver + ?Sized> TemporalDriverExt for S {
     911          182 :     async fn mutate<T: Send, E: Send>(
     912          182 :         &self,
     913          182 :         tenant: &TenantId,
     914          182 :         id: &str,
     915          182 :         f: impl FnOnce(&mut Value) -> Result<T, E> + Send,
     916          182 :     ) -> Result<Option<Result<T, E>>, NgsiError> {
     917          182 :         let slot = std::sync::Mutex::new(None);
     918          182 :         let r = self
     919          182 :             .mutate_boxed(
     920          182 :                 tenant,
     921          182 :                 id,
     922          182 :                 Box::new(|v| {
     923           92 :                     let r = f(v);
     924           92 :                     let flag = if r.is_ok() { Ok(()) } else { Err(()) };
     925           92 :                     *lock(&slot) = Some(r);
     926           92 :                     flag
     927           92 :                 }),
     928              :             )
     929          182 :             .await?;
     930          182 :         Ok(r.and_then(|_| into_inner(slot)))
     931          182 :     }
     932              : }
     933              : 
     934              : /// The no-history driver: temporal OFF as a driver choice. Client-facing
     935              : /// reads answer `OperationNotSupported`; the recorder and the internal
     936              : /// bookkeeping paths degrade to no-ops.
     937              : pub struct NoTemporal;
     938              : 
     939            6 : fn unsupported() -> NgsiError {
     940            6 :     NgsiError::OperationNotSupported("no temporal store is configured".into())
     941            6 : }
     942              : 
     943              : #[async_trait::async_trait]
     944              : impl TemporalDriver for NoTemporal {
     945           12 :     fn supported(&self) -> bool {
     946           12 :         false
     947           12 :     }
     948              :     async fn temporal_append(
     949              :         &self,
     950              :         _tenant: &TenantId,
     951              :         _id: &str,
     952              :         _shell: &Value,
     953              :         _additions: &Value,
     954            2 :     ) -> Result<(), NgsiError> {
     955              :         Ok(())
     956            2 :     }
     957              :     async fn query_temporal(
     958              :         &self,
     959              :         _tenant: &TenantId,
     960              :         _f: &filter::TemporalFilter<'_>,
     961            4 :     ) -> Result<filter::TemporalOutcome, NgsiError> {
     962              :         Err(unsupported())
     963            4 :     }
     964              :     async fn get_temporal(
     965              :         &self,
     966              :         _tenant: &TenantId,
     967              :         _id: &str,
     968              :         _f: &filter::TemporalFilter<'_>,
     969            2 :     ) -> Result<Option<Value>, NgsiError> {
     970              :         Err(unsupported())
     971            2 :     }
     972            2 :     async fn get(&self, _tenant: &TenantId, _id: &str) -> Result<Option<Value>, NgsiError> {
     973              :         Ok(None)
     974            2 :     }
     975            0 :     async fn create(&self, _tenant: &TenantId, _id: &str, _doc: Value) -> Result<bool, NgsiError> {
     976              :         Ok(false)
     977            0 :     }
     978            0 :     async fn upsert(&self, _tenant: &TenantId, _id: &str, _doc: Value) -> Result<bool, NgsiError> {
     979              :         Ok(false)
     980            0 :     }
     981            2 :     async fn delete(&self, _tenant: &TenantId, _id: &str) -> Result<bool, NgsiError> {
     982              :         Ok(false)
     983            2 :     }
     984            2 :     async fn list(&self, _tenant: &TenantId) -> Result<Vec<Value>, NgsiError> {
     985              :         Ok(Vec::new())
     986            2 :     }
     987              :     async fn mutate_boxed<'a>(
     988              :         &self,
     989              :         _tenant: &TenantId,
     990              :         _id: &str,
     991              :         _f: MutateFn<'a>,
     992            2 :     ) -> Result<Option<Result<(), ()>>, NgsiError> {
     993              :         Ok(None)
     994            2 :     }
     995              : }
     996              : 
     997              : #[cfg(test)]
     998              : mod tests {
     999              :     use super::*;
    1000              : 
    1001              :     /// The mode list is one source of truth: every variant round-trips
    1002              :     /// through its name, and the message for an unknown name lists all of
    1003              :     /// them — a backend added to the enum cannot be missing from either.
    1004              :     #[test]
    1005            2 :     fn every_store_mode_round_trips_and_the_error_lists_them_all() {
    1006            8 :         for m in StoreMode::ALL {
    1007            8 :             let back = m
    1008            8 :                 .as_str()
    1009            8 :                 .parse::<StoreMode>()
    1010            8 :                 .unwrap_or_else(|e| panic!("{m} must parse back: {e}"));
    1011            8 :             assert_eq!(back, m);
    1012              :         }
    1013            2 :         let err = "mongo".parse::<StoreMode>().expect_err("unknown mode");
    1014            2 :         assert!(err.contains("mongo"), "{err}");
    1015            8 :         for m in StoreMode::ALL {
    1016            8 :             assert!(err.contains(m.as_str()), "the message must name {m}: {err}");
    1017              :         }
    1018            2 :     }
    1019              : 
    1020              :     /// A minimal driver proving the boxed seam round-trips typed results:
    1021              :     /// present row → the closure's T and E cross intact; absent → None.
    1022              :     struct OneDoc(std::sync::Mutex<Option<Value>>);
    1023              :     #[async_trait::async_trait]
    1024              :     impl CurrentStateDriver for OneDoc {
    1025            0 :         async fn ping(&self) -> Result<(), NgsiError> {
    1026              :             Ok(())
    1027            0 :         }
    1028            0 :         async fn close(&self) {}
    1029            0 :         fn set_change_hook(&self, _h: ChangeHook) {}
    1030              :         async fn create(
    1031              :             &self,
    1032              :             _t: &TenantId,
    1033              :             _k: Kind,
    1034              :             _id: &str,
    1035              :             doc: Value,
    1036            2 :         ) -> Result<bool, NgsiError> {
    1037              :             *self.0.lock().expect("lock") = Some(doc);
    1038              :             Ok(true)
    1039            2 :         }
    1040              :         async fn batch_create(
    1041              :             &self,
    1042              :             _t: &TenantId,
    1043              :             _items: Vec<(String, Value)>,
    1044            0 :         ) -> Result<Vec<bool>, NgsiError> {
    1045              :             unimplemented!()
    1046            0 :         }
    1047              :         async fn list_page(
    1048              :             &self,
    1049              :             _t: &TenantId,
    1050              :             _k: Kind,
    1051              :             _after: Option<&str>,
    1052              :             _limit: usize,
    1053            0 :         ) -> Result<Vec<Value>, NgsiError> {
    1054              :             unimplemented!()
    1055            0 :         }
    1056              :         async fn list_slice(
    1057              :             &self,
    1058              :             _t: &TenantId,
    1059              :             _k: Kind,
    1060              :             _offset: usize,
    1061              :             _limit: usize,
    1062            0 :         ) -> Result<(Vec<Value>, usize), NgsiError> {
    1063              :             unimplemented!()
    1064            0 :         }
    1065              :         async fn batch_delete(
    1066              :             &self,
    1067              :             _t: &TenantId,
    1068              :             _ids: &[String],
    1069            0 :         ) -> Result<Vec<bool>, NgsiError> {
    1070              :             unimplemented!()
    1071            0 :         }
    1072              :         async fn batch_upsert(
    1073              :             &self,
    1074              :             _t: &TenantId,
    1075              :             _items: Vec<(String, Value)>,
    1076            0 :         ) -> Result<Vec<bool>, NgsiError> {
    1077              :             unimplemented!()
    1078            0 :         }
    1079              :         async fn upsert(
    1080              :             &self,
    1081              :             _t: &TenantId,
    1082              :             _k: Kind,
    1083              :             _id: &str,
    1084              :             _doc: Value,
    1085            0 :         ) -> Result<bool, NgsiError> {
    1086              :             unimplemented!()
    1087            0 :         }
    1088              :         async fn get(
    1089              :             &self,
    1090              :             _t: &TenantId,
    1091              :             _k: Kind,
    1092              :             _id: &str,
    1093            4 :         ) -> Result<Option<Value>, NgsiError> {
    1094              :             Ok(self.0.lock().expect("lock").clone())
    1095            4 :         }
    1096            0 :         async fn delete(&self, _t: &TenantId, _k: Kind, _id: &str) -> Result<bool, NgsiError> {
    1097              :             unimplemented!()
    1098            0 :         }
    1099              :         async fn delete_entity_if(
    1100              :             &self,
    1101              :             _t: &TenantId,
    1102              :             _id: &str,
    1103              :             _keep: &(dyn for<'v> Fn(&'v Value) -> bool + Sync),
    1104            0 :         ) -> Result<bool, NgsiError> {
    1105              :             unimplemented!()
    1106            0 :         }
    1107            0 :         async fn list(&self, _t: &TenantId, _k: Kind) -> Result<Vec<Value>, NgsiError> {
    1108              :             unimplemented!()
    1109            0 :         }
    1110              :         async fn matching_registrations(
    1111              :             &self,
    1112              :             _t: &TenantId,
    1113              :             _ids: Option<&[String]>,
    1114              :             _types: Option<&[String]>,
    1115            0 :         ) -> Result<Vec<Value>, NgsiError> {
    1116              :             unimplemented!()
    1117            0 :         }
    1118              :         async fn query_entities(
    1119              :             &self,
    1120              :             _t: &TenantId,
    1121              :             _f: &filter::EntityFilter<'_>,
    1122            0 :         ) -> Result<filter::QueryOutcome, NgsiError> {
    1123              :             unimplemented!()
    1124            0 :         }
    1125              :         async fn mutate_boxed<'a>(
    1126              :             &self,
    1127              :             _t: &TenantId,
    1128              :             _k: Kind,
    1129              :             _id: &str,
    1130              :             f: MutateFn<'a>,
    1131            6 :         ) -> Result<Option<Result<(), ()>>, NgsiError> {
    1132              :             let mut guard = self.0.lock().expect("lock");
    1133              :             match guard.as_mut() {
    1134              :                 None => Ok(None),
    1135              :                 Some(v) => {
    1136              :                     let mut copy = v.clone();
    1137              :                     match f(&mut copy) {
    1138              :                         Ok(()) => {
    1139              :                             *v = copy;
    1140              :                             Ok(Some(Ok(())))
    1141              :                         }
    1142              :                         Err(()) => Ok(Some(Err(()))),
    1143              :                     }
    1144              :                 }
    1145              :             }
    1146            6 :         }
    1147              :         async fn batch_mutate_boxed<'a>(
    1148              :             &self,
    1149              :             _t: &TenantId,
    1150              :             _ids: &[String],
    1151              :             _f: BatchMutateFn<'a>,
    1152            0 :         ) -> Result<Vec<Option<Result<(), ()>>>, NgsiError> {
    1153              :             unimplemented!()
    1154            0 :         }
    1155            0 :         async fn tenant_exists(&self, _t: &TenantId) -> Result<bool, NgsiError> {
    1156              :             Ok(true)
    1157            0 :         }
    1158            0 :         async fn subscription_tenants(&self) -> Result<Vec<String>, NgsiError> {
    1159              :             Ok(Vec::new())
    1160            0 :         }
    1161              :         async fn context_put(
    1162              :             &self,
    1163              :             _t: Option<&TenantId>,
    1164              :             _id: &str,
    1165              :             _doc: Value,
    1166            0 :         ) -> Result<(), NgsiError> {
    1167              :             Ok(())
    1168            0 :         }
    1169              :         async fn context_get(
    1170              :             &self,
    1171              :             _t: Option<&TenantId>,
    1172              :             _id: &str,
    1173            0 :         ) -> Result<Option<Value>, NgsiError> {
    1174              :             Ok(None)
    1175            0 :         }
    1176              :         async fn context_delete(
    1177              :             &self,
    1178              :             _t: Option<&TenantId>,
    1179              :             _id: &str,
    1180            0 :         ) -> Result<bool, NgsiError> {
    1181              :             Ok(false)
    1182            0 :         }
    1183            0 :         async fn context_list_meta(&self, _t: Option<&TenantId>) -> Result<Vec<Value>, NgsiError> {
    1184              :             Ok(Vec::new())
    1185            0 :         }
    1186              :     }
    1187              : 
    1188              :     #[tokio::test]
    1189            2 :     async fn typed_mutate_round_trips_through_the_boxed_seam() {
    1190            2 :         let t = TenantId::new("t").expect("tenant");
    1191            2 :         let d: std::sync::Arc<dyn CurrentStateDriver> =
    1192            2 :             std::sync::Arc::new(OneDoc(std::sync::Mutex::new(None)));
    1193              :         // absent → None, closure never runs
    1194            2 :         let r = d
    1195            2 :             .mutate::<u32, &str>(&t, Kind::Entity, "x", |_| panic!("must not run"))
    1196            2 :             .await
    1197            2 :             .expect("driver ok");
    1198            2 :         assert!(r.is_none());
    1199            2 :         d.create(&t, Kind::Entity, "x", serde_json::json!({"n": 1}))
    1200            2 :             .await
    1201            2 :             .expect("create");
    1202              :         // typed success crosses the boundary AND the write commits
    1203            2 :         let r = d
    1204            2 :             .mutate::<u32, &str>(&t, Kind::Entity, "x", |v| {
    1205            2 :                 v["n"] = serde_json::json!(2);
    1206            2 :                 Ok(7)
    1207            2 :             })
    1208            2 :             .await
    1209            2 :             .expect("driver ok");
    1210            2 :         assert_eq!(r, Some(Ok(7)));
    1211            2 :         assert_eq!(
    1212            2 :             d.get(&t, Kind::Entity, "x")
    1213            2 :                 .await
    1214            2 :                 .expect("get")
    1215            2 :                 .expect("doc")["n"],
    1216              :             2
    1217              :         );
    1218              :         // typed error crosses the boundary AND the write is discarded
    1219            2 :         let r = d
    1220            2 :             .mutate::<u32, &str>(&t, Kind::Entity, "x", |v| {
    1221            2 :                 v["n"] = serde_json::json!(99);
    1222            2 :                 Err("rejected")
    1223            2 :             })
    1224            2 :             .await
    1225            2 :             .expect("driver ok");
    1226            2 :         assert_eq!(r, Some(Err("rejected")));
    1227            2 :         assert_eq!(
    1228            2 :             d.get(&t, Kind::Entity, "x")
    1229            2 :                 .await
    1230            2 :                 .expect("get")
    1231            2 :                 .expect("doc")["n"],
    1232            2 :             2,
    1233            2 :             "a rejecting closure must not commit"
    1234            2 :         );
    1235            2 :     }
    1236              : 
    1237              :     /// A temporal driver that only records what the seam hands it: appends
    1238              :     /// as (id, additions) in call order, scope mutations on a held doc.
    1239              :     #[derive(Default)]
    1240              :     struct Recorder {
    1241              :         appends: std::sync::Mutex<Vec<(String, Value)>>,
    1242              :         doc: std::sync::Mutex<Option<Value>>,
    1243              :     }
    1244              :     #[async_trait::async_trait]
    1245              :     impl TemporalDriver for Recorder {
    1246              :         async fn temporal_append(
    1247              :             &self,
    1248              :             _t: &TenantId,
    1249              :             id: &str,
    1250              :             _shell: &Value,
    1251              :             additions: &Value,
    1252            6 :         ) -> Result<(), NgsiError> {
    1253              :             self.appends
    1254              :                 .lock()
    1255              :                 .expect("lock")
    1256              :                 .push((id.to_owned(), additions.clone()));
    1257              :             Ok(())
    1258            6 :         }
    1259              :         async fn query_temporal(
    1260              :             &self,
    1261              :             _t: &TenantId,
    1262              :             _f: &filter::TemporalFilter<'_>,
    1263            0 :         ) -> Result<filter::TemporalOutcome, NgsiError> {
    1264              :             unimplemented!()
    1265            0 :         }
    1266              :         async fn get_temporal(
    1267              :             &self,
    1268              :             _t: &TenantId,
    1269              :             _id: &str,
    1270              :             _f: &filter::TemporalFilter<'_>,
    1271            0 :         ) -> Result<Option<Value>, NgsiError> {
    1272              :             unimplemented!()
    1273            0 :         }
    1274            2 :         async fn get(&self, _t: &TenantId, _id: &str) -> Result<Option<Value>, NgsiError> {
    1275              :             Ok(self.doc.lock().expect("lock").clone())
    1276            2 :         }
    1277            2 :         async fn create(&self, _t: &TenantId, _id: &str, doc: Value) -> Result<bool, NgsiError> {
    1278              :             *self.doc.lock().expect("lock") = Some(doc);
    1279              :             Ok(true)
    1280            2 :         }
    1281            0 :         async fn upsert(&self, _t: &TenantId, _id: &str, _doc: Value) -> Result<bool, NgsiError> {
    1282              :             unimplemented!()
    1283            0 :         }
    1284            0 :         async fn delete(&self, _t: &TenantId, _id: &str) -> Result<bool, NgsiError> {
    1285              :             unimplemented!()
    1286            0 :         }
    1287            0 :         async fn list(&self, _t: &TenantId) -> Result<Vec<Value>, NgsiError> {
    1288              :             unimplemented!()
    1289            0 :         }
    1290              :         async fn mutate_boxed<'a>(
    1291              :             &self,
    1292              :             _t: &TenantId,
    1293              :             _id: &str,
    1294              :             f: MutateFn<'a>,
    1295            2 :         ) -> Result<Option<Result<(), ()>>, NgsiError> {
    1296              :             let mut guard = self.doc.lock().expect("lock");
    1297              :             match guard.as_mut() {
    1298              :                 None => Ok(None),
    1299              :                 Some(v) => Ok(Some(f(v))),
    1300              :             }
    1301            2 :         }
    1302              :     }
    1303              : 
    1304           12 :     fn ev(op: TemporalOp, id: &str, attr: &str, n: u32) -> TemporalEvent {
    1305           12 :         TemporalEvent {
    1306           12 :             op,
    1307           12 :             tenant: TenantId::new("t").expect("tenant"),
    1308           12 :             entity_id: id.into(),
    1309           12 :             shell: serde_json::json!({"id": id, "type": ["T"]}),
    1310           12 :             attr: attr.into(),
    1311           12 :             instance: serde_json::json!({"type": "Property", "value": n}),
    1312           12 :         }
    1313           12 :     }
    1314              : 
    1315              :     /// The drain folds one request's events into ONE append per entity run
    1316              :     /// — a 2-attribute entity is one call carrying both, not two — and
    1317              :     /// keeps production order across entities.
    1318              :     #[tokio::test]
    1319            2 :     async fn event_list_folds_a_request_into_one_append_per_entity_run() {
    1320            2 :         let d = Recorder::default();
    1321            2 :         d.event_list(&[
    1322            2 :             ev(TemporalOp::AttrCreated, "urn:a", "speed", 1),
    1323            2 :             ev(TemporalOp::AttrCreated, "urn:a", "speed", 2),
    1324            2 :             ev(TemporalOp::AttrModified, "urn:a", "heading", 3),
    1325            2 :             ev(TemporalOp::AttrModified, "urn:b", "speed", 4),
    1326            2 :             ev(TemporalOp::AttrModified, "urn:a", "speed", 5),
    1327            2 :         ])
    1328            2 :         .await
    1329            2 :         .expect("drain ok");
    1330            2 :         let appends = d.appends.lock().expect("lock").clone();
    1331            6 :         let ids: Vec<&str> = appends.iter().map(|(id, _)| id.as_str()).collect();
    1332            2 :         assert_eq!(
    1333              :             ids,
    1334              :             ["urn:a", "urn:b", "urn:a"],
    1335              :             "one append per entity run, in order"
    1336              :         );
    1337            2 :         let first = &appends[0].1;
    1338            2 :         assert_eq!(first["speed"].as_array().map(Vec::len), Some(2), "{first}");
    1339            2 :         assert_eq!(
    1340            2 :             first["heading"].as_array().map(Vec::len),
    1341              :             Some(1),
    1342              :             "{first}"
    1343              :         );
    1344            2 :         assert!(
    1345            2 :             first.get("value").is_none(),
    1346              :             "instances live under their attribute: {first}"
    1347              :         );
    1348            2 :         assert_eq!(appends[2].1["speed"][0]["value"], 5);
    1349            2 :     }
    1350              : 
    1351              :     /// 4.5.6: a scope change becomes a scope instance on the held temporal
    1352              :     /// doc (array-of-instances form), not an attribute append.
    1353              :     #[tokio::test]
    1354            2 :     async fn event_list_records_scope_changes_as_scope_instances() {
    1355            2 :         let t = TenantId::new("t").expect("tenant");
    1356            2 :         let d = Recorder::default();
    1357            2 :         d.create(
    1358            2 :             &t,
    1359            2 :             "urn:a",
    1360            2 :             serde_json::json!({"id": "urn:a", "scope": "/old"}),
    1361            2 :         )
    1362            2 :         .await
    1363            2 :         .expect("create");
    1364            2 :         let mut scope = ev(TemporalOp::ScopeChanged, "urn:a", "scope", 0);
    1365            2 :         scope.instance = serde_json::json!({"type": "Property", "value": "/new",
    1366            2 :                                             "observedAt": "2026-01-01T00:00:00Z"});
    1367            2 :         d.event_list(&[scope]).await.expect("drain ok");
    1368            2 :         assert!(
    1369            2 :             d.appends.lock().expect("lock").is_empty(),
    1370              :             "no attribute append for a scope change"
    1371              :         );
    1372            2 :         let doc = d.get(&t, "urn:a").await.expect("get").expect("doc");
    1373            2 :         assert_eq!(doc["scope"][0]["value"], "/new", "{doc}");
    1374            2 :         assert_eq!(
    1375            2 :             doc["scope"].as_array().map(Vec::len),
    1376            2 :             Some(1),
    1377            2 :             "the plain scope became an instance array: {doc}"
    1378            2 :         );
    1379            2 :     }
    1380              : 
    1381              :     #[tokio::test]
    1382            2 :     async fn no_temporal_degrades_without_panicking() {
    1383            2 :         let t = TenantId::new("t").expect("tenant");
    1384            2 :         let d: std::sync::Arc<dyn TemporalDriver> = std::sync::Arc::new(NoTemporal);
    1385            2 :         assert!(!d.supported());
    1386              :         // client-facing reads: the spec error, not a panic
    1387            2 :         let e = match d
    1388            2 :             .query_temporal(&t, &filter::TemporalFilter::default())
    1389            2 :             .await
    1390              :         {
    1391            2 :             Err(e) => e,
    1392            0 :             Ok(_) => panic!("query_temporal on NoTemporal must be unsupported"),
    1393              :         };
    1394            2 :         assert_eq!(e.status(), 422);
    1395            2 :         assert_eq!(e.kind(), "OperationNotSupported");
    1396              :         // internal bookkeeping: benign no-ops
    1397            2 :         assert!(d
    1398            2 :             .temporal_append(&t, "x", &Value::Null, &Value::Null)
    1399            2 :             .await
    1400            2 :             .is_ok());
    1401            2 :         assert!(!d.delete(&t, "x").await.expect("ok"));
    1402            2 :         assert!(d.list(&t).await.expect("ok").is_empty());
    1403            2 :         assert!(d
    1404            2 :             .mutate::<(), ()>(&t, "x", |_| Ok(()))
    1405            2 :             .await
    1406            2 :             .expect("ok")
    1407            2 :             .is_none());
    1408            2 :     }
    1409              : }
        

Generated by: LCOV version 2.0-1