LCOV - code coverage report
Current view: top level - antares-sql/src/store - any.rs (source / functions) Coverage Total Hit
Test: merged.info Lines: 90.3 % 1511 1365
Test Date: 2026-11-02 13:21:27 Functions: 55.0 % 756 416

            Line data    Source code
       1              : // SPDX-License-Identifier: EUPL-1.2
       2              : //! The store seam: ONE closed set of backends behind the
       3              : //! memory store's 12-method surface. An enum, not a trait — `mutate<T, E>` is
       4              : //! generic (dyn-incompatible), the backend set is closed by design
       5              : //! (exactly the implementations the product needs), and match
       6              : //! exhaustiveness forces every backend to answer every method.
       7              : //!
       8              : //! Every method returns `Result<_, NgsiError>`: the memory/file backend never
       9              : //! errors; the postgres backend maps sqlx failures to `InternalError` (500) —
      10              : //! a DB outage must be a visible 5xx, never a silent 404/409.
      11              : 
      12              : use antares_model::{NgsiError, TenantId};
      13              : use serde_json::Value;
      14              : 
      15              : #[cfg(feature = "postgres")]
      16              : use super::pg::doc::{DocKind, PgDocStore};
      17              : #[cfg(feature = "postgres")]
      18              : use super::pg::entity::PgEntityStore;
      19              : #[cfg(feature = "postgres")]
      20              : use super::pg::temporal::PgTemporalStore;
      21              : use super::{ChangeHook, Kind, Store};
      22              : #[cfg(feature = "postgres")]
      23              : use antares_store::ChangeVersion;
      24              : 
      25              : /// 5.5.6: unexpected failures (database errors, timeouts) surface as
      26              : /// InternalError. The client-visible detail is deliberately generic —
      27              : /// driver internals (SQL text, constraint names, connection state) go to
      28              : /// the server log only.
      29              : #[cfg(feature = "postgres")]
      30           69 : pub(crate) fn db(e: sqlx::Error) -> NgsiError {
      31              :     // The store's own spec errors travel out through the same sqlx channel
      32              :     // (the signature is fixed by the callers), so recover them — by VALUE, so
      33              :     // the variant and therefore the status survive — before the generic
      34              :     // mapping turns them all into a 500.
      35           69 :     if let sqlx::Error::Configuration(b) = e {
      36            8 :         return match b.downcast::<NgsiError>() {
      37            6 :             Ok(n) => *n,
      38            2 :             Err(b) => {
      39            2 :                 tracing::error!("database error: {b}");
      40            2 :                 NgsiError::InternalError("database error".into())
      41              :             }
      42              :         };
      43           61 :     }
      44              :     // The pool handed out no connection inside its acquire timeout: the
      45              :     // broker is holding every connection it may hold and the caller waited
      46              :     // the whole wall. That is overload, not a fault — the binding answers it
      47              :     // 503 with Retry-After (`negotiate::ApiError::Overloaded`), so the detail
      48              :     // is the constant both ends name.
      49           61 :     if matches!(e, sqlx::Error::PoolTimedOut) {
      50            2 :         metrics::counter!("antares_pg_pool_timeouts_total").increment(1);
      51            2 :         tracing::warn!(
      52              :             "connection pool exhausted: no connection within the acquire timeout \
      53              :              (raise ANTARES_PG_POOL, or the request rate is above what this \
      54              :              database can serve)"
      55              :         );
      56            2 :         return NgsiError::InternalError(antares_model::error::DB_OVERLOADED.into());
      57           59 :     }
      58              :     // 5.5.2: "database timeouts" are InternalError. SQLSTATE 57014 is the
      59              :     // session's statement_timeout firing — named in the detail so a wall hit
      60              :     // reads differently from a broken query in the operator's log.
      61           59 :     if let sqlx::Error::Database(d) = &e {
      62            6 :         if d.code().as_deref() == Some("57014") {
      63            6 :             tracing::warn!("database statement timeout: {d}");
      64            6 :             return NgsiError::InternalError("database statement timeout".into());
      65            0 :         }
      66           53 :     }
      67           53 :     tracing::error!("database error: {e}");
      68           53 :     NgsiError::InternalError("database error".into())
      69           69 : }
      70              : 
      71              : /// 4.22: the read-boundary "now" — every entity read strips expired docs and
      72              : /// instances against this stamp (UTC Z, millisecond precision, the same form
      73              : /// the broker writes into system timestamps).
      74        68924 : pub(crate) fn now_utc() -> String {
      75        68924 :     chrono::Utc::now().to_rfc3339_opts(chrono::SecondsFormat::Millis, true)
      76        68924 : }
      77              : 
      78              : #[cfg(feature = "postgres")]
      79       611697 : fn doc_kind(kind: Kind) -> Result<DocKind, NgsiError> {
      80       611697 :     match kind {
      81       144647 :         Kind::Subscription => Ok(DocKind::Subscription),
      82       173395 :         Kind::Registration => Ok(DocKind::Registration),
      83        14552 :         Kind::CSourceSubscription => Ok(DocKind::CSourceSubscription),
      84       137299 :         Kind::Snapshot => Ok(DocKind::Snapshot),
      85       137504 :         Kind::EntityMap => Ok(DocKind::EntityMap),
      86         4292 :         Kind::DistSub => Ok(DocKind::DistSub),
      87            4 :         Kind::DeadLetter => Ok(DocKind::DeadLetter),
      88            4 :         Kind::View => Ok(DocKind::View),
      89              :         // Entities and Temporal Representations have their own tables and
      90              :         // never reach the document store. Every caller routes them to their
      91              :         // own match arm first; one that does not has mismatched its arms,
      92              :         // and the mistake stays inside the one request.
      93            0 :         Kind::Entity | Kind::Temporal => Err(NgsiError::InternalError(format!(
      94            0 :             "{kind:?} is not a document-store kind"
      95            0 :         ))),
      96              :     }
      97       611697 : }
      98              : 
      99              : /// A transaction scoped to one tenant: begun, then RLS set for it first.
     100              : #[cfg(feature = "postgres")]
     101           82 : async fn tenant_tx(
     102           82 :     p: &PgBackend,
     103           82 :     tenant: &TenantId,
     104           82 : ) -> Result<sqlx::Transaction<'static, sqlx::Postgres>, NgsiError> {
     105           82 :     let mut tx = p.docs.pool().begin().await.map_err(db)?;
     106           82 :     crate::store::pg::set_tenant(&mut tx, tenant)
     107           82 :         .await
     108           82 :         .map_err(db)?;
     109           82 :     Ok(tx)
     110           82 : }
     111              : 
     112              : /// Postgres backend bundle: one pool, three table-family stores, plus the
     113              : /// change hook (the memory store emits its own; here the seam emits).
     114              : #[cfg(feature = "postgres")]
     115              : pub struct PgBackend {
     116              :     pub entities: PgEntityStore,
     117              :     pub temporal: PgTemporalStore,
     118              :     pub docs: PgDocStore,
     119              :     hook: std::sync::RwLock<Option<ChangeHook>>,
     120              :     /// Server and extension versions, read once at startup
     121              :     /// (`with_version`); `/q/health` serves this copy rather than querying
     122              :     /// the database on a polled endpoint.
     123              :     version: Value,
     124              : }
     125              : 
     126              : #[cfg(feature = "postgres")]
     127              : impl PgBackend {
     128          106 :     pub fn new(pool: sqlx::postgres::PgPool) -> Self {
     129          106 :         Self {
     130          106 :             entities: PgEntityStore::new(pool.clone()),
     131          106 :             temporal: PgTemporalStore::new(pool.clone()),
     132          106 :             docs: PgDocStore::new(pool),
     133          106 :             hook: std::sync::RwLock::new(None),
     134          106 :             version: Value::Object(serde_json::Map::new()),
     135          106 :         }
     136          106 :     }
     137              : 
     138              :     /// Seal the notification credentials under `ANTARES_SECRET_KEY`
     139              :     /// ([`super::pg::seal`]).
     140            4 :     pub fn with_secret_key(mut self, key: super::pg::seal::SecretKey) -> Self {
     141            4 :         self.docs = self.docs.with_secret_key(key);
     142            4 :         self
     143            4 :     }
     144              : 
     145              :     /// Give the batch writes their own pool (`ANTARES_PG_WRITE_RESERVE`):
     146              :     /// reads keep the main pool, so a flood of them cannot queue
     147              :     /// ahead of a pipeline's batch upsert.
     148            0 :     pub fn with_write_reserve(mut self, reserve: sqlx::postgres::PgPool) -> Self {
     149            0 :         self.entities = self.entities.with_batch_pool(reserve);
     150            0 :         self
     151            0 :     }
     152              : 
     153              :     /// Attach what the startup probe read from the server
     154              :     /// (`pg::version_info`), so `/q/health` can answer it without a query.
     155            4 :     pub fn with_version(mut self, version: Value) -> Self {
     156            4 :         self.version = version;
     157            4 :         self
     158            4 :     }
     159              : 
     160              :     /// Poison recovery (`into_inner`) is deliberate, the same choice the
     161              :     /// memory arm records: the hook runs real code over attacker-shaped JSON,
     162              :     /// and a panic inside it must unwind one request, not poison this lock
     163              :     /// and panic every later entity write until the process restarts.
     164         9508 :     async fn emit(
     165         9508 :         &self,
     166         9508 :         tenant: &TenantId,
     167         9508 :         before: Option<Value>,
     168         9508 :         after: Option<Value>,
     169         9508 :         version: ChangeVersion,
     170         9508 :     ) {
     171              :         // cloned out of the lock first: a read guard cannot be held across
     172              :         // the await the hook now costs.
     173         9508 :         let hook = self
     174         9508 :             .hook
     175         9508 :             .read()
     176         9508 :             .unwrap_or_else(std::sync::PoisonError::into_inner)
     177         9508 :             .clone();
     178         9508 :         if let Some(h) = hook {
     179         4120 :             h(tenant, before, after, version).await;
     180         5388 :         }
     181         9508 :     }
     182              : }
     183              : 
     184              : /// Instances per Attribute of one temporal document: every array member
     185              : /// whose items are all objects (an Entity's own `type` list is not one).
     186         5916 : fn instance_counts(doc: &Value) -> std::collections::HashMap<String, usize> {
     187         5916 :     doc.as_object()
     188         5916 :         .into_iter()
     189         5916 :         .flatten()
     190        28998 :         .filter_map(|(k, v)| {
     191        28998 :             let l = v.as_array().filter(|l| l.iter().all(Value::is_object))?;
     192        11180 :             Some((k.clone(), l.len()))
     193        28998 :         })
     194         5916 :         .collect()
     195         5916 : }
     196              : 
     197              : /// The memory/file store's history cap ([`Store::max_instances`]): the
     198              : /// write that leaves an Attribute of `after` past `cap` instances, more than
     199              : /// `was` held, is refused. One already past it that the write does not grow
     200              : /// (a lowered cap) still takes corrections and deletions.
     201       242840 : fn past_instance_cap(
     202       242840 :     was: &std::collections::HashMap<String, usize>,
     203       242840 :     after: &Value,
     204       242840 :     cap: usize,
     205       242840 : ) -> Result<(), NgsiError> {
     206       242840 :     if cap == 0 {
     207       238940 :         return Ok(());
     208         3900 :     }
     209         3900 :     match instance_counts(after)
     210         3900 :         .into_iter()
     211         9720 :         .find(|(k, n)| *n > cap && *n > was.get(k).copied().unwrap_or(0))
     212              :     {
     213              :         // A cap the client can do something about, refused like the batch
     214              :         // cap (400 BadRequestData): the same request will not fit later
     215              :         // either, until older instances are deleted or reaped.
     216           12 :         Some((attr, n)) => Err(NgsiError::BadRequestData(format!(
     217           12 :             "the history of Attribute {attr} would hold {n} instances, more than the \
     218           12 :              {cap} instances this broker keeps per Attribute; delete older instances first"
     219           12 :         ))),
     220         3888 :         None => Ok(()),
     221              :     }
     222       242840 : }
     223              : 
     224              : /// What `antares-api` sees. No core crate names redb or sqlx.
     225              : // One AnyStore exists per process — variant size difference is irrelevant.
     226              : #[allow(clippy::large_enum_variant)]
     227              : pub enum AnyStore {
     228              :     Mem(Store),
     229              :     #[cfg(feature = "postgres")]
     230              :     Pg(PgBackend),
     231              : }
     232              : 
     233              : /// A row's `id`, or `""` for one without: an id-less row sorts first and is
     234              : /// never skipped past, so a keyset walk over it cannot lose it. Only the
     235              : /// Postgres arm sorts rows it read whole; the memory arm's maps are already
     236              : /// keyed by id.
     237              : #[cfg(feature = "postgres")]
     238            0 : fn row_id(v: &Value) -> &str {
     239            0 :     v.get("id").and_then(Value::as_str).unwrap_or_default()
     240            0 : }
     241              : 
     242              : /// The bound on one size scan of a tenant (`GET /q/tenants/{tenant}`):
     243              : /// the history table runs to tens of GB, and a size is not worth a
     244              : /// longer hold on a connection.
     245              : #[cfg(feature = "postgres")]
     246              : const SIZE_SCAN_TIMEOUT: &str = "SET LOCAL statement_timeout = '5s'";
     247              : 
     248              : /// The attribute instances of one tenant, both history tables.
     249              : #[cfg(feature = "postgres")]
     250            8 : async fn count_instances(
     251            8 :     tx: &mut sqlx::Transaction<'static, sqlx::Postgres>,
     252            8 :     tenant: &TenantId,
     253            8 : ) -> Result<u64, NgsiError> {
     254            8 :     let n: i64 = sqlx::query_scalar(
     255            8 :         "SELECT (SELECT count(*) FROM attr_instances WHERE tenant_id = $1)
     256            8 :               + (SELECT count(*) FROM attr_rows WHERE tenant_id = $1)",
     257            8 :     )
     258            8 :     .bind(tenant.as_str())
     259            8 :     .fetch_one(&mut **tx)
     260            8 :     .await
     261            8 :     .map_err(db)?;
     262            6 :     Ok(n.max(0) as u64)
     263            8 : }
     264              : 
     265              : impl AnyStore {
     266              :     /// Readiness ping: can the store answer a trivial request
     267              :     /// RIGHT NOW? Memory/file are in-process (always ready); the Pg arm runs
     268              :     /// `SELECT 1` so a lost database (failover, network partition) flips
     269              :     /// /q/ready to 503 and the Service stops routing to this pod.
     270           16 :     pub async fn ping(&self) -> Result<(), NgsiError> {
     271           16 :         match self {
     272           16 :             AnyStore::Mem(_) => Ok(()),
     273              :             #[cfg(feature = "postgres")]
     274            0 :             AnyStore::Pg(p) => async {
     275            0 :                 sqlx::query("SELECT 1")
     276            0 :                     .execute(p.docs.pool())
     277            0 :                     .await
     278            0 :                     .map(|_| ())
     279            0 :             }
     280            0 :             .await
     281            0 :             .map_err(db),
     282              :         }
     283           16 :     }
     284              : 
     285              :     /// What this store runs on, for `/q/health`. The memory and file modes
     286              :     /// are one backend with two durability shapes and must not read as the
     287              :     /// same thing; the Postgres arm serves what the startup probe read.
     288          174 :     pub fn version_info(&self) -> Value {
     289          174 :         match self {
     290          158 :             AnyStore::Mem(s) => serde_json::json!({
     291          158 :                 "engine": if s.shadowed() { "redb" } else { "memory" },
     292              :             }),
     293              :             #[cfg(feature = "postgres")]
     294           16 :             AnyStore::Pg(p) => p.version.clone(),
     295              :         }
     296          174 :     }
     297              : 
     298              :     /// (Queued writers, peak) behind the single redb committer, reported
     299              :     /// only by a store that has one: `None` for the Pg arm (Postgres has no
     300              :     /// single-writer commit queue) and for a pure in-memory store, whose
     301              :     /// lock depth is not the fsync queue this number stands for.
     302           90 :     pub fn commit_queue(&self) -> Option<(usize, usize)> {
     303           90 :         match self {
     304           84 :             AnyStore::Mem(s) => s.shadowed().then(|| s.commit_queue()),
     305              :             #[cfg(feature = "postgres")]
     306            6 :             AnyStore::Pg(_) => None,
     307              :         }
     308           90 :     }
     309              : 
     310              :     /// The last step of the drain: close the connection pool so in-flight
     311              :     /// transactions finish and the server sees a clean disconnect instead of
     312              :     /// N abandoned backends. A no-op for the memory/file arm, whose durability
     313              :     /// is already commit-before-ack — there is nothing buffered to lose.
     314           26 :     pub async fn close(&self) {
     315           26 :         match self {
     316           22 :             AnyStore::Mem(_) => {}
     317              :             #[cfg(feature = "postgres")]
     318            4 :             AnyStore::Pg(p) => p.docs.pool().close().await,
     319              :         }
     320           26 :     }
     321              : 
     322         1292 :     pub fn set_change_hook(&self, h: ChangeHook) {
     323         1292 :         match self {
     324         1258 :             AnyStore::Mem(s) => s.set_change_hook(h),
     325              :             #[cfg(feature = "postgres")]
     326           34 :             AnyStore::Pg(p) => {
     327           34 :                 *p.hook
     328           34 :                     .write()
     329           34 :                     .unwrap_or_else(std::sync::PoisonError::into_inner) = Some(h)
     330              :             }
     331              :         }
     332         1292 :     }
     333              : 
     334              :     /// Turn the same-tx outbox producer on (bus=nats). The memory arm has
     335              :     /// no outbox — the broker's wiring rejects bus=nats without a Pg store,
     336              :     /// so this is unreachable there by construction.
     337            2 :     pub fn set_outbox(
     338            2 :         &self,
     339            2 :         #[cfg_attr(not(feature = "postgres"), allow(unused_variables))] on: bool,
     340            2 :     ) {
     341            2 :         match self {
     342            0 :             AnyStore::Mem(_) => {}
     343              :             #[cfg(feature = "postgres")]
     344            2 :             AnyStore::Pg(p) => p.entities.set_outbox(on),
     345              :         }
     346            2 :     }
     347              : 
     348              :     /// Seal the notification credentials a release before
     349              :     /// `ANTARES_SECRET_KEY` stored in plaintext; the count of rows
     350              :     /// sealed. The memory arm seals nothing.
     351            0 :     pub async fn seal_plaintext_credentials(&self) -> Result<u64, NgsiError> {
     352            0 :         match self {
     353            0 :             AnyStore::Mem(_) => Ok(0),
     354              :             #[cfg(feature = "postgres")]
     355            0 :             AnyStore::Pg(p) => p.docs.seal_plaintext().await.map_err(db),
     356              :         }
     357            0 :     }
     358              : 
     359              :     /// Asynchronous commit for the batch writes (ANTARES_PG_BATCH_ASYNC_COMMIT). The memory arm has no commit to relax.
     360            2 :     pub fn set_async_batch_commit(
     361            2 :         &self,
     362            2 :         #[cfg_attr(not(feature = "postgres"), allow(unused_variables))] on: bool,
     363            2 :     ) {
     364            2 :         match self {
     365            0 :             AnyStore::Mem(_) => {}
     366              :             #[cfg(feature = "postgres")]
     367            2 :             AnyStore::Pg(p) => p.entities.set_async_batch_commit(on),
     368              :         }
     369            2 :     }
     370              : 
     371              :     /// Outbox drain: oldest-first page of pending rows `(seq, tenant, event)`.
     372          730 :     pub async fn outbox_peek(
     373          730 :         &self,
     374          730 :         #[cfg_attr(not(feature = "postgres"), allow(unused_variables))] limit: i64,
     375          730 :     ) -> Result<Vec<(i64, String, Value)>, NgsiError> {
     376          730 :         match self {
     377            8 :             AnyStore::Mem(_) => Ok(Vec::new()),
     378              :             #[cfg(feature = "postgres")]
     379          722 :             AnyStore::Pg(p) => super::pg::outbox::peek(p.docs.pool(), limit)
     380          722 :                 .await
     381          722 :                 .map_err(db),
     382              :         }
     383          730 :     }
     384              : 
     385              :     /// Outbox drain: delete EXACTLY the published rows (never a blanket
     386              :     /// `seq <= max`, which loses a row committing between peek and ack).
     387            0 :     pub async fn outbox_ack(
     388            0 :         &self,
     389            0 :         #[cfg_attr(not(feature = "postgres"), allow(unused_variables))] seqs: &[i64],
     390            0 :     ) -> Result<u64, NgsiError> {
     391            0 :         match self {
     392            0 :             AnyStore::Mem(_) => Ok(0),
     393              :             #[cfg(feature = "postgres")]
     394            0 :             AnyStore::Pg(p) => super::pg::outbox::ack(p.docs.pool(), seqs)
     395            0 :                 .await
     396            0 :                 .map_err(db),
     397              :         }
     398            0 :     }
     399              : 
     400              :     /// Outbox drain: keep the rows of events the bus could not carry whole.
     401              :     /// The memory arm has no outbox, so nothing is ever retained there.
     402            0 :     pub async fn outbox_retain(
     403            0 :         &self,
     404            0 :         #[cfg_attr(not(feature = "postgres"), allow(unused_variables))] tenant: &TenantId,
     405            0 :         #[cfg_attr(not(feature = "postgres"), allow(unused_variables))] seqs: &[i64],
     406            0 :     ) -> Result<u64, NgsiError> {
     407            0 :         match self {
     408            0 :             AnyStore::Mem(_) => Ok(0),
     409              :             #[cfg(feature = "postgres")]
     410            0 :             AnyStore::Pg(p) => super::pg::outbox::retain(p.docs.pool(), tenant, seqs)
     411            0 :                 .await
     412            0 :                 .map_err(db),
     413              :         }
     414            0 :     }
     415              : 
     416              :     /// The whole event behind a claim-check reference, `None` once reaped.
     417            2 :     pub async fn outbox_event(
     418            2 :         &self,
     419            2 :         #[cfg_attr(not(feature = "postgres"), allow(unused_variables))] seq: i64,
     420            2 :         #[cfg_attr(not(feature = "postgres"), allow(unused_variables))] tenant: &TenantId,
     421            2 :     ) -> Result<Option<Value>, NgsiError> {
     422            2 :         match self {
     423            0 :             AnyStore::Mem(_) => Ok(None),
     424              :             #[cfg(feature = "postgres")]
     425            2 :             AnyStore::Pg(p) => super::pg::outbox::event(p.docs.pool(), seq, tenant)
     426            2 :                 .await
     427            2 :                 .map_err(db),
     428              :         }
     429            2 :     }
     430              : 
     431              :     /// Create one document of `kind`; `false` when that id is already taken.
     432       162148 :     pub async fn create(
     433       162148 :         &self,
     434       162148 :         tenant: &TenantId,
     435       162148 :         kind: Kind,
     436       162148 :         id: &str,
     437       162148 :         doc: Value,
     438       162148 :     ) -> Result<bool, NgsiError> {
     439       162148 :         match self {
     440       136812 :             AnyStore::Mem(s) => {
     441       136812 :                 if kind == Kind::Temporal {
     442          806 :                     past_instance_cap(&Default::default(), &doc, s.max_instances)?;
     443       136006 :                 }
     444       136810 :                 Ok(s.create(tenant, kind, id, doc).await)
     445              :             }
     446              :             #[cfg(feature = "postgres")]
     447        25336 :             AnyStore::Pg(p) => match kind {
     448              :                 Kind::Entity => {
     449         2244 :                     let created = p
     450         2244 :                         .entities
     451         2244 :                         .create_stamped(tenant, id, &doc)
     452         2244 :                         .await
     453         2244 :                         .map_err(db)?;
     454         2244 :                     let hit = created.is_some();
     455         2244 :                     if let Some(v) = created {
     456         2234 :                         p.emit(tenant, None, Some(doc), v).await;
     457           10 :                     }
     458         2244 :                     Ok(hit)
     459              :                 }
     460          514 :                 Kind::Temporal => p.temporal.create(tenant, id, &doc).await.map_err(db),
     461              :                 _ => {
     462        22578 :                     let dk = doc_kind(kind)?;
     463        22578 :                     p.docs.create(tenant, dk, id, &doc).await.map_err(db)
     464              :                 }
     465              :             },
     466              :         }
     467       162148 :     }
     468              : 
     469              :     /// Batch create (entities only): one multi-row statement on the Pg
     470              :     /// arm, per-item loop on the memory arm. Created-flags in input order.
     471          156 :     pub async fn batch_create(
     472          156 :         &self,
     473          156 :         tenant: &TenantId,
     474          156 :         items: Vec<(String, Value)>,
     475          156 :     ) -> Result<Vec<bool>, NgsiError> {
     476          156 :         match self {
     477          120 :             AnyStore::Mem(s) => {
     478          120 :                 let mut out = Vec::with_capacity(items.len());
     479         5174 :                 for (id, doc) in items {
     480         5174 :                     out.push(s.create(tenant, Kind::Entity, &id, doc).await);
     481              :                 }
     482          120 :                 Ok(out)
     483              :             }
     484              :             #[cfg(feature = "postgres")]
     485           36 :             AnyStore::Pg(p) => {
     486           36 :                 let created = p
     487           36 :                     .entities
     488           36 :                     .batch_create_stamped(tenant, &items)
     489           36 :                     .await
     490           36 :                     .map_err(db)?;
     491           36 :                 let flags = created.iter().map(Option::is_some).collect();
     492           54 :                 for ((_, doc), created) in items.into_iter().zip(created) {
     493           54 :                     if let Some(v) = created {
     494           42 :                         p.emit(tenant, None, Some(doc), v).await;
     495           12 :                     }
     496              :                 }
     497           36 :                 Ok(flags)
     498              :             }
     499              :         }
     500          156 :     }
     501              : 
     502              :     /// Batch delete (entities only): deleted-flags in input order; a
     503              :     /// duplicate id in the input deletes once and 404s the second time,
     504              :     /// matching the per-item loop's semantics (5.5.11.4).
     505          390 :     pub async fn batch_delete(
     506          390 :         &self,
     507          390 :         tenant: &TenantId,
     508          390 :         ids: &[String],
     509          390 :     ) -> Result<Vec<bool>, NgsiError> {
     510          390 :         match self {
     511          224 :             AnyStore::Mem(s) => {
     512          224 :                 let mut out = Vec::with_capacity(ids.len());
     513         7194 :                 for id in ids {
     514         7194 :                     out.push(s.delete(tenant, Kind::Entity, id).await);
     515              :                 }
     516          224 :                 Ok(out)
     517              :             }
     518              :             #[cfg(feature = "postgres")]
     519          166 :             AnyStore::Pg(p) => {
     520          166 :                 let deleted = p
     521          166 :                     .entities
     522          166 :                     .batch_delete_stamped(tenant, ids)
     523          166 :                     .await
     524          166 :                     .map_err(db)?;
     525          166 :                 let mut prev: std::collections::HashMap<String, (Value, ChangeVersion)> = deleted
     526          166 :                     .into_iter()
     527         1460 :                     .map(|(id, before, v)| (id, (before, v)))
     528          166 :                     .collect();
     529          166 :                 let mut out = Vec::with_capacity(ids.len());
     530         1484 :                 for id in ids {
     531         1484 :                     match prev.remove(id) {
     532         1460 :                         Some((before, v)) => {
     533         1460 :                             p.emit(tenant, Some(before), None, v).await;
     534         1460 :                             out.push(true);
     535              :                         }
     536           24 :                         None => out.push(false),
     537              :                     }
     538              :                 }
     539          166 :                 Ok(out)
     540              :             }
     541              :         }
     542          390 :     }
     543              : 
     544        20822 :     pub async fn upsert(
     545        20822 :         &self,
     546        20822 :         tenant: &TenantId,
     547        20822 :         kind: Kind,
     548        20822 :         id: &str,
     549        20822 :         doc: Value,
     550        20822 :     ) -> Result<bool, NgsiError> {
     551        20822 :         match self {
     552        18364 :             AnyStore::Mem(s) => {
     553        18364 :                 if kind == Kind::Temporal {
     554           52 :                     past_instance_cap(&Default::default(), &doc, s.max_instances)?;
     555        18312 :                 }
     556        18364 :                 Ok(s.upsert(tenant, kind, id, doc).await)
     557              :             }
     558              :             #[cfg(feature = "postgres")]
     559         2458 :             AnyStore::Pg(p) => match kind {
     560              :                 Kind::Entity => {
     561              :                     // replace-or-create without a pre-read: try the replace
     562              :                     // first (captures the true before-image under the row
     563              :                     // lock), fall back to create, and on a lost create race
     564              :                     // replace after all.
     565         2434 :                     let mut prev: Option<Value> = None;
     566         2434 :                     let mut version = ChangeVersion::default();
     567              :                     // one shape, two places: a closure cannot hold the await
     568              :                     // and an async closure is not a stable language feature,
     569              :                     // so the replace is written once and expanded twice
     570              :                     macro_rules! replace {
     571              :                         () => {
     572              :                             match p
     573              :                                 .entities
     574           14 :                                 .mutate_stamped(tenant, id, |d| {
     575           14 :                                     prev = Some(d.clone());
     576           14 :                                     *d = doc.clone();
     577           14 :                                     Ok::<(), std::convert::Infallible>(())
     578           14 :                                 })
     579              :                                 .await
     580              :                                 .map_err(db)?
     581              :                             {
     582              :                                 Some(Ok(((), v))) => {
     583              :                                     version = v;
     584              :                                     true
     585              :                                 }
     586              :                                 Some(Err(never)) => match never {},
     587              :                                 None => false,
     588              :                             }
     589              :                         };
     590              :                     }
     591         2434 :                     let mut existed = replace!();
     592         2434 :                     if !existed {
     593         2420 :                         if let Some(v) = p
     594         2420 :                             .entities
     595         2420 :                             .create_stamped(tenant, id, &doc)
     596         2420 :                             .await
     597         2420 :                             .map_err(db)?
     598         2420 :                         {
     599         2420 :                             version = v;
     600         2420 :                             existed = false;
     601         2420 :                         } else {
     602              :                             // lost the create race — replace instead
     603            0 :                             existed = replace!();
     604              :                         }
     605           14 :                     }
     606         2434 :                     p.emit(tenant, prev, Some(doc), version).await;
     607         2434 :                     Ok(existed)
     608              :                 }
     609              :                 Kind::Temporal => {
     610            4 :                     if p.temporal.create(tenant, id, &doc).await.map_err(db)? {
     611            0 :                         Ok(false)
     612              :                     } else {
     613            4 :                         p.temporal
     614            4 :                             .mutate(tenant, id, |d| {
     615            4 :                                 *d = doc.clone();
     616            4 :                                 Ok::<(), std::convert::Infallible>(())
     617            4 :                             })
     618            4 :                             .await
     619            4 :                             .map_err(db)?;
     620            4 :                         Ok(true)
     621              :                     }
     622              :                 }
     623              :                 _ => {
     624           20 :                     let dk = doc_kind(kind)?;
     625           20 :                     p.docs.upsert(tenant, dk, id, &doc).await.map_err(db)
     626              :                 }
     627              :             },
     628              :         }
     629        20822 :     }
     630              : 
     631        32284 :     pub async fn get(
     632        32284 :         &self,
     633        32284 :         tenant: &TenantId,
     634        32284 :         kind: Kind,
     635        32284 :         id: &str,
     636        32284 :     ) -> Result<Option<Value>, NgsiError> {
     637        32284 :         let doc = match self {
     638        26293 :             AnyStore::Mem(s) => s.get(tenant, kind, id),
     639              :             #[cfg(feature = "postgres")]
     640         5991 :             AnyStore::Pg(p) => match kind {
     641         1274 :                 Kind::Entity => p.entities.get(tenant, id).await.map_err(db)?,
     642          524 :                 Kind::Temporal => p.temporal.get(tenant, id).await.map_err(db)?,
     643         4193 :                 _ => p.docs.get(tenant, doc_kind(kind)?, id).await.map_err(db)?,
     644              :             },
     645              :         };
     646              :         // 4.22: an expired entity is invalid context — a read serves it to
     647              :         // no one, whichever arm stored it (the Pg sweep lags by design).
     648        32284 :         if kind == Kind::Entity {
     649        14846 :             let now = now_utc();
     650        14846 :             if let Some(mut d) = doc {
     651        12620 :                 if crate::store::filter::strip_expired(&mut d, &now) {
     652           16 :                     return Ok(None);
     653        12604 :                 }
     654        12604 :                 return Ok(Some(d));
     655         2226 :             }
     656         2226 :             return Ok(None);
     657        17438 :         }
     658        17438 :         Ok(doc)
     659        32284 :     }
     660              : 
     661              :     /// [`Self::get`] of several Entities, in one transaction on Postgres;
     662              :     /// an absent or expired id has no entry.
     663           72 :     pub async fn get_entities(
     664           72 :         &self,
     665           72 :         tenant: &TenantId,
     666           72 :         ids: &[&str],
     667           72 :     ) -> Result<std::collections::HashMap<String, Value>, NgsiError> {
     668           72 :         let found: std::collections::HashMap<String, Value> = match self {
     669           72 :             AnyStore::Mem(s) => ids
     670           72 :                 .iter()
     671          270 :                 .filter_map(|id| Some(((*id).to_owned(), s.get(tenant, Kind::Entity, id)?)))
     672           72 :                 .collect(),
     673              :             #[cfg(feature = "postgres")]
     674            0 :             AnyStore::Pg(p) => p.entities.get_many(tenant, ids).await.map_err(db)?,
     675              :         };
     676           72 :         let now = now_utc();
     677           72 :         Ok(found
     678           72 :             .into_iter()
     679           72 :             .filter_map(|(id, mut d)| {
     680           56 :                 (!crate::store::filter::strip_expired(&mut d, &now)).then_some((id, d))
     681           56 :             })
     682           72 :             .collect())
     683           72 :     }
     684              : 
     685        20116 :     pub async fn delete(&self, tenant: &TenantId, kind: Kind, id: &str) -> Result<bool, NgsiError> {
     686        20116 :         match self {
     687        12472 :             AnyStore::Mem(s) => Ok(s.delete(tenant, kind, id).await),
     688              :             #[cfg(feature = "postgres")]
     689         7644 :             AnyStore::Pg(p) => match kind {
     690              :                 Kind::Entity => {
     691              :                     // the before-image comes from the DELETE's own RETURNING —
     692              :                     // same transaction, never a separate racy read
     693         1396 :                     let prev = p.entities.delete_stamped(tenant, id).await.map_err(db)?;
     694         1396 :                     let hit = prev.is_some();
     695         1396 :                     if let Some((before, v)) = prev {
     696         1322 :                         p.emit(tenant, Some(before), None, v).await;
     697           74 :                     }
     698         1396 :                     Ok(hit)
     699              :                 }
     700         4664 :                 Kind::Temporal => p.temporal.delete(tenant, id).await.map_err(db),
     701         1584 :                 _ => p.docs.delete(tenant, doc_kind(kind)?, id).await.map_err(db),
     702              :             },
     703              :         }
     704        20116 :     }
     705              : 
     706              :     /// Delete one entity only if `keep` accepts the stored document, read
     707              :     /// and removed under one lock (see the driver trait).
     708         3126 :     pub async fn delete_entity_if(
     709         3126 :         &self,
     710         3126 :         tenant: &TenantId,
     711         3126 :         id: &str,
     712         3126 :         keep: &(dyn for<'v> Fn(&'v Value) -> bool + Sync),
     713         3126 :     ) -> Result<bool, NgsiError> {
     714              :         // 4.22 is applied to the document `keep` judges, exactly as `get`
     715              :         // applies it: an expired instance is not there to be matched on.
     716         3126 :         let now = now_utc();
     717         3126 :         let judge = |d: &Value| {
     718         2886 :             let mut d = d.clone();
     719         2886 :             !crate::store::filter::strip_expired(&mut d, &now) && keep(&d)
     720         2886 :         };
     721         3126 :         match self {
     722         1620 :             AnyStore::Mem(s) => Ok(s.delete_if(tenant, id, &judge).await.is_some()),
     723              :             #[cfg(feature = "postgres")]
     724         1506 :             AnyStore::Pg(p) => {
     725              :                 // the before-image comes from the DELETE's own RETURNING —
     726              :                 // same transaction, never a separate racy read
     727         1506 :                 let prev = p
     728         1506 :                     .entities
     729         1506 :                     .delete_if_stamped(tenant, id, &judge)
     730         1506 :                     .await
     731         1506 :                     .map_err(db)?;
     732         1506 :                 let hit = prev.is_some();
     733         1506 :                 if let Some((before, v)) = prev {
     734         1406 :                     p.emit(tenant, Some(before), None, v).await;
     735          100 :                 }
     736         1506 :                 Ok(hit)
     737              :             }
     738              :         }
     739         3126 :     }
     740              : 
     741         5212 :     pub async fn list(&self, tenant: &TenantId, kind: Kind) -> Result<Vec<Value>, NgsiError> {
     742         5212 :         let mut rows = match self {
     743         5078 :             AnyStore::Mem(s) => s.list(tenant, kind),
     744              :             #[cfg(feature = "postgres")]
     745          134 :             AnyStore::Pg(p) => match kind {
     746           32 :                 Kind::Entity => p.entities.list(tenant).await.map_err(db)?,
     747           12 :                 Kind::Temporal => p.temporal.list(tenant).await.map_err(db)?,
     748           90 :                 _ => p.docs.list(tenant, doc_kind(kind)?).await.map_err(db)?,
     749              :             },
     750              :         };
     751         5212 :         if kind == Kind::Entity {
     752          111 :             let now = now_utc();
     753         8223 :             rows.retain_mut(|d| !crate::store::filter::strip_expired(d, &now));
     754         5101 :         }
     755         5212 :         Ok(rows)
     756         5212 :     }
     757              : 
     758              :     /// Every document's id and digest (see `CurrentStateDriver::list_digests`):
     759              :     /// a Postgres doc kind digests in the database, so no body leaves it.
     760            0 :     pub async fn list_digests(
     761            0 :         &self,
     762            0 :         tenant: &TenantId,
     763            0 :         kind: Kind,
     764            0 :     ) -> Result<Vec<(String, u64)>, NgsiError> {
     765              :         #[cfg(feature = "postgres")]
     766            0 :         if let AnyStore::Pg(p) = self {
     767            0 :             if !matches!(kind, Kind::Entity | Kind::Temporal) {
     768            0 :                 return p
     769            0 :                     .docs
     770            0 :                     .list_digests(tenant, doc_kind(kind)?)
     771            0 :                     .await
     772            0 :                     .map_err(db);
     773            0 :             }
     774            0 :         }
     775            0 :         Ok(antares_store::digests(self.list(tenant, kind).await?))
     776            0 :     }
     777              : 
     778              :     /// One id-ordered page of documents (see `CurrentStateDriver::list_page`).
     779       617492 :     pub async fn list_page(
     780       617492 :         &self,
     781       617492 :         tenant: &TenantId,
     782       617492 :         kind: Kind,
     783       617492 :         after: Option<&str>,
     784       617492 :         limit: usize,
     785       617492 :     ) -> Result<Vec<Value>, NgsiError> {
     786       617492 :         match self {
     787        40213 :             AnyStore::Mem(s) => Ok(s.list_page(tenant, kind, after, limit)),
     788              :             #[cfg(feature = "postgres")]
     789       577279 :             AnyStore::Pg(p) => match kind {
     790              :                 // Not doc kinds: they live in their own tables, behind their
     791              :                 // own readers, and `doc_kind` refuses them.
     792              :                 Kind::Entity => {
     793           60 :                     let mut rows = p
     794           60 :                         .entities
     795           60 :                         .list_page(tenant, after, i64::try_from(limit).unwrap_or(i64::MAX))
     796           60 :                         .await
     797           60 :                         .map_err(db)?;
     798              :                     // 4.22: the statement excluded expired ENTITIES, so no
     799              :                     // document leaves this page and its length still means
     800              :                     // what the caller reads it as; expired INSTANCES are
     801              :                     // stripped here, at the read boundary, as everywhere else.
     802           60 :                     let now = now_utc();
     803         2026 :                     rows.retain_mut(|d| !crate::store::filter::strip_expired(d, &now));
     804           60 :                     Ok(rows)
     805              :                 }
     806              :                 // ponytail: temporal is sliced from the whole list, so a
     807              :                 // tenant large enough still refuses this read for volume; the
     808              :                 // upgrade is a keyset reader over the instance rows.
     809              :                 // Temporal has no keyset reader of its own: its documents are
     810              :                 // reconstructed from the instance rows, where the volume that
     811              :                 // needs bounding is instances and not entities — the one kind
     812              :                 // that does not yet honour `list_page`'s contract, and the
     813              :                 // reason nothing internal walks temporal state.
     814              :                 Kind::Temporal => {
     815            0 :                     let mut rows = AnyStore::list(self, tenant, kind).await?;
     816            0 :                     rows.sort_by(|a, b| row_id(a).cmp(row_id(b)));
     817            0 :                     Ok(rows
     818            0 :                         .into_iter()
     819            0 :                         .skip_while(|r| after.is_some_and(|a| row_id(r) <= a))
     820            0 :                         .take(limit)
     821            0 :                         .collect())
     822              :                 }
     823       577219 :                 _ => p
     824       577219 :                     .docs
     825       577219 :                     .list_page(
     826       577219 :                         tenant,
     827       577219 :                         doc_kind(kind)?,
     828       577219 :                         after,
     829       577219 :                         i64::try_from(limit).unwrap_or(i64::MAX),
     830              :                     )
     831       577219 :                     .await
     832       577219 :                     .map_err(db),
     833              :             },
     834              :         }
     835       617492 :     }
     836              : 
     837              :     /// One id-ordered window of documents and the total (see
     838              :     /// `CurrentStateDriver::list_slice`).
     839          288 :     pub async fn list_slice(
     840          288 :         &self,
     841          288 :         tenant: &TenantId,
     842          288 :         kind: Kind,
     843          288 :         offset: usize,
     844          288 :         limit: usize,
     845          288 :     ) -> Result<(Vec<Value>, usize), NgsiError> {
     846          288 :         match self {
     847          194 :             AnyStore::Mem(s) => Ok(s.list_slice(tenant, kind, offset, limit)),
     848              :             #[cfg(feature = "postgres")]
     849           94 :             AnyStore::Pg(p) => match kind {
     850              :                 // Not doc kinds: each reads its own window in SQL, so the
     851              :                 // whole-list ceiling never refuses a page.
     852              :                 Kind::Entity => {
     853           10 :                     let (mut rows, total) = p
     854           10 :                         .entities
     855           10 :                         .list_slice(
     856           10 :                             tenant,
     857           10 :                             i64::try_from(offset).unwrap_or(i64::MAX),
     858           10 :                             i64::try_from(limit).unwrap_or(i64::MAX),
     859           10 :                         )
     860           10 :                         .await
     861           10 :                         .map_err(db)?;
     862              :                     // 4.22 at the read boundary, as `list_page` does
     863           10 :                     let now = now_utc();
     864           18 :                     rows.retain_mut(|d| !crate::store::filter::strip_expired(d, &now));
     865           10 :                     Ok((rows, usize::try_from(total).unwrap_or(usize::MAX)))
     866              :                 }
     867              :                 // Temporal is sliced from the whole list, as in
     868              :                 // `list_page`: its documents are rebuilt from instance rows
     869              :                 // and the paged temporal query skips shells without history;
     870              :                 // the upgrade is a keyset reader over temporal_entities.
     871              :                 Kind::Temporal => {
     872            0 :                     let mut rows = AnyStore::list(self, tenant, kind).await?;
     873            0 :                     rows.sort_by(|a, b| row_id(a).cmp(row_id(b)));
     874            0 :                     let total = rows.len();
     875            0 :                     Ok((rows.into_iter().skip(offset).take(limit).collect(), total))
     876              :                 }
     877              :                 _ => {
     878           84 :                     let (page, total) = p
     879           84 :                         .docs
     880           84 :                         .list_slice(
     881           84 :                             tenant,
     882           84 :                             doc_kind(kind)?,
     883           84 :                             i64::try_from(offset).unwrap_or(i64::MAX),
     884           84 :                             i64::try_from(limit).unwrap_or(i64::MAX),
     885              :                         )
     886           84 :                         .await
     887           84 :                         .map_err(db)?;
     888           84 :                     Ok((page, usize::try_from(total).unwrap_or(usize::MAX)))
     889              :                 }
     890              :             },
     891              :         }
     892          288 :     }
     893              : 
     894              :     /// 5.12 registration candidates for these entity ids / types. The Pg arm
     895              :     /// reads the `csource_index` rows (an indexed narrowing); `memory`/`file`
     896              :     /// have nothing to push into and return the same snapshot `list` does.
     897              :     /// Either way the result is a SUPERSET — the caller's matcher decides
     898              :     /// every 5.12 condition, this only avoids reading registrations that
     899              :     /// cannot match on id or type. `types` must be expanded plain type IRIs
     900              :     /// (what the registration write stored); a caller holding terms or a 4.17
     901              :     /// selection expression passes `None` and narrows on ids alone.
     902         4855 :     pub async fn matching_registrations(
     903         4855 :         &self,
     904         4855 :         tenant: &TenantId,
     905         4855 :         #[cfg_attr(not(feature = "postgres"), allow(unused_variables))] ids: Option<&[String]>,
     906         4855 :         #[cfg_attr(not(feature = "postgres"), allow(unused_variables))] types: Option<&[String]>,
     907         4855 :     ) -> Result<Vec<Value>, NgsiError> {
     908         4855 :         match self {
     909         4355 :             AnyStore::Mem(s) => Ok(s.list(tenant, Kind::Registration)),
     910              :             #[cfg(feature = "postgres")]
     911          500 :             AnyStore::Pg(p) => p
     912          500 :                 .docs
     913          500 :                 .matching_registrations(tenant, ids, types)
     914          500 :                 .await
     915          500 :                 .map_err(db),
     916              :         }
     917         4855 :     }
     918              : 
     919              :     /// Query Entities with the filter pushed down where the backend
     920              :     /// can take it: SQL for Postgres, the borrowed documents for
     921              :     /// `memory`/`file` (`Store::query_entities`). Either way the caller
     922              :     /// applies the exact filter afterwards unless the outcome says the store
     923              :     /// already did.
     924         3829 :     pub async fn query_entities(
     925         3829 :         &self,
     926         3829 :         tenant: &TenantId,
     927         3829 :         f: &crate::store::filter::EntityFilter<'_>,
     928         3829 :     ) -> Result<crate::store::filter::QueryOutcome, NgsiError> {
     929         3829 :         self.query_paged(tenant, f, false).await
     930         3829 :     }
     931              : 
     932              :     /// `Store::query_candidates`: the Pg arm pages the candidates it cannot
     933              :     /// decide; the memory arm answers as `query_entities`.
     934          372 :     pub async fn query_candidates(
     935          372 :         &self,
     936          372 :         tenant: &TenantId,
     937          372 :         f: &crate::store::filter::EntityFilter<'_>,
     938          372 :     ) -> Result<crate::store::filter::QueryOutcome, NgsiError> {
     939          372 :         self.query_paged(tenant, f, true).await
     940          372 :     }
     941              : 
     942         4201 :     async fn query_paged(
     943         4201 :         &self,
     944         4201 :         tenant: &TenantId,
     945         4201 :         f: &crate::store::filter::EntityFilter<'_>,
     946         4201 :         candidates: bool,
     947         4201 :     ) -> Result<crate::store::filter::QueryOutcome, NgsiError> {
     948         4201 :         let mut outcome = match self {
     949         2989 :             AnyStore::Mem(s) => {
     950         2989 :                 let _ = candidates;
     951         2989 :                 s.query_entities(tenant, f)
     952              :             }
     953              :             #[cfg(feature = "postgres")]
     954         1212 :             AnyStore::Pg(p) => match candidates {
     955           32 :                 true => p.entities.query_candidates(tenant, f).await,
     956         1180 :                 false => p.entities.query(tenant, f).await,
     957              :             }
     958         1212 :             .map_err(db)?,
     959              :         };
     960              :         // 4.22: the Pg arm already excludes expired ENTITIES in SQL (so
     961              :         // paging/totals stay exact); instance stripping — and the whole job
     962              :         // on the memory arm — happens here at the read boundary.
     963         4201 :         let now = now_utc();
     964         4201 :         outcome
     965         4201 :             .rows
     966        39842 :             .retain_mut(|d| !crate::store::filter::strip_expired(d, &now));
     967         4201 :         Ok(outcome)
     968         4201 :     }
     969              : 
     970              :     /// The Entity types of the Tenant's live Entities, sorted, read without
     971              :     /// the documents: the `types` column on Postgres, the held
     972              :     /// documents' `type` on the memory arm.
     973          182 :     pub async fn entity_type_names(&self, tenant: &TenantId) -> Result<Vec<String>, NgsiError> {
     974          182 :         match self {
     975          138 :             AnyStore::Mem(s) => Ok(s.entity_type_names(tenant, &now_utc())),
     976              :             #[cfg(feature = "postgres")]
     977           44 :             AnyStore::Pg(p) => p.entities.type_names(tenant).await.map_err(db),
     978              :         }
     979          182 :     }
     980              : 
     981              :     /// The (type, member name) pairs of the Tenant's attribute catalogue,
     982              :     /// sorted: table `entity_attrs` on Postgres, which may still
     983              :     /// list a pair no live Entity carries; the live documents in memory.
     984          152 :     pub async fn entity_attr_pairs(
     985          152 :         &self,
     986          152 :         tenant: &TenantId,
     987          152 :     ) -> Result<Vec<(String, String)>, NgsiError> {
     988          152 :         match self {
     989           96 :             AnyStore::Mem(s) => Ok(s.entity_attr_pairs(tenant, &now_utc())),
     990              :             #[cfg(feature = "postgres")]
     991           56 :             AnyStore::Pg(p) => p.entities.attr_pairs(tenant).await.map_err(db),
     992              :         }
     993          152 :     }
     994              : 
     995              :     /// The types of the Tenant's live Entities that carry one of `any_of`,
     996              :     /// sorted: the types index on Postgres, one pass in memory.
     997           88 :     pub async fn entity_types_beside(
     998           88 :         &self,
     999           88 :         tenant: &TenantId,
    1000           88 :         any_of: &[String],
    1001           88 :     ) -> Result<Vec<String>, NgsiError> {
    1002           88 :         match self {
    1003           16 :             AnyStore::Mem(s) => Ok(s.entity_types_beside(tenant, any_of, &now_utc())),
    1004              :             #[cfg(feature = "postgres")]
    1005           72 :             AnyStore::Pg(p) => p.entities.types_beside(tenant, any_of).await.map_err(db),
    1006              :         }
    1007           88 :     }
    1008              : 
    1009              :     /// The id segments the Pg arm reads by its C-collated id index;
    1010              :     /// the memory arm keeps none and its probes narrow by literal.
    1011           16 :     pub async fn entity_id_segments(
    1012           16 :         &self,
    1013           16 :         tenant: &TenantId,
    1014           16 :         lead: &str,
    1015           16 :         sep: char,
    1016           16 :         max: usize,
    1017           16 :     ) -> Result<Option<Vec<String>>, NgsiError> {
    1018           16 :         match self {
    1019              :             AnyStore::Mem(_) => {
    1020            0 :                 let _ = (tenant, lead, sep, max);
    1021            0 :                 Ok(None)
    1022              :             }
    1023              :             #[cfg(feature = "postgres")]
    1024           16 :             AnyStore::Pg(p) => p
    1025           16 :                 .entities
    1026           16 :                 .id_segments(tenant, lead, sep, max)
    1027           16 :                 .await
    1028           16 :                 .map_err(db),
    1029              :         }
    1030           16 :     }
    1031              : 
    1032              :     /// Query Temporal Evolution with entity narrowing, instance-window
    1033              :     /// pruning AND entity paging pushed down. Same
    1034              :     /// contract: the API's window() is the arbiter; the memory arm returns
    1035              :     /// the full snapshot, never paged.
    1036         1092 :     pub async fn query_temporal(
    1037         1092 :         &self,
    1038         1092 :         tenant: &TenantId,
    1039         1092 :         #[cfg_attr(not(feature = "postgres"), allow(unused_variables))]
    1040         1092 :         f: &crate::store::filter::TemporalFilter<'_>,
    1041         1092 :     ) -> Result<crate::store::filter::TemporalOutcome, NgsiError> {
    1042         1092 :         let mut outcome = match self {
    1043          690 :             AnyStore::Mem(s) => crate::store::filter::TemporalOutcome {
    1044          690 :                 rows: s.list(tenant, Kind::Temporal),
    1045          690 :                 paged: false,
    1046          690 :                 total: None,
    1047          690 :                 aggregated: false,
    1048          690 :             },
    1049              :             #[cfg(feature = "postgres")]
    1050          402 :             AnyStore::Pg(p) => p.temporal.query(tenant, f).await.map_err(db)?,
    1051              :         };
    1052              :         // 4.22 on temporal reads: the Pg arm already dropped expired ENTITIES in
    1053              :         // SQL (paging exact); this strips expired attribute INSTANCES, and does
    1054              :         // the whole job on the memory arm (no SQL to push into).
    1055         1092 :         let now = now_utc();
    1056         1092 :         outcome
    1057         1092 :             .rows
    1058         2440 :             .retain_mut(|d| !crate::store::filter::strip_expired(d, &now));
    1059         1092 :         Ok(outcome)
    1060         1092 :     }
    1061              : 
    1062              :     /// Auto-recording fast path: append instances to an
    1063              :     /// entity's temporal evolution, creating the meta shell on first touch.
    1064              :     /// Pg: pure multi-row INSERT — no history read, no doc rewrite. Memory:
    1065              :     /// the create-or-extend the mirror always did, under the store lock.
    1066              :     /// `shell` carries the meta members; `additions` maps attr IRI →
    1067              :     /// instance array (instanceIds already stamped by the caller).
    1068              :     ///
    1069              :     /// Both arms record only for an entity that still exists: 5.6.6 deletes
    1070              :     /// the entity and then the temporal evolution recorded for it, so an
    1071              :     /// append overlapping the delete must not recreate history nothing will
    1072              :     /// ever clean again. An instance marked `temporal_only` never holds the
    1073              :     /// entities (they live in another backend) and records unconditionally;
    1074              :     /// there the delete overlap stays a window the temporal delete closes.
    1075       121920 :     pub async fn temporal_append(
    1076       121920 :         &self,
    1077       121920 :         tenant: &TenantId,
    1078       121920 :         id: &str,
    1079       121920 :         shell: &Value,
    1080       121920 :         additions: &Value,
    1081       121920 :     ) -> Result<(), NgsiError> {
    1082       121920 :         match self {
    1083       121874 :             AnyStore::Mem(s) => {
    1084       121874 :                 if !s.temporal_only && s.get(tenant, Kind::Entity, id).is_none() {
    1085            6 :                     return Ok(());
    1086       121868 :                 }
    1087       121868 :                 if s.get(tenant, Kind::Temporal, id).is_none() {
    1088              :                     // the cap the extension below applies, judged before the
    1089              :                     // shell is stored: refused there, the empty shell stayed
    1090       119844 :                     past_instance_cap(&Default::default(), additions, s.max_instances)?;
    1091              :                     // loser of a concurrent create race just extends below
    1092       119842 :                     let _ = s.create(tenant, Kind::Temporal, id, shell.clone()).await;
    1093         2024 :                 }
    1094       121866 :                 let cap = s.max_instances;
    1095       121866 :                 let mut refused = None;
    1096       121866 :                 s.mutate(tenant, Kind::Temporal, id, |doc| {
    1097       121866 :                     let was = if cap > 0 {
    1098         1742 :                         instance_counts(doc)
    1099              :                     } else {
    1100       120124 :                         Default::default()
    1101              :                     };
    1102       121866 :                     let target = doc.as_object_mut().ok_or(())?;
    1103              :                     // 4.8: the Entity's modifiedAt is its last write, an
    1104              :                     // Attribute's update included; never moved backwards
    1105       121866 :                     if let Some(m) = shell.get("modifiedAt").and_then(Value::as_str) {
    1106         9720 :                         let stale = target
    1107         9720 :                             .get("modifiedAt")
    1108         9720 :                             .and_then(Value::as_str)
    1109         9720 :                             .is_none_or(|cur| super::mem::earlier(cur, m));
    1110         9720 :                         if stale {
    1111         1761 :                             target.insert("modifiedAt".into(), Value::String(m.to_owned()));
    1112         7959 :                         }
    1113       112146 :                     }
    1114       121866 :                     if let Some(adds) = additions.as_object() {
    1115       126632 :                         for (k, v) in adds {
    1116       126632 :                             let mut incoming: Vec<Value> =
    1117       126632 :                                 v.as_array().cloned().unwrap_or_default();
    1118              :                             // The memory store keeps no provenance and no
    1119              :                             // versions; Postgres does.
    1120       127002 :                             incoming.iter_mut().for_each(|i| {
    1121       127002 :                                 antares_store::take_provenance(i);
    1122       127002 :                             });
    1123       126632 :                             match target.get_mut(k).and_then(Value::as_array_mut) {
    1124              :                                 // same instanceId = the same instance corrected
    1125              :                                 // (the pg arm's ON CONFLICT DO UPDATE)
    1126         1954 :                                 Some(cur) => {
    1127         1962 :                                     for inst in incoming {
    1128         1962 :                                         let iid = inst.get("instanceId");
    1129       143430 :                                         match cur.iter_mut().find(|c| c.get("instanceId") == iid) {
    1130           36 :                                             Some(slot) => *slot = inst,
    1131         1926 :                                             None => cur.push(inst),
    1132              :                                         }
    1133              :                                     }
    1134              :                                 }
    1135       124678 :                                 None => {
    1136       124678 :                                     target.insert(k.clone(), Value::Array(incoming));
    1137       124678 :                                 }
    1138              :                             }
    1139              :                         }
    1140            0 :                     }
    1141       121866 :                     past_instance_cap(&was, doc, cap).map_err(|e| {
    1142            4 :                         refused = Some(e);
    1143            4 :                     })
    1144       121866 :                 })
    1145       121866 :                 .await;
    1146       121866 :                 refused.map_or(Ok(()), Err)
    1147              :             }
    1148              :             #[cfg(feature = "postgres")]
    1149           46 :             AnyStore::Pg(p) => p
    1150           46 :                 .temporal
    1151           46 :                 .append(tenant, id, shell, additions)
    1152           46 :                 .await
    1153           46 :                 .map_err(db),
    1154              :         }
    1155       121920 :     }
    1156              : 
    1157              :     /// Mark this instance as the temporal half of a mixed deployment: the
    1158              :     /// entities live in another backend, so appends never look for them here.
    1159           10 :     pub fn temporal_only(mut self) -> Self {
    1160           10 :         match &mut self {
    1161           10 :             AnyStore::Mem(s) => s.temporal_only = true,
    1162              :             #[cfg(feature = "postgres")]
    1163            0 :             AnyStore::Pg(p) => p.temporal.temporal_only = true,
    1164              :         }
    1165           10 :         self
    1166           10 :     }
    1167              : 
    1168              :     /// Cap the instances one Attribute's history may hold in the memory/file
    1169              :     /// store (`0` = none); the Postgres arm keeps history in rows and has no
    1170              :     /// such document to bound.
    1171              :     #[cfg_attr(not(feature = "postgres"), allow(irrefutable_let_patterns))]
    1172           31 :     pub fn max_instances(mut self, cap: usize) -> Self {
    1173           31 :         if let AnyStore::Mem(s) = &mut self {
    1174           31 :             s.max_instances = cap;
    1175           31 :         }
    1176           31 :         self
    1177           31 :     }
    1178              : 
    1179              :     /// Retrieve Temporal Evolution with the same instance pruning.
    1180         1036 :     pub async fn get_temporal(
    1181         1036 :         &self,
    1182         1036 :         tenant: &TenantId,
    1183         1036 :         id: &str,
    1184         1036 :         #[cfg_attr(not(feature = "postgres"), allow(unused_variables))]
    1185         1036 :         f: &crate::store::filter::TemporalFilter<'_>,
    1186         1036 :     ) -> Result<Option<Value>, NgsiError> {
    1187         1036 :         let mut doc = match self {
    1188          768 :             AnyStore::Mem(s) => s.get(tenant, Kind::Temporal, id),
    1189              :             #[cfg(feature = "postgres")]
    1190          268 :             AnyStore::Pg(p) => p.temporal.get_range(tenant, id, f).await.map_err(db)?,
    1191              :         };
    1192              :         // 4.22: expired entity → None (Pg already did this in SQL); otherwise
    1193              :         // strip expired instances.
    1194         1036 :         if let Some(d) = &mut doc {
    1195          972 :             let now = now_utc();
    1196          972 :             if crate::store::filter::strip_expired(d, &now) {
    1197            0 :                 return Ok(None);
    1198          972 :             }
    1199           64 :         }
    1200         1036 :         Ok(doc)
    1201         1036 :     }
    1202              : 
    1203        25826 :     pub async fn mutate<T, E>(
    1204        25826 :         &self,
    1205        25826 :         tenant: &TenantId,
    1206        25826 :         kind: Kind,
    1207        25826 :         id: &str,
    1208        25826 :         f: impl FnOnce(&mut Value) -> Result<T, E>,
    1209        25826 :     ) -> Result<Option<Result<T, E>>, NgsiError> {
    1210        24181 :         match self {
    1211        24181 :             AnyStore::Mem(s) if kind == Kind::Temporal && s.max_instances > 0 => {
    1212              :                 // The cap's refusal is no error of the closure: it aborts the
    1213              :                 // write the same way and comes back as the store's own.
    1214              :                 enum Refused<E> {
    1215              :                     Closure(E),
    1216              :                     Cap(NgsiError),
    1217              :                 }
    1218          298 :                 let cap = s.max_instances;
    1219          298 :                 let r = s
    1220          298 :                     .mutate(tenant, kind, id, |doc| {
    1221          274 :                         let was = instance_counts(doc);
    1222          274 :                         let t = f(doc).map_err(Refused::Closure)?;
    1223          272 :                         past_instance_cap(&was, doc, cap).map_err(Refused::Cap)?;
    1224          268 :                         Ok(t)
    1225          274 :                     })
    1226          298 :                     .await;
    1227            6 :                 match r {
    1228           24 :                     None => Ok(None),
    1229          268 :                     Some(Ok(t)) => Ok(Some(Ok(t))),
    1230            2 :                     Some(Err(Refused::Closure(e))) => Ok(Some(Err(e))),
    1231            4 :                     Some(Err(Refused::Cap(e))) => Err(e),
    1232              :                 }
    1233              :             }
    1234        23883 :             AnyStore::Mem(s) => Ok(s.mutate(tenant, kind, id, f).await),
    1235              :             #[cfg(feature = "postgres")]
    1236         1645 :             AnyStore::Pg(p) => match kind {
    1237              :                 Kind::Entity => {
    1238              :                     // before/after captured for the change hook's
    1239              :                     // prev_payload INSIDE the row lock — a before-image read
    1240              :                     // in its own transaction can belong to a different version
    1241              :                     // than the one the lock serialized on.
    1242          454 :                     let mut before: Option<Value> = None;
    1243          454 :                     let mut after: Option<Value> = None;
    1244          454 :                     let r = p
    1245          454 :                         .entities
    1246          454 :                         .mutate_stamped(tenant, id, |d| {
    1247          436 :                             before = Some(d.clone());
    1248          436 :                             let r = f(d);
    1249          436 :                             if r.is_ok() {
    1250          418 :                                 after = Some(d.clone());
    1251          418 :                             }
    1252          436 :                             r
    1253          436 :                         })
    1254          454 :                         .await
    1255          454 :                         .map_err(db)?;
    1256          454 :                     let (r, version) = match r {
    1257          418 :                         Some(Ok((t, v))) => (Some(Ok(t)), Some(v)),
    1258           18 :                         Some(Err(e)) => (Some(Err(e)), None),
    1259           18 :                         None => (None, None),
    1260              :                     };
    1261          454 :                     if let (Some(v), Some(a)) = (version, after) {
    1262          418 :                         if before.as_ref() != Some(&a) {
    1263          402 :                             p.emit(tenant, before, Some(a), v).await;
    1264           16 :                         }
    1265           36 :                     }
    1266          454 :                     Ok(r)
    1267              :                 }
    1268          302 :                 Kind::Temporal => p.temporal.mutate(tenant, id, f).await.map_err(db),
    1269              :                 _ => {
    1270              :                     // FOR UPDATE + UPDATE in one tx: a bookkeeping writeback
    1271              :                     // racing a DELETE must never resurrect the row (047_06).
    1272          889 :                     let dk = doc_kind(kind)?;
    1273          889 :                     match p.docs.mutate(tenant, dk, id, f).await.map_err(db)? {
    1274          519 :                         Some(r) => Ok(Some(r)),
    1275          370 :                         None => Ok(None),
    1276              :                     }
    1277              :                 }
    1278              :             },
    1279              :         }
    1280        25826 :     }
    1281              : 
    1282              :     /// Batch upsert with REPLACE semantics for entities:
    1283              :     /// one statement + one transaction on the Pg arm, per-item loop on the
    1284              :     /// memory arm. Created-flags in input order.
    1285          180 :     pub async fn batch_upsert(
    1286          180 :         &self,
    1287          180 :         tenant: &TenantId,
    1288          180 :         items: Vec<(String, Value)>,
    1289          180 :     ) -> Result<Vec<bool>, NgsiError> {
    1290              :         // Each id once per call, on every arm: Postgres refuses an ON CONFLICT
    1291              :         // row affected twice and the memory arm would keep the last, so a
    1292              :         // repeat is the caller's bug (the API splits repeats into rounds).
    1293          180 :         let mut seen = std::collections::HashSet::with_capacity(items.len());
    1294          280 :         if let Some((id, _)) = items.iter().find(|(id, _)| !seen.insert(id.as_str())) {
    1295            6 :             return Err(NgsiError::InternalError(format!(
    1296            6 :                 "batch_upsert names {id} twice"
    1297            6 :             )));
    1298          174 :         }
    1299          174 :         match self {
    1300          116 :             AnyStore::Mem(s) => {
    1301          116 :                 let mut out = Vec::with_capacity(items.len());
    1302          170 :                 for (id, doc) in items {
    1303          170 :                     out.push(!s.upsert(tenant, Kind::Entity, &id, doc).await);
    1304              :                 }
    1305          116 :                 Ok(out)
    1306              :             }
    1307              :             #[cfg(feature = "postgres")]
    1308           58 :             AnyStore::Pg(p) => {
    1309           58 :                 let out = p
    1310           58 :                     .entities
    1311           58 :                     .batch_upsert_replace_stamped(tenant, &items)
    1312           58 :                     .await
    1313           58 :                     .map_err(db)?;
    1314           98 :                 for ((_, doc), (_, prev, v)) in items.iter().zip(&out) {
    1315           98 :                     p.emit(
    1316           98 :                         tenant,
    1317           98 :                         prev.clone(),
    1318           98 :                         Some(doc.clone()),
    1319           98 :                         v.clone().unwrap_or_default(),
    1320           98 :                     )
    1321           98 :                     .await;
    1322              :                 }
    1323           58 :                 Ok(out.into_iter().map(|(created, _, _)| created).collect())
    1324              :             }
    1325              :         }
    1326          180 :     }
    1327              : 
    1328              :     /// Batch read-modify-write for entities: one transaction + one ordered
    1329              :     /// lock set + one multi-row writeback on the Pg arm; per-item mutate on
    1330              :     /// the memory arm. Results align with `ids` (`None` = absent).
    1331          236 :     pub async fn batch_mutate<E>(
    1332          236 :         &self,
    1333          236 :         tenant: &TenantId,
    1334          236 :         ids: &[String],
    1335          236 :         mut f: impl FnMut(&str, &mut Value) -> Result<(), E>,
    1336          236 :     ) -> Result<Vec<Option<Result<(), E>>>, NgsiError> {
    1337          236 :         match self {
    1338          170 :             AnyStore::Mem(s) => {
    1339          170 :                 let mut out = Vec::with_capacity(ids.len());
    1340         8258 :                 for id in ids {
    1341         8258 :                     out.push(s.mutate(tenant, Kind::Entity, id, |d| f(id, d)).await);
    1342              :                 }
    1343          170 :                 Ok(out)
    1344              :             }
    1345              :             #[cfg(feature = "postgres")]
    1346           66 :             AnyStore::Pg(p) => {
    1347              :                 // hook images captured inside the lock, same as single mutate
    1348           66 :                 let mut images: Vec<(String, Value, Value)> = Vec::new();
    1349           66 :                 let (r, versions) = p
    1350           66 :                     .entities
    1351          112 :                     .batch_mutate_stamped(tenant, ids, |id, d| {
    1352          112 :                         let before = d.clone();
    1353          112 :                         let r = f(id, d);
    1354          112 :                         if r.is_ok() && *d != before {
    1355          110 :                             images.push((id.to_owned(), before, d.clone()));
    1356          110 :                         }
    1357          112 :                         r
    1358          112 :                     })
    1359           66 :                     .await
    1360           66 :                     .map_err(db)?;
    1361          110 :                 for (id, before, after) in images {
    1362          110 :                     let v = versions.get(&id).cloned().unwrap_or_default();
    1363          110 :                     p.emit(tenant, Some(before), Some(after), v).await;
    1364              :                 }
    1365           66 :                 Ok(r)
    1366              :             }
    1367              :         }
    1368          236 :     }
    1369              : 
    1370              :     /// 4.22 GC for the memory/file arm (the Pg arm's sweep lives in the
    1371              :     /// maintenance job, mode-switched in the broker). Returns reaped count.
    1372         1958 :     pub fn sweep_expired(&self) -> usize {
    1373         1958 :         match self {
    1374          962 :             AnyStore::Mem(s) => s.sweep_expired(&now_utc()),
    1375              :             // 4.22 reaping on the Pg arm is `pg::maintenance`'s job
    1376              :             // (`reap_expired_entities` / `reap_expired_instances`), which
    1377              :             // deletes in one indexed statement instead of walking the store;
    1378              :             // reads refuse an expired document either way.
    1379              :             #[cfg(feature = "postgres")]
    1380          996 :             AnyStore::Pg(_) => 0,
    1381              :         }
    1382         1958 :     }
    1383              : 
    1384              :     /// 5.5.10: does the Tenant exist? The default Tenant "implicitly exists";
    1385              :     /// others exist once implicitly created by a create operation.
    1386              :     ///
    1387              :     /// It answers for CLIENT tenants. A tenant the broker minted for itself
    1388              :     /// does hold a row — every write claims one, and that row is what keeps
    1389              :     /// the tenant enumerable to `subscription_tenants` across a restart — but
    1390              :     /// nothing asks this about one: the wall that asks sits outside the 6.3.22
    1391              :     /// snapshot scoping and reads the header the caller sent, which cannot
    1392              :     /// name an internal tenant. What a Snapshot's synthetic tenant holds is
    1393              :     /// asserted by the Snapshot document; the row only accounts for it, and
    1394              :     /// `tenant_ids` keeps it out of the customer-account listing.
    1395          348 :     pub async fn tenant_exists(&self, tenant: &TenantId) -> Result<bool, NgsiError> {
    1396          348 :         if tenant.as_str() == TenantId::DEFAULT {
    1397           14 :             return Ok(true);
    1398          334 :         }
    1399          334 :         match self {
    1400          252 :             AnyStore::Mem(s) => Ok(s.tenant_exists(tenant)),
    1401              :             #[cfg(feature = "postgres")]
    1402           82 :             AnyStore::Pg(p) => {
    1403           82 :                 async {
    1404           82 :                     let row =
    1405           82 :                         sqlx::query_scalar::<_, i32>("SELECT 1 FROM tenants WHERE tenant_id = $1")
    1406           82 :                             .bind(tenant.as_str())
    1407           82 :                             .fetch_optional(p.docs.pool())
    1408           82 :                             .await
    1409           82 :                             .map_err(db)?;
    1410           82 :                     Ok(row.is_some())
    1411           82 :                 }
    1412           82 :                 .await
    1413              :             }
    1414              :         }
    1415          348 :     }
    1416              : 
    1417              :     /// Every tenant name the backend knows, sorted. One cheap query: at the
    1418              :     /// 10 000-tenant target (ADR-0001) an inventory that counted every kind
    1419              :     /// of every tenant would be 7 counts per tenant on one transaction, so
    1420              :     /// the counts live in `tenant_stats_one` and are paid per lookup.
    1421         2004 :     pub async fn tenant_ids(&self) -> Result<Vec<String>, NgsiError> {
    1422         2004 :         match self {
    1423         1004 :             AnyStore::Mem(s) => Ok(s.tenant_ids()),
    1424              :             #[cfg(feature = "postgres")]
    1425         1000 :             AnyStore::Pg(p) => {
    1426         1000 :                 async {
    1427         1000 :                     let mut rows: Vec<String> =
    1428         1000 :                         sqlx::query_scalar("SELECT tenant_id FROM tenants ORDER BY 1")
    1429         1000 :                             .fetch_all(p.docs.pool())
    1430         1000 :                             .await
    1431         1000 :                             .map_err(db)?;
    1432              :                     // The broker's own tenants claim a row like any other
    1433              :                     // write, because that row is also the enumeration the
    1434              :                     // notification paths walk. The inventory is the list of
    1435              :                     // customer accounts, so they are filtered on the way out.
    1436       140284 :                     rows.retain(|t| !TenantId::is_reserved_str(t));
    1437              :                     // 5.5.10: the default Tenant implicitly exists, whether or
    1438              :                     // not a row was ever written for it.
    1439        12992 :                     if !rows.iter().any(|t| t == TenantId::DEFAULT) {
    1440            0 :                         rows.push(TenantId::DEFAULT.to_owned());
    1441            0 :                         rows.sort();
    1442         1000 :                     }
    1443         1000 :                     Ok(rows)
    1444         1000 :                 }
    1445         1000 :                 .await
    1446              :             }
    1447              :         }
    1448         2004 :     }
    1449              : 
    1450              :     /// What one tenant holds; `None` when it does not exist (5.5.10 keeps
    1451              :     /// the default Tenant existing even when empty).
    1452           44 :     pub async fn tenant_stats_one(
    1453           44 :         &self,
    1454           44 :         tenant: &TenantId,
    1455           44 :     ) -> Result<Option<antares_store::TenantStats>, NgsiError> {
    1456           44 :         if !self.tenant_exists(tenant).await? {
    1457           10 :             return Ok(None);
    1458           34 :         }
    1459           34 :         match self {
    1460           32 :             AnyStore::Mem(s) => Ok(Some(s.tenant_stats_one(tenant))),
    1461              :             #[cfg(feature = "postgres")]
    1462            2 :             AnyStore::Pg(p) => {
    1463            2 :                 async {
    1464            2 :                     let mut tx = tenant_tx(p, tenant).await?;
    1465              :                     // the tenant setting is what the RLS-guarded counts see
    1466            2 :                     let created_at: Option<String> = sqlx::query_scalar(
    1467            2 :                         "SELECT created_at::text FROM tenants WHERE tenant_id = $1",
    1468            2 :                     )
    1469            2 :                     .bind(tenant.as_str())
    1470            2 :                     .fetch_optional(&mut *tx)
    1471            2 :                     .await
    1472            2 :                     .map_err(db)?;
    1473            2 :                     let c: (i64, i64, i64, i64, i64, i64, i64) = sqlx::query_as(
    1474            2 :                         "SELECT (SELECT count(*) FROM entities WHERE tenant_id = $1),
    1475            2 :                             (SELECT count(*) FROM subscriptions WHERE tenant_id = $1),
    1476            2 :                             (SELECT count(*) FROM csource_registrations WHERE tenant_id = $1),
    1477            2 :                             (SELECT count(*) FROM csource_subscriptions WHERE tenant_id = $1),
    1478            2 :                             (SELECT count(*) FROM snapshots WHERE tenant_id = $1),
    1479            2 :                             (SELECT count(*) FROM entity_map_docs WHERE tenant_id = $1),
    1480            2 :                             (SELECT count(*) FROM dist_subs WHERE tenant_id = $1)",
    1481            2 :                     )
    1482            2 :                     .bind(tenant.as_str())
    1483            2 :                     .fetch_one(&mut *tx)
    1484            2 :                     .await
    1485            2 :                     .map_err(db)?;
    1486            2 :                     tx.commit().await.map_err(db)?;
    1487            2 :                     Ok(Some(antares_store::TenantStats {
    1488            2 :                         tenant: tenant.as_str().to_owned(),
    1489            2 :                         created_at,
    1490            2 :                         entities: c.0 as u64,
    1491            2 :                         subscriptions: c.1 as u64,
    1492            2 :                         registrations: c.2 as u64,
    1493            2 :                         csource_subscriptions: c.3 as u64,
    1494            2 :                         snapshots: c.4 as u64,
    1495            2 :                         entity_maps: c.5 as u64,
    1496            2 :                         dist_subs: c.6 as u64,
    1497            2 :                     }))
    1498            2 :                 }
    1499            2 :                 .await
    1500              :             }
    1501              :         }
    1502           44 :     }
    1503              : 
    1504              :     /// Purge the current-state half of one tenant in one transaction;
    1505              :     /// `false` when the tenant did not exist. The default tenant's row stays.
    1506          116 :     pub async fn purge_tenant(&self, tenant: &TenantId) -> Result<bool, NgsiError> {
    1507          116 :         match self {
    1508           86 :             AnyStore::Mem(s) => Ok(s.purge_tenant(tenant)),
    1509              :             #[cfg(feature = "postgres")]
    1510           30 :             AnyStore::Pg(p) => {
    1511           30 :                 async {
    1512           30 :                     let mut tx = tenant_tx(p, tenant).await?;
    1513           30 :                     let known = sqlx::query_scalar::<_, i32>(
    1514           30 :                         "SELECT 1 FROM tenants WHERE tenant_id = $1 FOR UPDATE",
    1515           30 :                     )
    1516           30 :                     .bind(tenant.as_str())
    1517           30 :                     .fetch_optional(&mut *tx)
    1518           30 :                     .await
    1519           30 :                     .map_err(db)?
    1520           30 :                     .is_some();
    1521           30 :                     if !known {
    1522            2 :                         return Ok(false);
    1523           28 :                     }
    1524          420 :                     for table in [
    1525           28 :                         "entities",
    1526           28 :                         // emptied by the delete trigger of `entities` (0024)
    1527           28 :                         "entity_values",
    1528           28 :                         "subscriptions",
    1529           28 :                         "csource_subscriptions",
    1530           28 :                         "csource_registrations",
    1531           28 :                         "csource_index",
    1532           28 :                         "outbox",
    1533           28 :                         "snapshots",
    1534           28 :                         "entity_map_docs",
    1535           28 :                         "dist_subs",
    1536           28 :                         "dead_letters",
    1537           28 :                         "views",
    1538           28 :                         "entity_types",
    1539           28 :                         "entity_attrs",
    1540           28 :                         // ADR-0021: `tenant_id` is GENERATED from the row's kind
    1541           28 :                         // and owner, so a Cached row's is NULL and this DELETE
    1542           28 :                         // takes only what the Tenant stored itself.
    1543           28 :                         "jsonld_contexts",
    1544           28 :                     ] {
    1545          420 :                         sqlx::query(sqlx::AssertSqlSafe(format!(
    1546          420 :                             "DELETE FROM {table} WHERE tenant_id = $1"
    1547          420 :                         )))
    1548          420 :                         .bind(tenant.as_str())
    1549          420 :                         .execute(&mut *tx)
    1550          420 :                         .await
    1551          420 :                         .map_err(db)?;
    1552              :                     }
    1553           28 :                     if tenant.as_str() != TenantId::DEFAULT {
    1554           28 :                         sqlx::query("DELETE FROM tenants WHERE tenant_id = $1")
    1555           28 :                             .bind(tenant.as_str())
    1556           28 :                             .execute(&mut *tx)
    1557           28 :                             .await
    1558           28 :                             .map_err(db)?;
    1559            0 :                     }
    1560           28 :                     tx.commit().await.map_err(db)?;
    1561           28 :                     Ok(true)
    1562           30 :                 }
    1563           30 :                 .await
    1564              :             }
    1565              :         }
    1566          116 :     }
    1567              : 
    1568         3155 :     pub async fn subscription_tenants(&self) -> Result<Vec<String>, NgsiError> {
    1569         3155 :         match self {
    1570         2958 :             AnyStore::Mem(s) => Ok(s.subscription_tenants()),
    1571              :             // Pg answers the superset the contract allows: every known
    1572              :             // tenant. Narrowing it would need a cross-tenant read of
    1573              :             // `subscriptions`, and that table is deliberately outside the
    1574              :             // `antares.service` escape — a subscription document carries
    1575              :             // endpoint.receiverInfo, i.e. the credentials the notification
    1576              :             // is sent with. The callers list per tenant under set_tenant
    1577              :             // afterwards, so the extra names cost one empty list each.
    1578              :             #[cfg(feature = "postgres")]
    1579          197 :             AnyStore::Pg(p) => {
    1580          197 :                 async {
    1581          197 :                     let rows =
    1582          197 :                         sqlx::query_scalar::<_, String>("SELECT tenant_id FROM tenants ORDER BY 1")
    1583          197 :                             .fetch_all(p.docs.pool())
    1584          197 :                             .await
    1585          197 :                             .map_err(db)?;
    1586          197 :                     Ok(rows)
    1587          197 :                 }
    1588          197 :                 .await
    1589              :             }
    1590              :         }
    1591         3155 :     }
    1592              : 
    1593          688 :     pub async fn context_put(
    1594          688 :         &self,
    1595          688 :         tenant: Option<&TenantId>,
    1596          688 :         id: &str,
    1597          688 :         doc: Value,
    1598          688 :     ) -> Result<(), NgsiError> {
    1599              :         // ADR-0021, the write half no `WHERE` on an existing row can state:
    1600              :         // the row that ARRIVES must belong to the caller too, or a Tenant
    1601              :         // could store term mappings under another Tenant's name and decide
    1602              :         // what that Tenant's payloads mean (5.5.7). Postgres says the same
    1603              :         // thing as the policy's WITH CHECK; this holds under the roles that
    1604              :         // bypass RLS.
    1605          688 :         if !antares_store::context_row_visible(&doc, tenant) {
    1606           12 :             return Err(NgsiError::InternalError(
    1607           12 :                 "@context belongs to another tenant".into(),
    1608           12 :             ));
    1609          676 :         }
    1610              :         // 5.13.1 names three kinds and the row's kind decides who reaches it
    1611              :         // (ADR-0021): a row without one would be Cached to Postgres and
    1612              :         // nothing at all to the memory arm.
    1613          676 :         if doc.get("kind").and_then(Value::as_str).is_none() {
    1614            0 :             return Err(NgsiError::InternalError(
    1615            0 :                 "@context row without a kind".into(),
    1616            0 :             ));
    1617          676 :         }
    1618          676 :         match self {
    1619          478 :             AnyStore::Mem(s) => s.context_put(tenant, id, doc),
    1620              :             #[cfg(feature = "postgres")]
    1621          198 :             AnyStore::Pg(p) => {
    1622          198 :                 let kind = doc
    1623          198 :                     .get("kind")
    1624          198 :                     .and_then(Value::as_str)
    1625          198 :                     .unwrap_or("Cached")
    1626          198 :                     .to_owned();
    1627          198 :                 p.docs
    1628          198 :                     .context_put(tenant, id, &doc, &kind)
    1629          198 :                     .await
    1630          198 :                     .map_err(db)
    1631              :             }
    1632              :         }
    1633          688 :     }
    1634              : 
    1635         1350 :     pub async fn context_get(
    1636         1350 :         &self,
    1637         1350 :         tenant: Option<&TenantId>,
    1638         1350 :         id: &str,
    1639         1350 :     ) -> Result<Option<Value>, NgsiError> {
    1640         1350 :         match self {
    1641          902 :             AnyStore::Mem(s) => Ok(s.context_get(tenant, id)),
    1642              :             #[cfg(feature = "postgres")]
    1643          448 :             AnyStore::Pg(p) => p.docs.context_get(tenant, id).await.map_err(db),
    1644              :         }
    1645         1350 :     }
    1646              : 
    1647        21058 :     pub async fn context_mutate(
    1648        21058 :         &self,
    1649        21058 :         tenant: Option<&TenantId>,
    1650        21058 :         id: &str,
    1651        21058 :         f: antares_store::MutateFn<'_>,
    1652        21058 :     ) -> Result<Option<Result<(), ()>>, NgsiError> {
    1653              :         // the row a closure leaves must still be one this caller may store,
    1654              :         // and keep its kind: the same two checks context_put makes
    1655        21058 :         let guarded = move |doc: &mut Value| {
    1656        20716 :             let kind = doc.get("kind").cloned();
    1657        20716 :             f(doc)?;
    1658        20716 :             if doc.get("kind") != kind.as_ref() || !antares_store::context_row_visible(doc, tenant)
    1659              :             {
    1660            0 :                 return Err(());
    1661        20716 :             }
    1662        20716 :             Ok(())
    1663        20716 :         };
    1664        21058 :         match self {
    1665        10730 :             AnyStore::Mem(s) => Ok(s.context_mutate(tenant, id, guarded)),
    1666              :             #[cfg(feature = "postgres")]
    1667        10328 :             AnyStore::Pg(p) => p.docs.context_mutate(tenant, id, guarded).await.map_err(db),
    1668              :         }
    1669        21058 :     }
    1670              : 
    1671          412 :     pub async fn context_delete(
    1672          412 :         &self,
    1673          412 :         tenant: Option<&TenantId>,
    1674          412 :         id: &str,
    1675          412 :     ) -> Result<bool, NgsiError> {
    1676          412 :         match self {
    1677          226 :             AnyStore::Mem(s) => Ok(s.context_delete(tenant, id)),
    1678              :             #[cfg(feature = "postgres")]
    1679          186 :             AnyStore::Pg(p) => p.docs.context_delete(tenant, id).await.map_err(db),
    1680              :         }
    1681          412 :     }
    1682              : 
    1683          282 :     pub async fn context_list_meta(
    1684          282 :         &self,
    1685          282 :         tenant: Option<&TenantId>,
    1686          282 :     ) -> Result<Vec<Value>, NgsiError> {
    1687          282 :         match self {
    1688          198 :             AnyStore::Mem(s) => Ok(s.context_list_meta(tenant)),
    1689              :             #[cfg(feature = "postgres")]
    1690           84 :             AnyStore::Pg(p) => p.docs.context_list_meta(tenant).await.map_err(db),
    1691              :         }
    1692          282 :     }
    1693              : }
    1694              : 
    1695              : #[cfg(all(test, feature = "postgres"))]
    1696              : mod db_error_tests {
    1697              :     #[allow(unused_imports)]
    1698              :     use antares_model::NgsiError;
    1699              : 
    1700              :     /// An acquire timeout is overload, not a fault, and the HTTP binding
    1701              :     /// recognises it by this exact detail. If the two ends ever spell it
    1702              :     /// differently the broker answers 500 to a condition a client could
    1703              :     /// have retried, so the constant is asserted rather than the words.
    1704              :     #[test]
    1705            2 :     fn a_pool_timeout_is_marked_as_overload() {
    1706            2 :         let pd = super::db(sqlx::Error::PoolTimedOut).to_problem_details();
    1707            2 :         assert_eq!(pd.detail, antares_model::error::DB_OVERLOADED);
    1708            2 :         assert_eq!(pd.title, "InternalError");
    1709              :         // and no other sqlx error borrows the mark
    1710            2 :         assert_ne!(
    1711            2 :             super::db(sqlx::Error::RowNotFound)
    1712            2 :                 .to_problem_details()
    1713              :                 .detail,
    1714              :             antares_model::error::DB_OVERLOADED
    1715              :         );
    1716            2 :     }
    1717              : 
    1718              :     /// 5.5.6 InternalError: the RFC 7807 `detail` a client sees must be
    1719              :     /// generic — driver internals (SQL text, row counts, connection
    1720              :     /// strings) belong in the server log, never in the response body.
    1721              :     #[test]
    1722            2 :     fn db_error_detail_is_generic() {
    1723            2 :         let pd = super::db(sqlx::Error::RowNotFound).to_problem_details();
    1724            2 :         assert_eq!(pd.detail, "database error");
    1725            2 :         assert_eq!(pd.title, "InternalError");
    1726            2 :         assert!(
    1727            2 :             !pd.detail.contains("no rows"),
    1728              :             "sqlx internals leaked into the client-visible detail: {}",
    1729              :             pd.detail
    1730              :         );
    1731              :         // a configuration error that is NOT one of ours stays generic too
    1732            2 :         let pd = super::db(sqlx::Error::Configuration("boom".into())).to_problem_details();
    1733            2 :         assert_eq!(pd.detail, "database error");
    1734            2 :         assert!(!pd.detail.contains("boom"), "{}", pd.detail);
    1735            2 :     }
    1736              : 
    1737              :     /// A spec error the store raised itself travels out through the driver
    1738              :     /// error channel. Rebuilding it as a fixed variant forces every one of
    1739              :     /// them to that variant's status — a 400 BadRequestData would reach the
    1740              :     /// client as a 403.
    1741              :     #[test]
    1742            2 :     fn a_store_raised_spec_error_keeps_its_own_status() {
    1743            6 :         for (err, kind, status) in [
    1744            2 :             (NgsiError::BadRequestData("x".into()), "BadRequestData", 400),
    1745            2 :             (NgsiError::TooManyResults("x".into()), "TooManyResults", 403),
    1746            2 :             (NgsiError::AlreadyExists("x".into()), "AlreadyExists", 409),
    1747            2 :         ] {
    1748            6 :             let out = super::db(sqlx::Error::Configuration(Box::new(err)));
    1749            6 :             assert_eq!(out.kind(), kind);
    1750            6 :             assert_eq!(out.status(), status, "{kind} lost its status");
    1751              :         }
    1752            2 :     }
    1753              : }
    1754              : 
    1755              : // The driver seam: `AnyStore` carries both driver interfaces, delegating to
    1756              : // the inherent methods above. New backends implement the traits directly —
    1757              : // this enum stays an implementation detail of the built-in backends, no
    1758              : // longer the API surface.
    1759              : /// The most Entities one history transaction appends. Each append
    1760              : /// holds `FOR KEY SHARE` on its Entity until the commit, which only a DELETE
    1761              : /// of that Entity waits for (writes lock `FOR NO KEY UPDATE`); a
    1762              : /// 1000-member page in one transaction would hold such a delete for the whole
    1763              : /// append. Small requests, a live vehicle feed's, are one transaction.
    1764              : // A fixed chunk; a measured one if lock waits show.
    1765              : #[cfg(feature = "postgres")]
    1766              : const HISTORY_CHUNK: usize = 64;
    1767              : 
    1768              : #[async_trait::async_trait]
    1769              : impl antares_store::CurrentStateDriver for AnyStore {
    1770           16 :     async fn ping(&self) -> Result<(), NgsiError> {
    1771              :         AnyStore::ping(self).await
    1772           16 :     }
    1773           86 :     fn commit_queue(&self) -> Option<(usize, usize)> {
    1774           86 :         AnyStore::commit_queue(self)
    1775           86 :     }
    1776           86 :     fn version_info(&self) -> Value {
    1777           86 :         AnyStore::version_info(self)
    1778           86 :     }
    1779           16 :     async fn close(&self) {
    1780              :         AnyStore::close(self).await;
    1781           16 :     }
    1782         1290 :     fn set_change_hook(&self, h: super::ChangeHook) {
    1783         1290 :         AnyStore::set_change_hook(self, h);
    1784         1290 :     }
    1785            0 :     fn set_outbox(&self, on: bool) {
    1786            0 :         AnyStore::set_outbox(self, on);
    1787            0 :     }
    1788          730 :     async fn outbox_peek(&self, limit: i64) -> Result<Vec<(i64, String, Value)>, NgsiError> {
    1789              :         AnyStore::outbox_peek(self, limit).await
    1790          730 :     }
    1791            0 :     async fn outbox_ack(&self, seqs: &[i64]) -> Result<u64, NgsiError> {
    1792              :         AnyStore::outbox_ack(self, seqs).await
    1793            0 :     }
    1794            0 :     async fn outbox_retain(&self, tenant: &TenantId, seqs: &[i64]) -> Result<u64, NgsiError> {
    1795              :         AnyStore::outbox_retain(self, tenant, seqs).await
    1796            0 :     }
    1797            2 :     async fn outbox_event(&self, seq: i64, tenant: &TenantId) -> Result<Option<Value>, NgsiError> {
    1798              :         AnyStore::outbox_event(self, seq, tenant).await
    1799            2 :     }
    1800              :     async fn create(
    1801              :         &self,
    1802              :         tenant: &TenantId,
    1803              :         kind: Kind,
    1804              :         id: &str,
    1805              :         doc: Value,
    1806       160682 :     ) -> Result<bool, NgsiError> {
    1807              :         AnyStore::create(self, tenant, kind, id, doc).await
    1808       160682 :     }
    1809              :     async fn batch_create(
    1810              :         &self,
    1811              :         tenant: &TenantId,
    1812              :         items: Vec<(String, Value)>,
    1813          156 :     ) -> Result<Vec<bool>, NgsiError> {
    1814              :         AnyStore::batch_create(self, tenant, items).await
    1815          156 :     }
    1816              :     async fn batch_delete(
    1817              :         &self,
    1818              :         tenant: &TenantId,
    1819              :         ids: &[String],
    1820          390 :     ) -> Result<Vec<bool>, NgsiError> {
    1821              :         AnyStore::batch_delete(self, tenant, ids).await
    1822          390 :     }
    1823              :     async fn batch_upsert(
    1824              :         &self,
    1825              :         tenant: &TenantId,
    1826              :         items: Vec<(String, Value)>,
    1827          178 :     ) -> Result<Vec<bool>, NgsiError> {
    1828              :         AnyStore::batch_upsert(self, tenant, items).await
    1829          178 :     }
    1830              :     async fn upsert(
    1831              :         &self,
    1832              :         tenant: &TenantId,
    1833              :         kind: Kind,
    1834              :         id: &str,
    1835              :         doc: Value,
    1836        20760 :     ) -> Result<bool, NgsiError> {
    1837              :         AnyStore::upsert(self, tenant, kind, id, doc).await
    1838        20760 :     }
    1839              :     async fn get(
    1840              :         &self,
    1841              :         tenant: &TenantId,
    1842              :         kind: Kind,
    1843              :         id: &str,
    1844        30592 :     ) -> Result<Option<Value>, NgsiError> {
    1845              :         AnyStore::get(self, tenant, kind, id).await
    1846        30592 :     }
    1847              :     async fn get_entities(
    1848              :         &self,
    1849              :         tenant: &TenantId,
    1850              :         ids: &[&str],
    1851           72 :     ) -> Result<std::collections::HashMap<String, Value>, NgsiError> {
    1852              :         AnyStore::get_entities(self, tenant, ids).await
    1853           72 :     }
    1854         4960 :     async fn delete(&self, tenant: &TenantId, kind: Kind, id: &str) -> Result<bool, NgsiError> {
    1855              :         AnyStore::delete(self, tenant, kind, id).await
    1856         4960 :     }
    1857              :     async fn delete_entity_if(
    1858              :         &self,
    1859              :         tenant: &TenantId,
    1860              :         id: &str,
    1861              :         keep: &(dyn for<'v> Fn(&'v Value) -> bool + Sync),
    1862         3126 :     ) -> Result<bool, NgsiError> {
    1863              :         AnyStore::delete_entity_if(self, tenant, id, keep).await
    1864         3126 :     }
    1865         5160 :     async fn list(&self, tenant: &TenantId, kind: Kind) -> Result<Vec<Value>, NgsiError> {
    1866              :         AnyStore::list(self, tenant, kind).await
    1867         5160 :     }
    1868              :     async fn list_digests(
    1869              :         &self,
    1870              :         tenant: &TenantId,
    1871              :         kind: Kind,
    1872            0 :     ) -> Result<Vec<(String, u64)>, NgsiError> {
    1873              :         AnyStore::list_digests(self, tenant, kind).await
    1874            0 :     }
    1875              :     async fn list_page(
    1876              :         &self,
    1877              :         tenant: &TenantId,
    1878              :         kind: Kind,
    1879              :         after: Option<&str>,
    1880              :         limit: usize,
    1881       617492 :     ) -> Result<Vec<Value>, NgsiError> {
    1882              :         AnyStore::list_page(self, tenant, kind, after, limit).await
    1883       617492 :     }
    1884              :     async fn list_slice(
    1885              :         &self,
    1886              :         tenant: &TenantId,
    1887              :         kind: Kind,
    1888              :         offset: usize,
    1889              :         limit: usize,
    1890          286 :     ) -> Result<(Vec<Value>, usize), NgsiError> {
    1891              :         AnyStore::list_slice(self, tenant, kind, offset, limit).await
    1892          286 :     }
    1893              :     async fn matching_registrations(
    1894              :         &self,
    1895              :         tenant: &TenantId,
    1896              :         ids: Option<&[String]>,
    1897              :         types: Option<&[String]>,
    1898         4855 :     ) -> Result<Vec<Value>, NgsiError> {
    1899              :         AnyStore::matching_registrations(self, tenant, ids, types).await
    1900         4855 :     }
    1901              :     async fn query_entities(
    1902              :         &self,
    1903              :         tenant: &TenantId,
    1904              :         f: &crate::store::filter::EntityFilter<'_>,
    1905         3723 :     ) -> Result<crate::store::filter::QueryOutcome, NgsiError> {
    1906              :         AnyStore::query_entities(self, tenant, f).await
    1907         3723 :     }
    1908              :     async fn query_candidates(
    1909              :         &self,
    1910              :         tenant: &TenantId,
    1911              :         f: &crate::store::filter::EntityFilter<'_>,
    1912          372 :     ) -> Result<crate::store::filter::QueryOutcome, NgsiError> {
    1913              :         AnyStore::query_candidates(self, tenant, f).await
    1914          372 :     }
    1915          182 :     async fn entity_type_names(&self, tenant: &TenantId) -> Result<Option<Vec<String>>, NgsiError> {
    1916              :         AnyStore::entity_type_names(self, tenant).await.map(Some)
    1917          182 :     }
    1918              :     async fn entity_attr_pairs(
    1919              :         &self,
    1920              :         tenant: &TenantId,
    1921          152 :     ) -> Result<Option<Vec<(String, String)>>, NgsiError> {
    1922              :         AnyStore::entity_attr_pairs(self, tenant).await.map(Some)
    1923          152 :     }
    1924              :     async fn entity_types_beside(
    1925              :         &self,
    1926              :         tenant: &TenantId,
    1927              :         any_of: &[String],
    1928           84 :     ) -> Result<Option<Vec<String>>, NgsiError> {
    1929              :         AnyStore::entity_types_beside(self, tenant, any_of)
    1930              :             .await
    1931              :             .map(Some)
    1932           84 :     }
    1933              :     async fn entity_id_segments(
    1934              :         &self,
    1935              :         tenant: &TenantId,
    1936              :         lead: &str,
    1937              :         sep: char,
    1938              :         max: usize,
    1939           16 :     ) -> Result<Option<Vec<String>>, NgsiError> {
    1940              :         AnyStore::entity_id_segments(self, tenant, lead, sep, max).await
    1941           16 :     }
    1942              :     async fn mutate_boxed<'a>(
    1943              :         &self,
    1944              :         tenant: &TenantId,
    1945              :         kind: Kind,
    1946              :         id: &str,
    1947              :         f: antares_store::MutateFn<'a>,
    1948        24976 :     ) -> Result<Option<Result<(), ()>>, NgsiError> {
    1949              :         AnyStore::mutate(self, tenant, kind, id, f).await
    1950        24976 :     }
    1951              :     async fn batch_mutate_boxed<'a>(
    1952              :         &self,
    1953              :         tenant: &TenantId,
    1954              :         ids: &[String],
    1955              :         mut f: antares_store::BatchMutateFn<'a>,
    1956          236 :     ) -> Result<Vec<Option<Result<(), ()>>>, NgsiError> {
    1957         8324 :         AnyStore::batch_mutate(self, tenant, ids, |id, v| f(id, v)).await
    1958          236 :     }
    1959              :     /// The Postgres arm answers this in ONE statement (see
    1960              :     /// `pg::doc::record_delivery`); every other arm runs the shared rule as a
    1961              :     /// `mutate`, which is what the one statement is a translation of.
    1962              :     async fn record_delivery(
    1963              :         &self,
    1964              :         tenant: &TenantId,
    1965              :         kind: Kind,
    1966              :         id: &str,
    1967              :         now: &str,
    1968         5004 :     ) -> Result<Option<antares_store::Delivery>, NgsiError> {
    1969              :         // `doc_kind` exists only with the Postgres arm, so its call lives
    1970              :         // inside the gate with it: outside the feature this function is the
    1971              :         // shared rule and nothing else.
    1972              :         #[cfg(feature = "postgres")]
    1973              :         {
    1974              :             if let (AnyStore::Pg(p), Ok(dk)) = (self, doc_kind(kind)) {
    1975              :                 let found = p
    1976              :                     .docs
    1977              :                     .record_delivery(tenant, dk, id, now)
    1978              :                     .await
    1979              :                     .map_err(db)?;
    1980              :                 return Ok(
    1981          238 :                     found.map(|(doc, prev_success)| antares_store::Delivery { doc, prev_success })
    1982              :                 );
    1983              :             }
    1984              :         }
    1985              :         antares_store::record_delivery_via_mutate(self, tenant, kind, id, now).await
    1986         5004 :     }
    1987              :     /// The Postgres arm books every attempt in ONE statement (see
    1988              :     /// `pg::doc::record_deliveries`); every other arm books them one by one.
    1989              :     async fn record_deliveries(
    1990              :         &self,
    1991              :         tenant: &TenantId,
    1992              :         kind: Kind,
    1993              :         attempts: &[(&str, &str)],
    1994           36 :     ) -> Result<Vec<Option<antares_store::Delivery>>, NgsiError> {
    1995              :         #[cfg(feature = "postgres")]
    1996              :         {
    1997              :             if let (AnyStore::Pg(p), Ok(dk)) = (self, doc_kind(kind)) {
    1998              :                 let mut found = p
    1999              :                     .docs
    2000              :                     .record_deliveries(tenant, dk, attempts)
    2001              :                     .await
    2002              :                     .map_err(db)?;
    2003              :                 return Ok(attempts
    2004              :                     .iter()
    2005          106 :                     .map(|(id, _)| {
    2006          106 :                         found
    2007          106 :                             .remove(*id)
    2008          106 :                             .map(|(doc, prev_success)| antares_store::Delivery {
    2009          102 :                                 doc,
    2010          102 :                                 prev_success,
    2011          102 :                             })
    2012          106 :                     })
    2013              :                     .collect());
    2014              :             }
    2015              :         }
    2016              :         let mut out = Vec::with_capacity(attempts.len());
    2017              :         for (id, now) in attempts {
    2018              :             out.push(antares_store::record_delivery_via_mutate(self, tenant, kind, id, now).await?);
    2019              :         }
    2020              :         Ok(out)
    2021           36 :     }
    2022         1956 :     async fn sweep_expired(&self) -> usize {
    2023              :         AnyStore::sweep_expired(self)
    2024         1956 :     }
    2025              :     async fn retain_current(
    2026              :         &self,
    2027              :         tenant: &TenantId,
    2028              :         type_iri: &str,
    2029              :         before: &str,
    2030              :         batch: usize,
    2031              :         dry_run: bool,
    2032           16 :     ) -> Result<u64, NgsiError> {
    2033              :         match self {
    2034              :             AnyStore::Mem(s) => Ok(s.retain_current(tenant, type_iri, before, batch, dry_run)),
    2035              :             #[cfg(feature = "postgres")]
    2036              :             AnyStore::Pg(p) => crate::store::pg::maintenance::retain_current(
    2037              :                 p.docs.pool(),
    2038              :                 tenant.as_str(),
    2039              :                 type_iri,
    2040              :                 before,
    2041              :                 batch,
    2042              :                 dry_run,
    2043              :             )
    2044              :             .await
    2045              :             .map_err(db),
    2046              :         }
    2047           16 :     }
    2048          298 :     async fn tenant_exists(&self, tenant: &TenantId) -> Result<bool, NgsiError> {
    2049              :         AnyStore::tenant_exists(self, tenant).await
    2050          298 :     }
    2051         3153 :     async fn subscription_tenants(&self) -> Result<Vec<String>, NgsiError> {
    2052              :         AnyStore::subscription_tenants(self).await
    2053         3153 :     }
    2054         2000 :     async fn tenant_ids(&self) -> Result<Vec<String>, NgsiError> {
    2055              :         AnyStore::tenant_ids(self).await
    2056         2000 :     }
    2057              :     async fn history_overrides(
    2058              :         &self,
    2059              :         limit: usize,
    2060            4 :     ) -> Result<Vec<(String, String, Value)>, NgsiError> {
    2061              :         match self {
    2062              :             AnyStore::Mem(s) => Ok(s.history_overrides(limit)),
    2063              :             #[cfg(feature = "postgres")]
    2064              :             AnyStore::Pg(p) => {
    2065              :                 crate::store::pg::maintenance::history_overrides(p.docs.pool(), limit)
    2066              :                     .await
    2067              :                     .map_err(db)
    2068              :             }
    2069              :         }
    2070            4 :     }
    2071              :     async fn tenant_stats_one(
    2072              :         &self,
    2073              :         tenant: &TenantId,
    2074           40 :     ) -> Result<Option<antares_store::TenantStats>, NgsiError> {
    2075              :         AnyStore::tenant_stats_one(self, tenant).await
    2076           40 :     }
    2077              :     async fn tenant_bytes(
    2078              :         &self,
    2079              :         tenant: &TenantId,
    2080           38 :     ) -> Result<antares_store::TenantBytes, NgsiError> {
    2081              :         match self {
    2082              :             AnyStore::Mem(s) => Ok(s.tenant_bytes(tenant)),
    2083              :             #[cfg(feature = "postgres")]
    2084              :             AnyStore::Pg(p) => {
    2085            8 :                 async {
    2086            8 :                     let mut tx = tenant_tx(p, tenant).await?;
    2087              :                     // a scan of the tenant: bounded, a slow one answers no size
    2088            8 :                     sqlx::query(SIZE_SCAN_TIMEOUT)
    2089            8 :                         .execute(&mut *tx)
    2090            8 :                         .await
    2091            8 :                         .map_err(db)?;
    2092              :                     // the stored row, TOAST included, by the first type the
    2093              :                     // write recorded
    2094            8 :                     let rows: Vec<(String, i64)> = sqlx::query_as(
    2095            8 :                         "SELECT coalesce(types[1], ''), sum(pg_column_size(e.*))::bigint
    2096            8 :                            FROM entities e WHERE tenant_id = $1 GROUP BY 1",
    2097            8 :                     )
    2098            8 :                     .bind(tenant.as_str())
    2099            8 :                     .fetch_all(&mut *tx)
    2100            8 :                     .await
    2101            8 :                     .map_err(db)?;
    2102            6 :                     let mut out = antares_store::TenantBytes::default();
    2103            6 :                     for (ty, n) in rows {
    2104            6 :                         let n = n.max(0) as u64;
    2105            6 :                         out.entities = out.entities.saturating_add(n);
    2106            6 :                         out.by_type.insert(ty, n);
    2107            6 :                     }
    2108            6 :                     Ok(out)
    2109            8 :                 }
    2110              :                 .await
    2111              :             }
    2112              :         }
    2113           38 :     }
    2114          116 :     async fn purge_tenant(&self, tenant: &TenantId) -> Result<bool, NgsiError> {
    2115              :         AnyStore::purge_tenant(self, tenant).await
    2116          116 :     }
    2117              :     async fn context_put(
    2118              :         &self,
    2119              :         tenant: Option<&TenantId>,
    2120              :         id: &str,
    2121              :         doc: Value,
    2122          688 :     ) -> Result<(), NgsiError> {
    2123              :         AnyStore::context_put(self, tenant, id, doc).await
    2124          688 :     }
    2125              :     async fn context_get(
    2126              :         &self,
    2127              :         tenant: Option<&TenantId>,
    2128              :         id: &str,
    2129         1350 :     ) -> Result<Option<Value>, NgsiError> {
    2130              :         AnyStore::context_get(self, tenant, id).await
    2131         1350 :     }
    2132              :     async fn context_mutate<'a>(
    2133              :         &self,
    2134              :         tenant: Option<&TenantId>,
    2135              :         id: &str,
    2136              :         f: antares_store::MutateFn<'a>,
    2137        21058 :     ) -> Result<Option<Result<(), ()>>, NgsiError> {
    2138              :         AnyStore::context_mutate(self, tenant, id, f).await
    2139        21058 :     }
    2140          412 :     async fn context_delete(&self, tenant: Option<&TenantId>, id: &str) -> Result<bool, NgsiError> {
    2141              :         AnyStore::context_delete(self, tenant, id).await
    2142          412 :     }
    2143          282 :     async fn context_list_meta(&self, tenant: Option<&TenantId>) -> Result<Vec<Value>, NgsiError> {
    2144              :         AnyStore::context_list_meta(self, tenant).await
    2145          282 :     }
    2146              : }
    2147              : 
    2148              : #[async_trait::async_trait]
    2149              : impl antares_store::TemporalDriver for AnyStore {
    2150            8 :     async fn close(&self) {
    2151              :         AnyStore::close(self).await;
    2152            8 :     }
    2153            0 :     fn set_short_history(&self, short: antares_store::ShortHistory) {
    2154            0 :         match self {
    2155              :             // Memory has no WAL to save
    2156            0 :             AnyStore::Mem(_) => drop(short),
    2157              :             #[cfg(feature = "postgres")]
    2158            0 :             AnyStore::Pg(p) => p.temporal.set_short_history(short),
    2159              :         }
    2160            0 :     }
    2161              :     async fn versions(
    2162              :         &self,
    2163              :         tenant: &TenantId,
    2164              :         entity_id: &str,
    2165            0 :     ) -> Result<serde_json::Map<String, Value>, NgsiError> {
    2166              :         match self {
    2167              :             // Memory keeps no versions or provenance
    2168              :             AnyStore::Mem(_) => {
    2169              :                 let _ = (tenant, entity_id);
    2170              :                 Ok(serde_json::Map::new())
    2171              :             }
    2172              :             #[cfg(feature = "postgres")]
    2173              :             AnyStore::Pg(p) => p.temporal.versions(tenant, entity_id).await.map_err(db),
    2174              :         }
    2175            0 :     }
    2176              :     async fn erase(
    2177              :         &self,
    2178              :         tenant: &TenantId,
    2179              :         entity_id: &str,
    2180              :         attrs: Option<&[String]>,
    2181              :         before: Option<&str>,
    2182            0 :     ) -> Result<u64, NgsiError> {
    2183              :         match self {
    2184              :             AnyStore::Mem(_) => {
    2185            0 :                 let instant = |t: &str| {
    2186            0 :                     chrono::DateTime::parse_from_rfc3339(
    2187            0 :                         &antares_store::filter::canonical_datetime(t),
    2188              :                     )
    2189            0 :                     .ok()
    2190            0 :                 };
    2191              :                 // an instance goes when it was observed at or before the cut;
    2192              :                 // a cut that is no DateTime erases nothing
    2193              :                 let cut = before.map(instant);
    2194            0 :                 let goes = |i: &Value| match cut {
    2195            0 :                     None => true,
    2196            0 :                     Some(cut) => {
    2197            0 :                         let at = i
    2198            0 :                             .get("observedAt")
    2199            0 :                             .and_then(Value::as_str)
    2200            0 :                             .and_then(instant);
    2201            0 :                         cut.zip(at).is_some_and(|(cut, at)| at <= cut)
    2202              :                     }
    2203            0 :                 };
    2204            0 :                 let erased = AnyStore::mutate(self, tenant, Kind::Temporal, entity_id, |doc| {
    2205            0 :                     let o = doc.as_object_mut().ok_or(())?;
    2206            0 :                     let mut n = 0u64;
    2207            0 :                     for (k, v) in o.iter_mut() {
    2208            0 :                         let named = attrs.is_none_or(|a| a.contains(k));
    2209            0 :                         let Some(list) = v.as_array_mut().filter(|_| named) else {
    2210            0 :                             continue;
    2211              :                         };
    2212            0 :                         if !list.iter().all(Value::is_object) {
    2213            0 :                             continue; // the Entity's own `type` list
    2214            0 :                         }
    2215            0 :                         let was = list.len();
    2216            0 :                         list.retain(|i| !goes(i));
    2217            0 :                         let gone = was.saturating_sub(list.len());
    2218            0 :                         n = n.saturating_add(u64::try_from(gone).unwrap_or(u64::MAX));
    2219              :                     }
    2220            0 :                     let emptied = o
    2221            0 :                         .values()
    2222            0 :                         .filter_map(Value::as_array)
    2223            0 :                         .all(|l| l.is_empty() || !l.iter().all(Value::is_object));
    2224            0 :                     Ok::<_, ()>((n, emptied))
    2225            0 :                 })
    2226              :                 .await?;
    2227              :                 let Some(Ok((n, emptied))) = erased else {
    2228              :                     return Ok(0);
    2229              :                 };
    2230              :                 if attrs.is_none() && emptied {
    2231              :                     AnyStore::delete(self, tenant, Kind::Temporal, entity_id).await?;
    2232              :                 }
    2233              :                 Ok(n)
    2234              :             }
    2235              :             #[cfg(feature = "postgres")]
    2236              :             AnyStore::Pg(p) => p
    2237              :                 .temporal
    2238              :                 .erase(tenant, entity_id, attrs, before)
    2239              :                 .await
    2240              :                 .map_err(db),
    2241              :         }
    2242            0 :     }
    2243              :     async fn retain_history(
    2244              :         &self,
    2245              :         tenant: &TenantId,
    2246              :         scope: antares_store::HistoryScope<'_>,
    2247              :         before: &str,
    2248              :         batch: usize,
    2249              :         dry_run: bool,
    2250           32 :     ) -> Result<u64, NgsiError> {
    2251              :         match self {
    2252              :             AnyStore::Mem(s) => Ok(s.retain_history(tenant, scope, before, batch, dry_run)),
    2253              :             #[cfg(feature = "postgres")]
    2254              :             AnyStore::Pg(p) => crate::store::pg::maintenance::retain_history(
    2255              :                 p.docs.pool(),
    2256              :                 tenant.as_str(),
    2257              :                 scope,
    2258              :                 before,
    2259              :                 batch,
    2260              :                 dry_run,
    2261              :             )
    2262              :             .await
    2263              :             .map_err(db),
    2264              :         }
    2265           32 :     }
    2266              :     async fn drop_retained_history(
    2267              :         &self,
    2268              :         floors: &[(String, String)],
    2269            0 :     ) -> Result<Vec<String>, NgsiError> {
    2270              :         match self {
    2271              :             // memory keeps no partitions
    2272              :             AnyStore::Mem(_) => {
    2273            0 :                 let _ = floors;
    2274              :                 Ok(Vec::new())
    2275              :             }
    2276              :             #[cfg(feature = "postgres")]
    2277              :             AnyStore::Pg(p) => {
    2278              :                 crate::store::pg::maintenance::drop_retained_partitions(p.docs.pool(), floors)
    2279              :                     .await
    2280              :                     .map_err(db)
    2281              :             }
    2282              :         }
    2283            0 :     }
    2284           80 :     fn version_info(&self) -> Value {
    2285           80 :         AnyStore::version_info(self)
    2286           80 :     }
    2287            6 :     async fn attr_instance_count(&self, tenant: &TenantId) -> Result<u64, NgsiError> {
    2288              :         match self {
    2289              :             AnyStore::Mem(s) => Ok(s.attr_instance_count(tenant)),
    2290              :             #[cfg(feature = "postgres")]
    2291              :             AnyStore::Pg(p) => {
    2292            4 :                 async {
    2293            4 :                     let mut tx = tenant_tx(p, tenant).await?;
    2294            4 :                     count_instances(&mut tx, tenant).await
    2295            4 :                 }
    2296              :                 .await
    2297              :             }
    2298              :         }
    2299            6 :     }
    2300              :     async fn attr_instance_count_within(
    2301              :         &self,
    2302              :         tenant: &TenantId,
    2303              :         bound: std::time::Duration,
    2304           34 :     ) -> Result<u64, NgsiError> {
    2305              :         match self {
    2306              :             AnyStore::Mem(s) => {
    2307              :                 // the memory store counts at once: no bound to apply
    2308              :                 let _ = bound;
    2309              :                 Ok(s.attr_instance_count(tenant))
    2310              :             }
    2311              :             #[cfg(feature = "postgres")]
    2312              :             AnyStore::Pg(p) => {
    2313            4 :                 async {
    2314            4 :                     let mut tx = tenant_tx(p, tenant).await?;
    2315              :                     // a scan of the tenant's history: a slow one answers no count
    2316            4 :                     sqlx::query("SELECT set_config('statement_timeout', $1, true)")
    2317            4 :                         .bind(format!("{}ms", bound.as_millis().max(1)))
    2318            4 :                         .execute(&mut *tx)
    2319            4 :                         .await
    2320            4 :                         .map_err(db)?;
    2321            4 :                     count_instances(&mut tx, tenant).await
    2322            4 :                 }
    2323              :                 .await
    2324              :             }
    2325              :         }
    2326           34 :     }
    2327           36 :     async fn attr_instance_bytes(&self, tenant: &TenantId) -> Result<u64, NgsiError> {
    2328              :         match self {
    2329              :             AnyStore::Mem(s) => Ok(s.attr_instance_bytes(tenant)),
    2330              :             #[cfg(feature = "postgres")]
    2331              :             AnyStore::Pg(p) => {
    2332            8 :                 async {
    2333            8 :                     let mut tx = tenant_tx(p, tenant).await?;
    2334            8 :                     sqlx::query(SIZE_SCAN_TIMEOUT)
    2335            8 :                         .execute(&mut *tx)
    2336            8 :                         .await
    2337            8 :                         .map_err(db)?;
    2338            8 :                     let n: i64 = sqlx::query_scalar(
    2339            8 :                         "SELECT (SELECT coalesce(sum(pg_column_size(a.*)), 0)
    2340            8 :                                    FROM attr_instances a WHERE tenant_id = $1)::bigint
    2341            8 :                               + (SELECT coalesce(sum(pg_column_size(r.*)), 0)
    2342            8 :                                    FROM attr_rows r WHERE tenant_id = $1)::bigint",
    2343            8 :                     )
    2344            8 :                     .bind(tenant.as_str())
    2345            8 :                     .fetch_one(&mut *tx)
    2346            8 :                     .await
    2347            8 :                     .map_err(db)?;
    2348            6 :                     Ok(n.max(0) as u64)
    2349            8 :                 }
    2350              :                 .await
    2351              :             }
    2352              :         }
    2353           36 :     }
    2354          112 :     async fn purge_tenant(&self, tenant: &TenantId) -> Result<(), NgsiError> {
    2355              :         match self {
    2356              :             AnyStore::Mem(s) => {
    2357              :                 s.purge_kinds(tenant, &[Kind::Temporal]);
    2358              :                 Ok(())
    2359              :             }
    2360              :             #[cfg(feature = "postgres")]
    2361              :             AnyStore::Pg(p) => {
    2362           26 :                 async {
    2363           26 :                     let mut tx = tenant_tx(p, tenant).await?;
    2364              :                     // whole statements, not a spliced table name: the
    2365              :                     // attr_instances tenant-predicate check reads literals
    2366          130 :                     for sql in [
    2367           26 :                         "DELETE FROM attr_instances WHERE tenant_id = $1",
    2368           26 :                         "DELETE FROM attr_rows WHERE tenant_id = $1",
    2369           26 :                         "DELETE FROM iri_ids WHERE tenant_id = $1",
    2370           26 :                         "DELETE FROM attr_versions WHERE tenant_id = $1",
    2371           26 :                         "DELETE FROM temporal_entities WHERE tenant_id = $1",
    2372           26 :                     ] {
    2373          130 :                         sqlx::query(sql)
    2374          130 :                             .bind(tenant.as_str())
    2375          130 :                             .execute(&mut *tx)
    2376          130 :                             .await
    2377          130 :                             .map_err(db)?;
    2378              :                     }
    2379           26 :                     tx.commit().await.map_err(db)?;
    2380           26 :                     p.temporal.forget_tenant(tenant);
    2381           26 :                     Ok(())
    2382           26 :                 }
    2383              :                 .await
    2384              :             }
    2385              :         }
    2386          112 :     }
    2387              :     async fn temporal_append(
    2388              :         &self,
    2389              :         tenant: &TenantId,
    2390              :         id: &str,
    2391              :         shell: &Value,
    2392              :         additions: &Value,
    2393       121884 :     ) -> Result<(), NgsiError> {
    2394              :         AnyStore::temporal_append(self, tenant, id, shell, additions).await
    2395       121884 :     }
    2396              :     /// On Postgres a drained request is one transaction per
    2397              :     /// `HISTORY_CHUNK` Entities; a scope change, and the memory
    2398              :     /// arm, take the default's append per Entity.
    2399       123506 :     async fn event_list(&self, evs: &[antares_store::TemporalEvent]) -> Result<(), NgsiError> {
    2400              :         #[cfg(feature = "postgres")]
    2401              :         if let (AnyStore::Pg(p), Some(appends)) = (self, antares_store::fold_appends(evs)) {
    2402              :             // One chunk that fails (a lock timeout) loses its own Entities'
    2403              :             // history, never the rest of the request's.
    2404              :             let mut first = None;
    2405              :             for chunk in appends.chunks(HISTORY_CHUNK) {
    2406              :                 if let Err(e) = p.temporal.append_many(chunk).await {
    2407              :                     first.get_or_insert(e);
    2408              :                 }
    2409              :             }
    2410            0 :             return first.map_or(Ok(()), |e| Err(db(e)));
    2411              :         }
    2412              :         antares_store::drain_events(self, evs).await
    2413       123506 :     }
    2414              :     /// Only the Postgres arm pages in SQL, and only an exact prefilter
    2415              :     /// makes that page the evaluator's page; the memory arm answers
    2416              :     /// `paged: false` and the caller pages.
    2417          246 :     fn q_pushdown_exact(
    2418          246 :         &self,
    2419          246 :         q: &antares_ql::QNode,
    2420          246 :         range: Option<&crate::store::filter::InstanceRange<'_>>,
    2421          246 :         expand: &dyn Fn(&str) -> String,
    2422          246 :     ) -> bool {
    2423          246 :         match self {
    2424              :             // the memory arm pages nothing, so the filter is not its business
    2425              :             AnyStore::Mem(_) => {
    2426          146 :                 let _ = (q, range, expand);
    2427          146 :                 false
    2428              :             }
    2429              :             #[cfg(feature = "postgres")]
    2430          100 :             AnyStore::Pg(_) => crate::compile::qprefilter::prefilter_exact(q, range, expand),
    2431              :         }
    2432          246 :     }
    2433              :     async fn query_temporal(
    2434              :         &self,
    2435              :         tenant: &TenantId,
    2436              :         f: &crate::store::filter::TemporalFilter<'_>,
    2437         1046 :     ) -> Result<crate::store::filter::TemporalOutcome, NgsiError> {
    2438              :         AnyStore::query_temporal(self, tenant, f).await
    2439         1046 :     }
    2440              :     async fn get_temporal(
    2441              :         &self,
    2442              :         tenant: &TenantId,
    2443              :         id: &str,
    2444              :         f: &crate::store::filter::TemporalFilter<'_>,
    2445         1032 :     ) -> Result<Option<Value>, NgsiError> {
    2446              :         AnyStore::get_temporal(self, tenant, id, f).await
    2447         1032 :     }
    2448         1604 :     async fn get(&self, tenant: &TenantId, id: &str) -> Result<Option<Value>, NgsiError> {
    2449              :         AnyStore::get(self, tenant, Kind::Temporal, id).await
    2450         1604 :     }
    2451         1256 :     async fn create(&self, tenant: &TenantId, id: &str, doc: Value) -> Result<bool, NgsiError> {
    2452              :         AnyStore::create(self, tenant, Kind::Temporal, id, doc).await
    2453         1256 :     }
    2454           56 :     async fn upsert(&self, tenant: &TenantId, id: &str, doc: Value) -> Result<bool, NgsiError> {
    2455              :         AnyStore::upsert(self, tenant, Kind::Temporal, id, doc).await
    2456           56 :     }
    2457        15032 :     async fn delete(&self, tenant: &TenantId, id: &str) -> Result<bool, NgsiError> {
    2458              :         AnyStore::delete(self, tenant, Kind::Temporal, id).await
    2459        15032 :     }
    2460           50 :     async fn list(&self, tenant: &TenantId) -> Result<Vec<Value>, NgsiError> {
    2461              :         AnyStore::list(self, tenant, Kind::Temporal).await
    2462           50 :     }
    2463              :     async fn mutate_boxed<'a>(
    2464              :         &self,
    2465              :         tenant: &TenantId,
    2466              :         id: &str,
    2467              :         f: antares_store::MutateFn<'a>,
    2468          848 :     ) -> Result<Option<Result<(), ()>>, NgsiError> {
    2469              :         // Postgres takes the provenance into its column in decompose; the
    2470              :         // memory store keeps none, so neither arm answers it as data.
    2471              :         let keeps_none = matches!(self, AnyStore::Mem(_));
    2472          730 :         AnyStore::mutate(self, tenant, Kind::Temporal, id, move |doc: &mut Value| {
    2473          730 :             let done = f(doc);
    2474          730 :             if keeps_none {
    2475          454 :                 antares_store::strip_provenance(doc);
    2476          548 :             }
    2477          730 :             done
    2478          730 :         })
    2479              :         .await
    2480          848 :     }
    2481              : }
    2482              : 
    2483              : #[cfg(test)]
    2484              : mod temporal_only_tests {
    2485              :     use super::*;
    2486              :     use antares_model::TenantId;
    2487              : 
    2488            4 :     async fn append(store: &AnyStore) -> Option<Value> {
    2489            4 :         let t = TenantId::new("combo").expect("tenant");
    2490            4 :         let shell = serde_json::json!({"id": "urn:a", "type": ["T"]});
    2491            4 :         let adds = serde_json::json!({"speed": [{"instanceId": "urn:i:1", "value": 1}]});
    2492            4 :         store
    2493            4 :             .temporal_append(&t, "urn:a", &shell, &adds)
    2494            4 :             .await
    2495            4 :             .expect("append");
    2496            4 :         store.get(&t, Kind::Temporal, "urn:a").await.expect("get")
    2497            4 :     }
    2498              : 
    2499              :     /// A shared instance records only for an entity it holds (5.6.6 delete
    2500              :     /// overlap); the temporal half of a mixed deployment records
    2501              :     /// unconditionally, since the entities live elsewhere.
    2502              :     #[tokio::test]
    2503            2 :     async fn a_temporal_only_instance_records_without_the_entity() {
    2504            2 :         assert!(append(&AnyStore::Mem(Store::default())).await.is_none());
    2505            2 :         let doc = append(&AnyStore::Mem(Store::default()).temporal_only())
    2506            2 :             .await
    2507            2 :             .expect("recorded");
    2508            2 :         assert_eq!(doc["speed"][0]["value"], 1);
    2509            2 :     }
    2510              : }
    2511              : 
    2512              : #[cfg(test)]
    2513              : mod instance_cap_tests {
    2514              :     use super::*;
    2515              :     use antares_model::TenantId;
    2516              :     use antares_store::TemporalDriverExt;
    2517              : 
    2518           46 :     fn t() -> TenantId {
    2519           46 :         TenantId::new("capped").expect("tenant")
    2520           46 :     }
    2521              : 
    2522           42 :     fn inst(i: usize) -> Value {
    2523           42 :         serde_json::json!({"instanceId": format!("urn:i:{i}"), "type": "Property", "value": i,
    2524           42 :                            "observedAt": format!("2026-01-01T00:00:{:02}Z", i % 60)})
    2525           42 :     }
    2526              : 
    2527           20 :     async fn append(store: &AnyStore, i: usize) -> Result<(), NgsiError> {
    2528           20 :         let shell = serde_json::json!({"id": "urn:a", "type": ["T"]});
    2529           20 :         let adds = serde_json::json!({"speed": [inst(i)]});
    2530           20 :         store.temporal_append(&t(), "urn:a", &shell, &adds).await
    2531           20 :     }
    2532              : 
    2533           12 :     async fn speed(store: &AnyStore) -> usize {
    2534           12 :         store
    2535           12 :             .get(&t(), Kind::Temporal, "urn:a")
    2536           12 :             .await
    2537           12 :             .expect("get")
    2538           12 :             .and_then(|d| d["speed"].as_array().map(Vec::len))
    2539           12 :             .unwrap_or(0)
    2540           12 :     }
    2541              : 
    2542              :     /// One Attribute's history in the memory/file store is bounded: the
    2543              :     /// append past the cap is refused with an error that says why, and
    2544              :     /// nothing of it is written. The auto-recording of Entity writes
    2545              :     /// takes this path.
    2546              :     #[tokio::test]
    2547            2 :     async fn an_append_past_the_instance_cap_is_refused_and_writes_nothing() {
    2548            2 :         let store = AnyStore::Mem(Store::default())
    2549            2 :             .temporal_only()
    2550            2 :             .max_instances(3);
    2551            6 :         for i in 0..3 {
    2552            6 :             append(&store, i).await.expect("under the cap");
    2553              :         }
    2554            2 :         assert_eq!(speed(&store).await, 3);
    2555            2 :         let refused = append(&store, 3).await;
    2556            2 :         assert!(
    2557            2 :             matches!(&refused, Err(NgsiError::BadRequestData(m)) if m.contains("3 instances")),
    2558              :             "{refused:?}"
    2559              :         );
    2560            2 :         assert_eq!(speed(&store).await, 3, "the refused append wrote");
    2561              :         // a correction of a held instance adds none and still lands
    2562            2 :         append(&store, 2)
    2563            2 :             .await
    2564            2 :             .expect("same instanceId, same count");
    2565              :         // no cap, no refusal
    2566            2 :         let open = AnyStore::Mem(Store::default()).temporal_only();
    2567           10 :         for i in 0..5 {
    2568           10 :             append(&open, i).await.expect("uncapped");
    2569            2 :         }
    2570            2 :         assert_eq!(speed(&open).await, 5);
    2571            2 :     }
    2572              : 
    2573              :     /// A first append past the cap is refused before anything is stored:
    2574              :     /// no attribute-less Temporal Entity is left behind it.
    2575              :     #[tokio::test]
    2576            2 :     async fn a_refused_first_append_leaves_no_empty_temporal_shell() {
    2577            2 :         let store = AnyStore::Mem(Store::default())
    2578            2 :             .temporal_only()
    2579            2 :             .max_instances(2);
    2580            2 :         let shell = serde_json::json!({"id": "urn:a", "type": ["T"]});
    2581            2 :         let adds = serde_json::json!({"speed": [inst(0), inst(1), inst(2)]});
    2582            2 :         assert!(store
    2583            2 :             .temporal_append(&t(), "urn:a", &shell, &adds)
    2584            2 :             .await
    2585            2 :             .is_err());
    2586            2 :         assert!(store
    2587            2 :             .get(&t(), Kind::Temporal, "urn:a")
    2588            2 :             .await
    2589            2 :             .expect("get")
    2590            2 :             .is_none());
    2591            2 :     }
    2592              : 
    2593              :     /// The temporal API's own writes (5.6.11 create, 5.6.12 add) go through
    2594              :     /// create and mutate: the same cap, the same refusal.
    2595              :     #[tokio::test]
    2596            2 :     async fn the_temporal_api_writes_hit_the_same_cap() {
    2597              :         use antares_store::TemporalDriver;
    2598            2 :         let store = AnyStore::Mem(Store::default())
    2599            2 :             .temporal_only()
    2600            2 :             .max_instances(3);
    2601            4 :         let doc = |n: usize| {
    2602            4 :             serde_json::json!({"id": "urn:a", "type": ["T"],
    2603            4 :                                                "speed": (0..n).map(inst).collect::<Vec<_>>()})
    2604            4 :         };
    2605            2 :         let refused = TemporalDriver::create(&store, &t(), "urn:a", doc(4)).await;
    2606            2 :         assert!(
    2607            2 :             matches!(refused, Err(NgsiError::BadRequestData(_))),
    2608              :             "{refused:?}"
    2609              :         );
    2610            2 :         assert_eq!(speed(&store).await, 0, "the refused create wrote");
    2611            2 :         assert!(TemporalDriver::create(&store, &t(), "urn:a", doc(3))
    2612            2 :             .await
    2613            2 :             .expect("create at the cap"));
    2614            2 :         let grow = TemporalDriverExt::mutate(&store, &t(), "urn:a", |d| {
    2615            2 :             if let Some(a) = d["speed"].as_array_mut() {
    2616            2 :                 a.push(inst(9));
    2617            2 :             }
    2618            2 :             Ok::<(), ()>(())
    2619            2 :         })
    2620            2 :         .await;
    2621            2 :         assert!(
    2622            2 :             matches!(grow, Err(NgsiError::BadRequestData(_))),
    2623              :             "{grow:?}"
    2624              :         );
    2625            2 :         assert_eq!(speed(&store).await, 3, "the refused add wrote");
    2626              :         // the closure's own refusal still reaches its caller as its own
    2627            2 :         let own = TemporalDriverExt::mutate(&store, &t(), "urn:a", |_| Err::<(), _>("mine")).await;
    2628            2 :         assert!(matches!(own, Ok(Some(Err("mine")))), "{own:?}");
    2629              :         // deleting instances of a full history is never refused
    2630            2 :         let shrink = TemporalDriverExt::mutate(&store, &t(), "urn:a", |d| {
    2631            2 :             if let Some(a) = d["speed"].as_array_mut() {
    2632            2 :                 a.pop();
    2633            2 :             }
    2634            2 :             Ok::<(), ()>(())
    2635            2 :         })
    2636            2 :         .await;
    2637            2 :         assert!(matches!(shrink, Ok(Some(Ok(())))), "{shrink:?}");
    2638            2 :         assert_eq!(speed(&store).await, 2);
    2639            2 :     }
    2640              : }
        

Generated by: LCOV version 2.0-1