Line data Source code
1 : // SPDX-License-Identifier: EUPL-1.2
2 : //! The storage seam: two driver traits every backend implements, plus the
3 : //! backend-neutral types they speak (resource kinds, filter shapes, the
4 : //! store-mode enum). This crate names no backend — redb, sqlx and the
5 : //! browser's OPFS all live behind these traits in their own crates.
6 : //!
7 : //! Current-state and temporal storage are SEPARATE interfaces on purpose:
8 : //! a deployment may run postgres current-state with no temporal store at
9 : //! all, or memory current-state with a database-backed history. A driver
10 : //! that does not support an operation answers with an error (or a benign
11 : //! no-op for internal bookkeeping), never a panic — `NoTemporal` is the
12 : //! canonical instance.
13 : #![cfg_attr(not(test), warn(clippy::expect_used))]
14 : #![deny(missing_docs)]
15 :
16 : #[cfg(feature = "test-kit")]
17 : /// The shared driver contract every backend runs as a test. It asserts, so
18 : /// it panics on a store that breaks an invariant — that is the point.
19 : #[allow(clippy::expect_used)]
20 : pub mod contract;
21 : pub mod filter;
22 :
23 : use antares_model::{NgsiError, TenantId};
24 : use serde_json::Value;
25 :
26 : /// [`CurrentStateDriver::list_digests`] of documents read out: each one's
27 : /// `id` and a digest of its JSON text, a document without an id left out.
28 : #[must_use]
29 0 : pub fn digests(docs: Vec<Value>) -> Vec<(String, u64)> {
30 : use std::hash::{DefaultHasher, Hash, Hasher};
31 0 : docs.into_iter()
32 0 : .filter_map(|doc| {
33 0 : let id = doc.get("id")?.as_str()?.to_owned();
34 0 : let mut h = DefaultHasher::new();
35 0 : doc.to_string().hash(&mut h);
36 0 : Some((id, h.finish()))
37 0 : })
38 0 : .collect()
39 0 : }
40 :
41 : /// A stored document as the object it is. Every document a driver holds is a
42 : /// JSON object — `contract` asserts the shape on every backend — so a value
43 : /// that is not one means the driver returned something the contract forbids
44 : /// underneath a live request. That fails the one request; it never takes the
45 : /// process down, which is what an unwrap here would do.
46 22404 : pub fn stored_object(doc: &mut Value) -> Result<&mut serde_json::Map<String, Value>, NgsiError> {
47 22404 : doc.as_object_mut()
48 22404 : .ok_or_else(|| NgsiError::InternalError("stored document is not a JSON object".into()))
49 22404 : }
50 :
51 : /// What a [`ChangeHook`] call returns: the hook records temporal history
52 : /// through the temporal driver, which is asynchronous, so the driver that
53 : /// fires the hook awaits it before its own write returns.
54 : pub type HookFuture<'a> = std::pin::Pin<Box<dyn std::future::Future<Output = ()> + Send + 'a>>;
55 :
56 : /// Called with (tenant, before, after, version) on every entity write — the
57 : /// local-mode change feed: create ⇒ (None, Some), delete ⇒ (Some, None).
58 : /// Shared rather than owned: a driver clones it out of its lock before
59 : /// awaiting, since a lock guard cannot be held across an await. Two writers
60 : /// of one Entity may run their hooks in either order; `version` is the order
61 : /// they committed in ([`ChangeVersion`]).
62 : pub type ChangeHook = std::sync::Arc<
63 : dyn for<'a> Fn(&'a TenantId, Option<Value>, Option<Value>, ChangeVersion) -> HookFuture<'a>
64 : + Send
65 : + Sync,
66 : >;
67 :
68 : /// Where one entity change stands among the changes of its Entity, read
69 : /// under the write's row lock: the incarnation (the stored row's creation
70 : /// stamp, which tells a recreated Entity from the one deleted before it, as
71 : /// its version starts again at 1) and the version the write left, one past
72 : /// the deleted row's for a delete. Ordered: of two changes of one (Tenant,
73 : /// id), the greater committed last. A driver without row versions counts
74 : /// its commits instead, under the order it emits in; one that cannot tell
75 : /// the order passes the default, and equal versions are all admitted.
76 : #[derive(Clone, Debug, Default, PartialEq, Eq, PartialOrd, Ord)]
77 : pub struct ChangeVersion {
78 : /// The stored row's creation stamp as the driver spells it; empty on a
79 : /// driver that counts commits.
80 : pub incarnation: String,
81 : /// The row version the write left, or the driver's commit count.
82 : pub version: i64,
83 : }
84 :
85 : /// The newest [`ChangeVersion`] a change-hook consumer has seen per (Tenant,
86 : /// Entity id), so it drops a change a later commit's hook overtook instead
87 : /// of ending with the older image. Bounded: two generations of at most `cap`
88 : /// entries, the older forgotten when the newer fills, so memory stays at
89 : /// `2 * cap` keys whatever the number of Entities. A key forgotten admits
90 : /// its next change: hooks overtake each other within the few writes in
91 : /// flight at once, far fewer than `cap`.
92 : pub struct NewestSeen {
93 : cap: usize,
94 : gens: std::sync::Mutex<[std::collections::HashMap<(String, String), Seen>; 2]>,
95 : }
96 :
97 : /// The newest version admitted per incarnation of one key, newest
98 : /// incarnation last; at most [`INCARNATIONS`] of them.
99 : type Seen = Vec<(String, i64)>;
100 :
101 : /// Incarnations remembered per key: a delete and recreate burst longer than
102 : /// this forgets the oldest, whose next change is then admitted.
103 : const INCARNATIONS: usize = 4;
104 :
105 : impl NewestSeen {
106 : /// A map of at most `2 * cap` keys.
107 1292 : pub fn new(cap: usize) -> Self {
108 1292 : Self {
109 1292 : cap: cap.max(1),
110 1292 : gens: std::sync::Mutex::default(),
111 1292 : }
112 1292 : }
113 :
114 : /// `false` when a change of `(tenant, id)` newer than `v` was admitted;
115 : /// otherwise `v` becomes the newest and `true`. An equal version is
116 : /// admitted: a change seen twice is delivered twice, never lost.
117 : ///
118 : /// Versions are compared only within one incarnation, never across
119 : /// them. The incarnation is a wall-clock creation stamp: one recreated in
120 : /// the same millisecond, or by a pod whose clock runs behind, does not
121 : /// sort after the deleted one's. Nor does arrival order rank them: with
122 : /// A, C and B arriving in that order from hooks that overtook each
123 : /// other, any rule that drops a "superseded" incarnation ends up muting
124 : /// the live one. So a change is dropped only when its own incarnation
125 : /// already delivered a newer version (the delete leaves one past the
126 : /// row's, so a late update of a deleted incarnation whose delete was seen
127 : /// is dropped). A late change of another incarnation is admitted: at
128 : /// worst a stale image, never a lost one.
129 140958 : pub fn admit(&self, tenant: &str, id: &str, v: &ChangeVersion) -> bool {
130 : // a poisoned map is a panic between two plain map operations: the
131 : // map itself is whole
132 140958 : let mut gens = self
133 140958 : .gens
134 140958 : .lock()
135 140958 : .unwrap_or_else(std::sync::PoisonError::into_inner);
136 140958 : let key = (tenant.to_owned(), id.to_owned());
137 140958 : let mut seen: Seen = gens[0]
138 140958 : .get(&key)
139 140958 : .or_else(|| gens[1].get(&key))
140 140958 : .cloned()
141 140958 : .unwrap_or_default();
142 140958 : if let Some(i) = seen.iter().position(|(inc, _)| *inc == v.incarnation) {
143 18020 : if seen[i].1 > v.version {
144 16 : return false;
145 18004 : }
146 18004 : seen.remove(i);
147 122938 : } else if seen.len() >= INCARNATIONS {
148 120 : seen.remove(0);
149 122818 : }
150 140942 : seen.push((v.incarnation.clone(), v.version));
151 140942 : if gens[0].len() >= self.cap && !gens[0].contains_key(&key) {
152 12 : gens[1] = std::mem::take(&mut gens[0]);
153 140930 : }
154 140942 : gens[0].insert(key, seen);
155 140942 : true
156 140958 : }
157 : }
158 :
159 : /// Which resource family an operation touches.
160 : #[derive(Clone, Copy, Debug, PartialEq, Eq)]
161 : pub enum Kind {
162 : /// 5.2.4 Entity current-state documents.
163 : Entity,
164 : /// 5.2.12 Subscription documents.
165 : Subscription,
166 : /// 5.2.9 Context Source Registration documents.
167 : Registration,
168 : /// Context Source Registration Subscription documents (5.11).
169 : CSourceSubscription,
170 : /// Temporal Representation documents (5.2.5).
171 : Temporal,
172 : /// 5.16 Snapshot status documents (+ the internal synth-tenant index).
173 : Snapshot,
174 : /// 5.14 EntityMap API documents.
175 : EntityMap,
176 : /// 5.8.1.4 distributed-subscription mappings (remote ids per CSR).
177 : DistSub,
178 : /// Notifications a delivery policy gave up on (dead letters), kept
179 : /// under the subscription's tenant for replay.
180 : DeadLetter,
181 : /// Stored view documents under the reserved tenant `antares-views`,
182 : /// and the policy engine's rule documents (ADR-0020 amendment) under `antares-policy`.
183 : /// No NGSI-LD route reads or writes this kind.
184 : View,
185 : }
186 :
187 : /// The four store backends, decided ONCE at startup and threaded as a value —
188 : /// never re-derived from strings or runtime probes, so a section gated on the
189 : /// wrong mode is unrepresentable.
190 : #[derive(Clone, Copy, Debug, PartialEq, Eq, Default)]
191 : pub enum StoreMode {
192 : /// In-process maps; nothing survives a restart.
193 : #[default]
194 : Memory,
195 : /// Embedded single-file store on local disk.
196 : File,
197 : /// PostgreSQL with PostGIS.
198 : Postgres,
199 : /// PostgreSQL with PostGIS and TimescaleDB hypertables for history.
200 : Timescale,
201 : }
202 :
203 : impl StoreMode {
204 : /// Every backend this workspace knows, in the order a listing shows
205 : /// them. One source of truth: `FromStr`, the unknown-mode message and
206 : /// the broker's built-with shelf all read it, so a backend added to the
207 : /// enum cannot go missing from any of them.
208 : pub const ALL: [StoreMode; 4] = [
209 : StoreMode::Memory,
210 : StoreMode::File,
211 : StoreMode::Postgres,
212 : StoreMode::Timescale,
213 : ];
214 :
215 : /// The accepted mode names, `memory|file|postgres|timescale`.
216 4 : pub fn names() -> String {
217 4 : StoreMode::ALL
218 4 : .iter()
219 16 : .map(|m| m.as_str())
220 4 : .collect::<Vec<_>>()
221 4 : .join("|")
222 4 : }
223 :
224 : /// The mode name as accepted by `FromStr` (`memory|file|postgres|timescale`).
225 510 : pub fn as_str(self) -> &'static str {
226 510 : match self {
227 193 : StoreMode::Memory => "memory",
228 124 : StoreMode::File => "file",
229 108 : StoreMode::Postgres => "postgres",
230 85 : StoreMode::Timescale => "timescale",
231 : }
232 510 : }
233 : /// Shared-database modes — the only ones that can back multiple instances.
234 41 : pub fn is_pg(self) -> bool {
235 41 : matches!(self, StoreMode::Postgres | StoreMode::Timescale)
236 41 : }
237 : }
238 :
239 : impl std::str::FromStr for StoreMode {
240 : type Err = String;
241 123 : fn from_str(s: &str) -> Result<Self, Self::Err> {
242 123 : StoreMode::ALL
243 123 : .into_iter()
244 224 : .find(|m| m.as_str() == s)
245 123 : .ok_or_else(|| format!("unknown store mode {s} ({})", StoreMode::names()))
246 123 : }
247 : }
248 :
249 : impl std::fmt::Display for StoreMode {
250 6 : fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
251 6 : f.write_str(self.as_str())
252 6 : }
253 : }
254 :
255 : /// What one tenant holds, per current-state document kind.
256 : #[derive(Debug, Clone, Default, PartialEq, Eq)]
257 : pub struct TenantStats {
258 : /// The tenant name.
259 : pub tenant: String,
260 : /// When the tenant row was created, if the backend records it.
261 : pub created_at: Option<String>,
262 : /// 5.2.4 entities.
263 : pub entities: u64,
264 : /// 5.2.12 subscriptions.
265 : pub subscriptions: u64,
266 : /// 5.2.9 context source registrations.
267 : pub registrations: u64,
268 : /// Context source registration subscriptions.
269 : pub csource_subscriptions: u64,
270 : /// 5.16 snapshots.
271 : pub snapshots: u64,
272 : /// Entity maps (5.14).
273 : pub entity_maps: u64,
274 : /// Distributed subscriptions this tenant placed at other brokers.
275 : pub dist_subs: u64,
276 : }
277 :
278 : /// How many bytes one tenant's Entities take in the store, measured
279 : /// there and never estimated: the stored row on Postgres, the serialized
280 : /// document in memory.
281 : #[derive(Debug, Clone, Default, PartialEq, Eq)]
282 : pub struct TenantBytes {
283 : /// Every Entity of the tenant.
284 : pub entities: u64,
285 : /// The same bytes by the Entity's first type, so the values sum to
286 : /// `entities`.
287 : pub by_type: std::collections::BTreeMap<String, u64>,
288 : }
289 :
290 : /// The read-modify-write closure as the object-safe seam carries it. The
291 : /// typed `Result<T, E>` travels through [`CurrentStateDriverExt::mutate`]'s
292 : /// side slot; the boxed closure only signals commit (`Ok`) vs reject
293 : /// (`Err`) to the driver.
294 : pub type MutateFn<'a> = Box<dyn FnOnce(&mut Value) -> Result<(), ()> + Send + 'a>;
295 : /// Per-id variant for batch mutation; the driver calls it once per PRESENT
296 : /// id, in input order — the ext trait's error slot depends on that order.
297 : pub type BatchMutateFn<'a> = Box<dyn FnMut(&str, &mut Value) -> Result<(), ()> + Send + 'a>;
298 : /// The delivery stamp expressed as a `mutate`. This is the rule itself —
299 : /// what `timesSent`, `lastNotification`, `lastSuccess` and `status` become
300 : /// after one attempt (5.2.14.2) — so a backend that reimplements
301 : /// [`CurrentStateDriver::record_delivery`] in its own query language is
302 : /// reimplementing THIS, and the two must agree.
303 4800 : pub async fn record_delivery_via_mutate(
304 4800 : d: &(impl CurrentStateDriver + ?Sized),
305 4800 : tenant: &TenantId,
306 4800 : kind: Kind,
307 4800 : id: &str,
308 4800 : now: &str,
309 4800 : ) -> Result<Option<Delivery>, NgsiError> {
310 4800 : let mut out: Option<Delivery> = None;
311 4800 : d.mutate::<(), ()>(tenant, kind, id, |doc| {
312 4780 : let mut prev_success = None;
313 4780 : if let Some(o) = doc.as_object_mut() {
314 4780 : o.remove("status");
315 4780 : }
316 4780 : if let Some(n) = doc
317 4780 : .as_object_mut()
318 4780 : .and_then(|o| o.get_mut("notification"))
319 4780 : .and_then(Value::as_object_mut)
320 4764 : {
321 4764 : let sent = n.get("timesSent").and_then(Value::as_i64).unwrap_or(0);
322 4764 : n.insert("timesSent".into(), serde_json::json!(sent + 1));
323 4764 : n.insert("lastNotification".into(), Value::String(now.to_owned()));
324 4764 : prev_success = n.insert("lastSuccess".into(), Value::String(now.to_owned()));
325 4764 : n.insert("status".into(), Value::String("ok".into()));
326 4764 : }
327 4780 : out = Some(Delivery {
328 4780 : doc: doc.clone(),
329 4780 : prev_success,
330 4780 : });
331 4780 : Ok(())
332 4780 : })
333 4800 : .await?;
334 4800 : Ok(out)
335 4800 : }
336 :
337 : /// The forward stamp expressed as a `mutate`. This is the Table 5.2.9-2
338 : /// rule itself — what `timesSent`, `timesFailed`, `lastSuccess`,
339 : /// `lastFailure` and `status` become after one distributed operation — so a
340 : /// backend that reimplements [`CurrentStateDriver::record_forward`] in its
341 : /// own query language is reimplementing THIS, and the two must agree.
342 : ///
343 : /// `timesSent` moves on EVERY attempt: the table defines it as the number of
344 : /// times the registration "triggered a distributed operation, including
345 : /// failed attempts". A member is written when it first has meaning, which is
346 : /// what cardinality 0..1 and "created on first successful operation" ask for:
347 : /// a registration that has only ever succeeded carries no `timesFailed` and
348 : /// no `lastFailure`, and one that has only ever failed carries no
349 : /// `lastSuccess`. `status` always names the LAST attempt.
350 1404 : pub async fn record_forward_via_mutate(
351 1404 : d: &(impl CurrentStateDriver + ?Sized),
352 1404 : tenant: &TenantId,
353 1404 : id: &str,
354 1404 : now: &str,
355 1404 : ok: bool,
356 1404 : ) -> Result<Option<Value>, NgsiError> {
357 1404 : let mut out: Option<Value> = None;
358 1404 : d.mutate::<(), ()>(tenant, Kind::Registration, id, |doc| {
359 1378 : if let Some(o) = doc.as_object_mut() {
360 1378 : let sent = o.get("timesSent").and_then(Value::as_i64).unwrap_or(0);
361 1378 : o.insert("timesSent".into(), serde_json::json!(sent + 1));
362 1378 : if ok {
363 1082 : o.insert("lastSuccess".into(), Value::String(now.to_owned()));
364 1082 : o.insert("status".into(), Value::String("ok".into()));
365 1082 : } else {
366 296 : let failed = o.get("timesFailed").and_then(Value::as_i64).unwrap_or(0);
367 296 : o.insert("timesFailed".into(), serde_json::json!(failed + 1));
368 296 : o.insert("lastFailure".into(), Value::String(now.to_owned()));
369 296 : o.insert("status".into(), Value::String("failed".into()));
370 296 : }
371 0 : }
372 1378 : out = Some(doc.clone());
373 1378 : Ok(())
374 1378 : })
375 1404 : .await?;
376 1404 : Ok(out)
377 1404 : }
378 :
379 : /// What one delivery attempt wrote: the stored subscription as it now
380 : /// stands (the mirror is fed from it) and the `lastSuccess` that was there
381 : /// before, which a failed attempt puts back.
382 : #[derive(Debug, Clone)]
383 : pub struct Delivery {
384 : /// The subscription as it now stands, the shape the mirror is fed.
385 : pub doc: Value,
386 : /// The `notification.lastSuccess` this attempt overwrote, absent when
387 : /// the subscription had never succeeded.
388 : pub prev_success: Option<Value>,
389 : }
390 :
391 : /// ADR-0021: the Tenant a stored `@context` row belongs to, or `None` when it
392 : /// belongs to none.
393 : ///
394 : /// 5.13.1 "Cached" is a copy of a public document the broker downloaded for
395 : /// whoever named its URL, so every Tenant reaches it. "Hosted" and
396 : /// "ImplicitlyCreated" are documents a client stored THROUGH a Tenant, and
397 : /// 5.5.7 makes their term mappings decide what that Tenant's payloads mean;
398 : /// the owner travels in the row. A row written before the member existed
399 : /// reads as the default Tenant's, and so does a row whose `kind` is missing
400 : /// altogether: no writer produces one, and an unrecognised row is safer read
401 : /// as somebody's than as everybody's.
402 41036 : pub fn context_row_owner(row: &Value) -> Option<&str> {
403 41036 : (row["kind"].as_str() != Some("Cached"))
404 41036 : .then(|| row["owner"].as_str().unwrap_or(TenantId::DEFAULT))
405 41036 : }
406 :
407 : /// Whether a call acting for `tenant` — `None` for no Tenant at all — reaches
408 : /// the row. The rule [`context_row_owner`] states, in the form the stores and
409 : /// the API both need.
410 40962 : pub fn context_row_visible(row: &Value, tenant: Option<&TenantId>) -> bool {
411 40962 : match context_row_owner(row) {
412 38464 : None => true,
413 2498 : Some(owner) => tenant.is_some_and(|t| t.as_str() == owner),
414 : }
415 40962 : }
416 :
417 : /// Current-state storage: everything except the temporal evolution.
418 : ///
419 : /// Contract carried over from the enum seam it replaces: every mutate is
420 : /// one transaction under the row lock, and a missing row is `None`, never
421 : /// an insert (a bookkeeping writeback racing a DELETE must not resurrect
422 : /// the row). Backends map their internal failures to
423 : /// `NgsiError::InternalError` with a GENERIC client-visible detail.
424 : #[async_trait::async_trait]
425 : pub trait CurrentStateDriver: Send + Sync {
426 : /// Readiness RIGHT NOW — a lost database flips /q/ready to 503.
427 : async fn ping(&self) -> Result<(), NgsiError>;
428 : /// (Queued writers, peak) of a single-writer commit section, if the
429 : /// backend has one.
430 0 : fn commit_queue(&self) -> Option<(usize, usize)> {
431 0 : None
432 0 : }
433 : /// Drain: finish in-flight work and disconnect cleanly.
434 : async fn close(&self);
435 : /// What this driver runs on, for `/q/health`: engine, server version,
436 : /// extensions — whatever an operator needs to tell two deployments of
437 : /// the same backend name apart. Read from state captured at startup,
438 : /// never by querying on the request: health is polled. An empty object
439 : /// (the default) means the driver has nothing to add to its name.
440 0 : fn version_info(&self) -> Value {
441 0 : Value::Object(serde_json::Map::new())
442 0 : }
443 : /// Installs the (tenant, before, after) hook called on every entity write.
444 : fn set_change_hook(&self, h: ChangeHook);
445 : /// Turn the same-transaction outbox producer on (bus=nats).
446 0 : fn set_outbox(&self, on: bool) {
447 0 : let _ = on;
448 0 : }
449 : /// Outbox drain: oldest-first page of pending rows `(seq, tenant, event)`.
450 0 : async fn outbox_peek(&self, limit: i64) -> Result<Vec<(i64, String, Value)>, NgsiError> {
451 : let _ = limit;
452 : Ok(Vec::new())
453 0 : }
454 : /// Outbox drain: delete EXACTLY the published rows.
455 0 : async fn outbox_ack(&self, seqs: &[i64]) -> Result<u64, NgsiError> {
456 0 : let _ = seqs;
457 : Ok(0)
458 0 : }
459 : /// Outbox drain: keep the rows of events the bus could not carry whole
460 : /// and take them out of the drain's page. The published message holds a
461 : /// claim-check reference; the row holds the bodies it stands for.
462 0 : async fn outbox_retain(&self, tenant: &TenantId, seqs: &[i64]) -> Result<u64, NgsiError> {
463 : let _ = (tenant, seqs);
464 : Ok(0)
465 0 : }
466 : /// The whole event behind a claim-check reference, or `None` once it has
467 : /// been reaped. Scoped to the tenant that wrote it.
468 0 : async fn outbox_event(&self, seq: i64, tenant: &TenantId) -> Result<Option<Value>, NgsiError> {
469 : let _ = (seq, tenant);
470 : Ok(None)
471 0 : }
472 :
473 : /// Insert a document; `false` if the id already exists (nothing written).
474 : async fn create(
475 : &self,
476 : tenant: &TenantId,
477 : kind: Kind,
478 : id: &str,
479 : doc: Value,
480 : ) -> Result<bool, NgsiError>;
481 : /// Batch create (entities only); created-flags in input order.
482 : async fn batch_create(
483 : &self,
484 : tenant: &TenantId,
485 : items: Vec<(String, Value)>,
486 : ) -> Result<Vec<bool>, NgsiError>;
487 : /// Batch delete (entities only); deleted-flags in input order, a
488 : /// duplicate id deletes once and reads absent the second time.
489 : async fn batch_delete(&self, tenant: &TenantId, ids: &[String])
490 : -> Result<Vec<bool>, NgsiError>;
491 : /// Batch upsert with REPLACE semantics (entities only); created-flags in
492 : /// input order.
493 : async fn batch_upsert(
494 : &self,
495 : tenant: &TenantId,
496 : items: Vec<(String, Value)>,
497 : ) -> Result<Vec<bool>, NgsiError>;
498 : /// Insert or replace a document. `true` means a document was ALREADY
499 : /// there and this call replaced it; `false` means this call created it.
500 : /// The polarity is the opposite of [`Self::batch_upsert`], which answers
501 : /// created-flags — the batch path needs them to split 201 from 204
502 : /// (5.6.8) while the single path ignores the value.
503 : async fn upsert(
504 : &self,
505 : tenant: &TenantId,
506 : kind: Kind,
507 : id: &str,
508 : doc: Value,
509 : ) -> Result<bool, NgsiError>;
510 : /// Read one document; `None` if absent.
511 : async fn get(
512 : &self,
513 : tenant: &TenantId,
514 : kind: Kind,
515 : id: &str,
516 : ) -> Result<Option<Value>, NgsiError>;
517 : /// Several Entities at once, keyed by id; an absent one has no entry. A
518 : /// backend that can read them together overrides this (Postgres: one
519 : /// transaction); the default reads them one by one.
520 : async fn get_entities(
521 : &self,
522 : tenant: &TenantId,
523 : ids: &[&str],
524 14 : ) -> Result<std::collections::HashMap<String, Value>, NgsiError> {
525 : let mut found = std::collections::HashMap::with_capacity(ids.len());
526 : for id in ids {
527 : if let Some(doc) = self.get(tenant, Kind::Entity, id).await? {
528 : found.insert((*id).to_owned(), doc);
529 : }
530 : }
531 : Ok(found)
532 14 : }
533 : /// Delete one document; `false` if it was absent.
534 : async fn delete(&self, tenant: &TenantId, kind: Kind, id: &str) -> Result<bool, NgsiError>;
535 : /// Delete one Entity, but only if `keep` accepts the stored document.
536 : /// `false` = absent OR refused; the caller cannot tell the two apart and
537 : /// does not need to, because 5.6.6.4 gives the same answer to both: an
538 : /// Entity the selector excludes "is not known" for this operation.
539 : ///
540 : /// The read and the delete happen under one lock. Required, deliberately:
541 : /// the obvious default is `get` then `delete`, and that pair is the race
542 : /// this method exists to remove — between the two the Entity can be
543 : /// deleted and recreated under the same id, and the delete then lands on
544 : /// a document the caller never inspected. A backend inheriting a default
545 : /// would keep the race with no compile error to say so.
546 : async fn delete_entity_if(
547 : &self,
548 : tenant: &TenantId,
549 : id: &str,
550 : keep: &(dyn for<'v> Fn(&'v Value) -> bool + Sync),
551 : ) -> Result<bool, NgsiError>;
552 : /// Every document of this kind in the tenant.
553 : ///
554 : /// A backend may refuse a tenant that holds too many to materialize
555 : /// (5.5.6 TooManyResults). That is right for a client query and wrong
556 : /// for a reader that must see ALL of them — use [`Self::list_page`].
557 : async fn list(&self, tenant: &TenantId, kind: Kind) -> Result<Vec<Value>, NgsiError>;
558 : /// Every document of this kind in the tenant as its id and a digest of
559 : /// its body, for a reader that holds the bodies already and fetches only
560 : /// the ones whose digest changed. Two equal bodies have one
561 : /// digest on one backend; nothing else is promised of it. Refused as
562 : /// [`Self::list`] is. A backend that digests without reading the
563 : /// documents out overrides this; the default reads them, which is
564 : /// correct and no cheaper.
565 : async fn list_digests(
566 : &self,
567 : tenant: &TenantId,
568 : kind: Kind,
569 0 : ) -> Result<Vec<(String, u64)>, NgsiError> {
570 : Ok(digests(self.list(tenant, kind).await?))
571 0 : }
572 : /// One id-ordered page of documents, for the internal readers that must
573 : /// see every one of them and so cannot be refused: ids strictly greater
574 : /// than `after`, at most `limit`. A short page means the end.
575 : ///
576 : /// Keyset, not offset: the caller walks a tenant that is being written
577 : /// to underneath it, where OFFSET skips and repeats rows. The peak cost
578 : /// is one page, not the whole tenant, which is what the row ceiling on
579 : /// `list` was protecting — so this carries no ceiling of its own, and a
580 : /// backend may not refuse it for volume.
581 : ///
582 : /// Required, deliberately: the obvious default is `list` sliced, and
583 : /// `list` is the read that may refuse. A backend inheriting that would
584 : /// silently reacquire the outage this method exists to prevent, with no
585 : /// compile error to say so.
586 : async fn list_page(
587 : &self,
588 : tenant: &TenantId,
589 : kind: Kind,
590 : after: Option<&str>,
591 : limit: usize,
592 : ) -> Result<Vec<Value>, NgsiError>;
593 : /// One id-ordered window of documents and the size of the whole set:
594 : /// elements `offset..offset + limit`, plus the count 6.3.10's `count`
595 : /// parameter reports. `limit` 0 is a legal request for the count alone.
596 : ///
597 : /// 5.5.9.1: "the query resolution mechanisms of the NGSI-LD System shall
598 : /// ensure that only up to a maximum of L NGSI-LD Elements are RETRIEVED
599 : /// and returned to the NGSI-LD client". Reading a whole tenant to serve
600 : /// one page of it is what that sentence rules out, and it is why this
601 : /// carries no row ceiling: the window bounds the result by construction,
602 : /// so the only thing a ceiling could refuse is a page the client is
603 : /// entitled to.
604 : ///
605 : /// Offset, not the keyset of [`Self::list_page`], because this serves
606 : /// 5.5.9.2's `limit`/`offset`, which lets a client "jump to a desired
607 : /// set of elements". A cursor cannot answer that; the two reads exist
608 : /// for different callers and neither replaces the other.
609 : ///
610 : /// Required for the same reason `list_page` is: the obvious default
611 : /// slices `list`, and `list` is the read that may refuse.
612 : async fn list_slice(
613 : &self,
614 : tenant: &TenantId,
615 : kind: Kind,
616 : offset: usize,
617 : limit: usize,
618 : ) -> Result<(Vec<Value>, usize), NgsiError>;
619 : /// Registrations that can match these ids/types (a backend may narrow;
620 : /// returning the full tenant list is always correct).
621 : async fn matching_registrations(
622 : &self,
623 : tenant: &TenantId,
624 : ids: Option<&[String]>,
625 : types: Option<&[String]>,
626 : ) -> Result<Vec<Value>, NgsiError>;
627 : /// Query Entities with the filter pushed down where the backend can
628 : /// take it; the caller re-checks unless the outcome says `decided`.
629 : async fn query_entities(
630 : &self,
631 : tenant: &TenantId,
632 : f: &filter::EntityFilter<'_>,
633 : ) -> Result<filter::QueryOutcome, NgsiError>;
634 : /// `query_entities` for a caller that re-decides every row and walks the
635 : /// match set in id-ordered pages: the backend may push `f.page` over the
636 : /// candidates it cannot decide (`paged` without `decided`), and never
637 : /// counts them. The default reads as `query_entities` does.
638 : async fn query_candidates(
639 : &self,
640 : tenant: &TenantId,
641 : f: &filter::EntityFilter<'_>,
642 0 : ) -> Result<filter::QueryOutcome, NgsiError> {
643 : self.query_entities(tenant, f).await
644 0 : }
645 : /// The Entity types the Tenant's live Entities carry (5.7.5), sorted,
646 : /// read without the documents: `/types` names them, and folding every
647 : /// Entity for the names read a whole Tenant per call. `None`
648 : /// when the backend has no such read, and the caller folds instead.
649 0 : async fn entity_type_names(&self, tenant: &TenantId) -> Result<Option<Vec<String>>, NgsiError> {
650 0 : let _ = tenant;
651 : Ok(None)
652 0 : }
653 : /// The types the Tenant's live Entities of two or more types carry when
654 : /// they also carry one of `any_of`, sorted: a view's rule names
655 : /// its types, and a type the rule does not name is shown only on an
656 : /// Entity of both. An Entity of `any_of` alone adds nothing. `None` when
657 : /// the backend has no such read, and the caller probes every type.
658 : async fn entity_types_beside(
659 : &self,
660 : tenant: &TenantId,
661 : any_of: &[String],
662 14 : ) -> Result<Option<Vec<String>>, NgsiError> {
663 : let _ = (tenant, any_of);
664 : Ok(None)
665 14 : }
666 : /// Each distinct `lead + s` the Tenant's ids start with, `s` being the
667 : /// text after `lead` up to the first `sep` (an id without one stands
668 : /// for itself), sorted by bytes: with
669 : /// [`filter::id_pattern_segments`] they turn an id pattern into exact
670 : /// prefixes. `None` when the backend has no such read or more than
671 : /// `max` exist.
672 : async fn entity_id_segments(
673 : &self,
674 : tenant: &TenantId,
675 : lead: &str,
676 : sep: char,
677 : max: usize,
678 0 : ) -> Result<Option<Vec<String>>, NgsiError> {
679 : let _ = (tenant, lead, sep, max);
680 : Ok(None)
681 0 : }
682 : /// The (type, member name) pairs the Tenant's Entities carry, sorted:
683 : /// `/types` with details and `/attributes` probe these instead
684 : /// of folding every Entity. The list may name a pair no live Entity
685 : /// carries any more, never miss one a live Entity carries; member names
686 : /// include the Entity frame's. `None` when the backend keeps no such
687 : /// list, and the caller folds instead.
688 : async fn entity_attr_pairs(
689 : &self,
690 : tenant: &TenantId,
691 2 : ) -> Result<Option<Vec<(String, String)>>, NgsiError> {
692 2 : let _ = tenant;
693 : Ok(None)
694 2 : }
695 : /// Read-modify-write under the row lock; `None` = absent (never an
696 : /// insert), `Some(Err)` = the closure rejected, nothing committed.
697 : async fn mutate_boxed<'a>(
698 : &self,
699 : tenant: &TenantId,
700 : kind: Kind,
701 : id: &str,
702 : f: MutateFn<'a>,
703 : ) -> Result<Option<Result<(), ()>>, NgsiError>;
704 : /// Batch read-modify-write (entities only); results align with `ids`.
705 : async fn batch_mutate_boxed<'a>(
706 : &self,
707 : tenant: &TenantId,
708 : ids: &[String],
709 : f: BatchMutateFn<'a>,
710 : ) -> Result<Vec<Option<Result<(), ()>>>, NgsiError>;
711 : /// 5.2.14.2 delivery bookkeeping: stamp one delivery attempt on a
712 : /// subscription and hand back the stored document. `timesSent` moves by
713 : /// one, `lastNotification` and `lastSuccess` take `now`, and the previous
714 : /// `lastSuccess` comes back so a failed attempt can roll it back.
715 : /// `None` means the row is gone — the subscription was deleted between
716 : /// matching and delivery, and nothing may be sent (5.8.6).
717 : ///
718 : /// The default expresses it as a `mutate`, which is correct everywhere.
719 : /// A backend whose `mutate` locks the row across a network round trip
720 : /// should override it with one statement: at fan-out every delivery on
721 : /// one subscription contends for that row, so the lock hold time — not
722 : /// the statement count — is what serializes delivery.
723 : async fn record_delivery(
724 : &self,
725 : tenant: &TenantId,
726 : kind: Kind,
727 : id: &str,
728 : now: &str,
729 0 : ) -> Result<Option<Delivery>, NgsiError> {
730 : record_delivery_via_mutate(self, tenant, kind, id, now).await
731 0 : }
732 : /// [`Self::record_delivery`] for several attempts at once, one answer per
733 : /// attempt in input order. The ids are distinct: an attempt books exactly
734 : /// one notification and its pre-image is the value it overwrote. The
735 : /// default books them one by one; a backend that can should write them
736 : /// as one statement, which is what keeps a fan-out of thousands of
737 : /// in-process deliveries at one round trip per batch.
738 : async fn record_deliveries(
739 : &self,
740 : tenant: &TenantId,
741 : kind: Kind,
742 : attempts: &[(&str, &str)],
743 4 : ) -> Result<Vec<Option<Delivery>>, NgsiError> {
744 : let mut out = Vec::with_capacity(attempts.len());
745 : for (id, now) in attempts {
746 : out.push(self.record_delivery(tenant, kind, id, now).await?);
747 : }
748 : Ok(out)
749 4 : }
750 : /// Reap expired docs/instances (backends with their own maintenance job
751 : /// return 0).
752 0 : async fn sweep_expired(&self) -> usize {
753 : 0
754 0 : }
755 : /// Retention of one Entity type in one Tenant (`/q/retention`):
756 : /// delete at most `batch` Entities whose types include `type_iri`, that
757 : /// carry no `expiresAt` of their own (a writer's 4.22 stamp wins) and
758 : /// were last modified before `before`, a UTC DateTime. Returns how many
759 : /// were deleted, or with `dry_run` would be, deleting nothing; a driver
760 : /// without current state has none.
761 : async fn retain_current(
762 : &self,
763 : tenant: &TenantId,
764 : type_iri: &str,
765 : before: &str,
766 : batch: usize,
767 : dry_run: bool,
768 0 : ) -> Result<u64, NgsiError> {
769 : let _ = (tenant, type_iri, before, batch, dry_run);
770 : Ok(0)
771 0 : }
772 : /// The Entities, across every Tenant, that carry a [`HISTORY_RETENTION`]
773 : /// Property: at most `limit` of (tenant, id, the Property as
774 : /// stored). Cross-tenant maintenance, like the 4.22 reap; a driver
775 : /// without current state has none.
776 : async fn history_overrides(
777 : &self,
778 : limit: usize,
779 0 : ) -> Result<Vec<(String, String, Value)>, NgsiError> {
780 : let _ = limit;
781 : Ok(Vec::new())
782 0 : }
783 : /// Table 5.2.9-2 forward bookkeeping: stamp one distributed operation on
784 : /// a registration and hand back the stored document. `None` means the row
785 : /// is gone — the registration was deleted while its forward was in
786 : /// flight, and a bookkeeping writeback must never resurrect it.
787 : ///
788 : /// The default expresses it as a `mutate`, which is correct everywhere.
789 : // ponytail: one read-modify-write per forward, so a registration every
790 : // request fans out to is one contended row; a backend whose `mutate`
791 : // locks that row across a network round trip can collapse it into one
792 : // statement the way `record_delivery` does, once a profile asks.
793 : async fn record_forward(
794 : &self,
795 : tenant: &TenantId,
796 : id: &str,
797 : now: &str,
798 : ok: bool,
799 1404 : ) -> Result<Option<Value>, NgsiError> {
800 : record_forward_via_mutate(self, tenant, id, now, ok).await
801 1404 : }
802 : /// 5.5.10: the default Tenant implicitly exists; others once created.
803 : async fn tenant_exists(&self, tenant: &TenantId) -> Result<bool, NgsiError>;
804 : /// The iteration domain of the interval sweep and of every mirror
805 : /// hydration: a tenant holding a Subscription, a Context Source
806 : /// Registration Subscription OR a Registration SHALL appear. A superset
807 : /// is allowed — the callers list per tenant afterwards and an empty list
808 : /// costs nothing — so a backend that cannot narrow the set cheaply may
809 : /// return every tenant it knows. A SUBSET is a silent outage: a tenant
810 : /// missing here never fires a periodic notification and never reaches
811 : /// the mirror.
812 : ///
813 : /// Registrations are in the domain because one of the hydrations fills
814 : /// the REGISTRATION mirror, and the federation path reads that mirror
815 : /// alone whenever it is installed. A domain that stopped at
816 : /// subscription-holding tenants left a tenant with registrations and no
817 : /// subscription forwarding to no Context Source at all.
818 : async fn subscription_tenants(&self) -> Result<Vec<String>, NgsiError>;
819 : /// Every tenant the backend knows, sorted. The default Tenant is listed
820 : /// even when empty (5.5.10: it always exists). Names only: at the
821 : /// 10 000-tenant target (ADR-0001) an inventory carrying per-kind counts
822 : /// would cost a count per kind per tenant, so the counts are paid per
823 : /// lookup in `tenant_stats_one`.
824 0 : async fn tenant_ids(&self) -> Result<Vec<String>, NgsiError> {
825 : Err(NgsiError::OperationNotSupported("tenant inventory".into()))
826 0 : }
827 : /// What one tenant holds; `None` when it does not exist.
828 0 : async fn tenant_stats_one(&self, tenant: &TenantId) -> Result<Option<TenantStats>, NgsiError> {
829 0 : let _ = tenant;
830 : Err(NgsiError::OperationNotSupported("tenant inventory".into()))
831 0 : }
832 : /// The bytes one tenant's Entities take; a scan of the tenant, so the
833 : /// caller caches it.
834 0 : async fn tenant_bytes(&self, tenant: &TenantId) -> Result<TenantBytes, NgsiError> {
835 0 : let _ = tenant;
836 : Err(NgsiError::OperationNotSupported("tenant size".into()))
837 0 : }
838 : /// Remove every current-state document of one tenant; `false` when the
839 : /// tenant did not exist. The default Tenant is emptied but keeps
840 : /// existing.
841 0 : async fn purge_tenant(&self, tenant: &TenantId) -> Result<bool, NgsiError> {
842 0 : let _ = tenant;
843 : Err(NgsiError::OperationNotSupported("tenant purge".into()))
844 0 : }
845 : /// 5.13 @context documents. `tenant` is the Tenant the call acts for, and
846 : /// `None` means no Tenant at all — the boot warm and the `Cached`
847 : /// write-through, which touch documents downloaded from public URLs.
848 : ///
849 : /// A backend enforces ADR-0021 on it: a `Cached` row belongs to no Tenant
850 : /// and is reachable with any `tenant`, and every other kind is reachable
851 : /// only by the Tenant named in its `owner` member. A backend that ignores
852 : /// the parameter fails the driver contract.
853 : async fn context_put(
854 : &self,
855 : tenant: Option<&TenantId>,
856 : id: &str,
857 : doc: Value,
858 : ) -> Result<(), NgsiError>;
859 : /// Read a stored @context document by id; `None` if absent or owned by
860 : /// another Tenant.
861 : async fn context_get(
862 : &self,
863 : tenant: Option<&TenantId>,
864 : id: &str,
865 : ) -> Result<Option<Value>, NgsiError>;
866 : /// Read-modify-write of one stored @context document under its row lock:
867 : /// `None` = absent or owned by another Tenant (never an insert),
868 : /// `Some(Err)` = the closure rejected and nothing was committed. A usage
869 : /// bump or a refetch racing a delete must leave the row deleted, and two
870 : /// racing bumps must both count (5.13.3.5).
871 : async fn context_mutate<'a>(
872 : &self,
873 : tenant: Option<&TenantId>,
874 : id: &str,
875 : f: MutateFn<'a>,
876 : ) -> Result<Option<Result<(), ()>>, NgsiError>;
877 : /// Delete a stored @context document; `false` if it was absent or owned
878 : /// by another Tenant.
879 : async fn context_delete(&self, tenant: Option<&TenantId>, id: &str) -> Result<bool, NgsiError>;
880 : /// Every stored @context document.
881 : /// Every stored `@context` row WITHOUT its `body` member — the url,
882 : /// localId, kind, owner and usage counters, and not the document.
883 : ///
884 : /// There is deliberately no read that returns every row WITH its body.
885 : /// A body is accepted up to `MAX_CONTEXT_BYTES` (5 MiB) and only the
886 : /// `Cached` rows are capped in number, so one such read materializes
887 : /// gigabytes — on the boot path, where it decides whether the broker
888 : /// starts at all. The callers that need one body ask for it by id
889 : /// ([`Self::context_get`]); nothing needs them all at once.
890 : async fn context_list_meta(&self, tenant: Option<&TenantId>) -> Result<Vec<Value>, NgsiError>;
891 : }
892 :
893 : /// The typed-slot lock, taken and released inside one closure call. A
894 : /// poisoned slot is read through: the panic that poisoned it is already on
895 : /// its way out of the driver, and the value behind it is whatever the
896 : /// closure managed to write.
897 22118 : fn lock<T>(m: &std::sync::Mutex<T>) -> std::sync::MutexGuard<'_, T> {
898 22118 : m.lock().unwrap_or_else(|p| p.into_inner())
899 22118 : }
900 :
901 : /// The same, for taking the slot's value once the driver has answered.
902 22332 : fn into_inner<T>(m: std::sync::Mutex<T>) -> T {
903 22332 : m.into_inner().unwrap_or_else(|p| p.into_inner())
904 22332 : }
905 :
906 : /// Typed sugar over the boxed mutate seam — call sites keep their
907 : /// `Result<T, E>` closures; the value crosses the object boundary in a
908 : /// side slot.
909 : pub trait CurrentStateDriverExt {
910 : /// Typed read-modify-write: `None` = absent, `Some(Err(e))` = the closure
911 : /// rejected and nothing was committed.
912 : fn mutate<T: Send, E: Send>(
913 : &self,
914 : tenant: &TenantId,
915 : kind: Kind,
916 : id: &str,
917 : f: impl FnOnce(&mut Value) -> Result<T, E> + Send,
918 : ) -> impl std::future::Future<Output = Result<Option<Result<T, E>>, NgsiError>> + Send;
919 : /// Typed batch read-modify-write (entities only); results align with `ids`.
920 : fn batch_mutate<E: Send>(
921 : &self,
922 : tenant: &TenantId,
923 : ids: &[String],
924 : f: impl FnMut(&str, &mut Value) -> Result<(), E> + Send,
925 : ) -> impl std::future::Future<Output = Result<Vec<Option<Result<(), E>>>, NgsiError>> + Send;
926 : }
927 :
928 : impl<S: CurrentStateDriver + ?Sized> CurrentStateDriverExt for S {
929 24982 : async fn mutate<T: Send, E: Send>(
930 24982 : &self,
931 24982 : tenant: &TenantId,
932 24982 : kind: Kind,
933 24982 : id: &str,
934 24982 : f: impl FnOnce(&mut Value) -> Result<T, E> + Send,
935 24982 : ) -> Result<Option<Result<T, E>>, NgsiError> {
936 24982 : let slot = std::sync::Mutex::new(None);
937 24982 : let r = self
938 24982 : .mutate_boxed(
939 24982 : tenant,
940 24982 : kind,
941 24982 : id,
942 24982 : Box::new(|v| {
943 21364 : let r = f(v);
944 21364 : let flag = if r.is_ok() { Ok(()) } else { Err(()) };
945 21364 : *lock(&slot) = Some(r);
946 21364 : flag
947 21364 : }),
948 : )
949 24982 : .await?;
950 24982 : Ok(r.and_then(|_| into_inner(slot)))
951 24982 : }
952 :
953 236 : async fn batch_mutate<E: Send>(
954 236 : &self,
955 236 : tenant: &TenantId,
956 236 : ids: &[String],
957 236 : mut f: impl FnMut(&str, &mut Value) -> Result<(), E> + Send,
958 236 : ) -> Result<Vec<Option<Result<(), E>>>, NgsiError> {
959 : // Errors land in the queue in closure-call order, which the trait
960 : // contract fixes to input order over present ids.
961 236 : let errs = std::sync::Mutex::new(std::collections::VecDeque::new());
962 236 : let r = self
963 236 : .batch_mutate_boxed(
964 236 : tenant,
965 236 : ids,
966 8324 : Box::new(|id, v| match f(id, v) {
967 8306 : Ok(()) => Ok(()),
968 18 : Err(e) => {
969 18 : lock(&errs).push_back(e);
970 18 : Err(())
971 : }
972 8324 : }),
973 : )
974 236 : .await?;
975 236 : let mut errs = into_inner(errs);
976 236 : let mut out = Vec::with_capacity(r.len());
977 8382 : for slot in r {
978 8382 : out.push(match slot {
979 58 : None => None,
980 8306 : Some(Ok(())) => Some(Ok(())),
981 : // one queued error per rejected id is the trait contract; a
982 : // driver that reports more rejections than the closure raised
983 : // has broken it, and the batch fails rather than inventing an
984 : // error for the caller to read.
985 18 : Some(Err(())) => Some(Err(errs.pop_front().ok_or_else(|| {
986 0 : NgsiError::InternalError(
987 0 : "batch driver reported more rejections than were raised".into(),
988 0 : )
989 0 : })?)),
990 : });
991 : }
992 236 : Ok(out)
993 236 : }
994 : }
995 :
996 : /// What a temporal event records.
997 : #[derive(Clone, Copy, Debug, PartialEq, Eq)]
998 : pub enum TemporalOp {
999 : /// An Attribute the entity did not carry before (one event per instance).
1000 : AttrCreated,
1001 : /// A changed instance of an existing Attribute (4.5.6 append).
1002 : AttrModified,
1003 : /// 4.5.6: the Scope changed through the Core API — recorded as a scope
1004 : /// instance whose observedAt copies modifiedAt.
1005 : ScopeChanged,
1006 : }
1007 :
1008 : /// One change the write path hands to the temporal seam. Events are
1009 : /// produced per attribute INSTANCE (the gate chain and a columnar writer
1010 : /// both work per instance) and drained per request, in order.
1011 : #[derive(Clone, Debug)]
1012 : pub struct TemporalEvent {
1013 : /// What the event records.
1014 : pub op: TemporalOp,
1015 : /// Owning tenant.
1016 : pub tenant: TenantId,
1017 : /// Id of the entity whose history this instance belongs to.
1018 : pub entity_id: String,
1019 : /// The entity's meta shell (id, type, createdAt, modifiedAt, scope) as
1020 : /// it stood after the write — what `temporal_append` creates on first
1021 : /// touch.
1022 : pub shell: Value,
1023 : /// Expanded Attribute name, or `scope`.
1024 : pub attr: String,
1025 : /// The instance snapshot: value, datasetId, observedAt, instanceId…
1026 : pub instance: Value,
1027 : }
1028 :
1029 : /// [`TemporalDriver::event_list`]'s default: consecutive events of one Entity
1030 : /// become one `temporal_append`; a scope change goes through `mutate`, in
1031 : /// order. A driver that overrides `event_list` falls back to it.
1032 : ///
1033 : /// An Entity run the driver refuses (a memory/file history at its cap)
1034 : /// loses its own events and no other's: the drain goes on, and answers the
1035 : /// first refusal once the whole request is through.
1036 121600 : pub async fn drain_events<D: TemporalDriver + ?Sized>(
1037 121600 : d: &D,
1038 121600 : evs: &[TemporalEvent],
1039 121600 : ) -> Result<(), NgsiError> {
1040 121600 : let mut first = None;
1041 121600 : let mut i = 0;
1042 243462 : while i < evs.len() {
1043 121862 : let (tenant, id) = (&evs[i].tenant, evs[i].entity_id.as_str());
1044 121862 : let end = i + evs[i..]
1045 121862 : .iter()
1046 127304 : .take_while(|e| e.tenant == *tenant && e.entity_id == id)
1047 121862 : .count();
1048 121862 : if let Err(e) = drain_run(d, &evs[i..end]).await {
1049 4 : first.get_or_insert(e);
1050 121858 : }
1051 121862 : i = end;
1052 : }
1053 121600 : first.map_or(Ok(()), Err)
1054 121600 : }
1055 :
1056 : /// One Entity's consecutive events of [`drain_events`].
1057 121862 : async fn drain_run<D: TemporalDriver + ?Sized>(
1058 121862 : d: &D,
1059 121862 : run: &[TemporalEvent],
1060 121862 : ) -> Result<(), NgsiError> {
1061 121862 : let Some(head) = run.first() else {
1062 0 : return Ok(());
1063 : };
1064 121862 : let (tenant, id) = (&head.tenant, head.entity_id.as_str());
1065 121862 : let mut additions = serde_json::Map::new();
1066 121862 : let mut shell = &head.shell;
1067 127042 : for ev in run {
1068 127042 : shell = &ev.shell;
1069 127042 : if ev.op == TemporalOp::ScopeChanged {
1070 : // the additions before it first: they may create the doc the
1071 : // scope instance goes on, and history keeps production order
1072 54 : if !additions.is_empty() {
1073 2 : let pending = Value::Object(std::mem::take(&mut additions));
1074 2 : d.temporal_append(tenant, id, shell, &pending).await?;
1075 52 : }
1076 54 : if push_scope(d, tenant, id, &ev.instance).await?.is_none() {
1077 : // no temporal doc yet: create the shell, then record
1078 26 : let empty = Value::Object(serde_json::Map::new());
1079 26 : d.temporal_append(tenant, id, shell, &empty).await?;
1080 26 : push_scope(d, tenant, id, &ev.instance).await?;
1081 28 : }
1082 126988 : } else if let Some(arr) = additions
1083 126988 : .entry(ev.attr.clone())
1084 126988 : .or_insert_with(|| Value::Array(Vec::new()))
1085 126988 : .as_array_mut()
1086 126988 : {
1087 126988 : arr.push(ev.instance.clone());
1088 126988 : }
1089 : }
1090 121862 : if !additions.is_empty() {
1091 121826 : d.temporal_append(tenant, id, shell, &Value::Object(additions))
1092 121826 : .await?;
1093 36 : }
1094 121858 : Ok(())
1095 121862 : }
1096 :
1097 : /// Appends a scope instance to the held temporal doc; `None` when there is
1098 : /// no doc.
1099 80 : async fn push_scope<D: TemporalDriver + ?Sized>(
1100 80 : d: &D,
1101 80 : tenant: &TenantId,
1102 80 : id: &str,
1103 80 : inst: &Value,
1104 80 : ) -> Result<Option<Result<(), ()>>, NgsiError> {
1105 80 : d.mutate(tenant, id, |doc| {
1106 54 : let target = doc.as_object_mut().ok_or(())?;
1107 54 : match target.get_mut("scope").and_then(Value::as_array_mut) {
1108 32 : Some(arr) if arr.first().is_some_and(Value::is_object) => {
1109 2 : arr.push(inst.clone());
1110 2 : }
1111 52 : _ => {
1112 52 : target.insert("scope".into(), Value::Array(vec![inst.clone()]));
1113 52 : }
1114 : }
1115 54 : Ok::<(), ()>(())
1116 54 : })
1117 80 : .await
1118 80 : }
1119 :
1120 : /// A drained request folded into one append per run of consecutive events of
1121 : /// one Entity, as [`TemporalDriver::event_list`]'s default folds it: tenant,
1122 : /// id, the last shell, the additions by Attribute. `None` when an event
1123 : /// changes scope, which goes through `mutate` in order with the appends
1124 : /// (a driver that writes several appends at once takes this).
1125 123512 : pub fn fold_appends(evs: &[TemporalEvent]) -> Option<Vec<(&TenantId, &str, &Value, Value)>> {
1126 131586 : if evs.iter().any(|ev| ev.op == TemporalOp::ScopeChanged) {
1127 38 : return None;
1128 123474 : }
1129 123474 : let mut out: Vec<(&TenantId, &str, &Value, Value)> = Vec::new();
1130 131546 : for ev in evs {
1131 131546 : let same = out
1132 131546 : .last()
1133 131546 : .is_some_and(|(t, id, _, _)| **t == ev.tenant && *id == ev.entity_id);
1134 131546 : if !same {
1135 123786 : out.push((
1136 123786 : &ev.tenant,
1137 123786 : &ev.entity_id,
1138 123786 : &ev.shell,
1139 123786 : Value::Object(serde_json::Map::new()),
1140 123786 : ));
1141 123786 : }
1142 131546 : if let Some((_, _, shell, Value::Object(additions))) = out.last_mut() {
1143 131546 : *shell = &ev.shell;
1144 131546 : if let Some(arr) = additions
1145 131546 : .entry(ev.attr.clone())
1146 131546 : .or_insert_with(|| Value::Array(Vec::new()))
1147 131546 : .as_array_mut()
1148 131546 : {
1149 131546 : arr.push(ev.instance.clone());
1150 131546 : }
1151 0 : }
1152 : }
1153 123474 : Some(out)
1154 123512 : }
1155 :
1156 : /// The member an attribute instance carries its write's provenance in from
1157 : /// the API to the temporal store: `channel`,
1158 : /// `actor`, `operation`, `reason`, `requestId`. A store takes it out of the
1159 : /// instance before it keeps the instance, whoever put it there, so no read
1160 : /// ever answers it as data; a JSON-LD keyword is never an attribute member.
1161 : pub const PROVENANCE_KEY: &str = "@provenance";
1162 :
1163 : /// Takes [`PROVENANCE_KEY`] out of an instance, returning what it held.
1164 146685 : pub fn take_provenance(instance: &mut Value) -> Option<Value> {
1165 146685 : instance.as_object_mut()?.remove(PROVENANCE_KEY)
1166 146685 : }
1167 :
1168 : /// Takes [`PROVENANCE_KEY`] out of every instance of a temporal document:
1169 : /// what a store that keeps no provenance does before it keeps the document.
1170 454 : pub fn strip_provenance(doc: &mut Value) {
1171 2888 : for instances in doc.as_object_mut().into_iter().flat_map(|d| d.values_mut()) {
1172 2888 : for instance in instances.as_array_mut().into_iter().flatten() {
1173 2532 : take_provenance(instance);
1174 2532 : }
1175 : }
1176 454 : }
1177 :
1178 : /// The expanded name of the Property an Entity carries its own history
1179 : /// retention in: a duration or `off`, the most
1180 : /// specific level, beside `expiresAt`, which ends the Entity itself.
1181 : pub const HISTORY_RETENTION: &str = "https://uri.etsi.org/ngsi-ld/default-context/historyRetention";
1182 :
1183 : /// The longest history retention whose rows a store may keep unlogged
1184 : /// (docs/src/storage.md "Short history"): a crash loses at most the
1185 : /// trail the sweep drops within this span anyway.
1186 : pub const SHORT_HISTORY: std::time::Duration = std::time::Duration::from_secs(3600);
1187 :
1188 : /// Which writes record history of [`SHORT_HISTORY`] or less, from the
1189 : /// Tenants' retention rules: a store keeps those rows where a crash
1190 : /// may lose them and writes them no WAL.
1191 : #[derive(Debug, Default, Clone, PartialEq, Eq)]
1192 : pub struct ShortHistory {
1193 : /// (Tenant, expanded type) of every type rule that says something of
1194 : /// history: `true` when it keeps [`SHORT_HISTORY`] or less.
1195 : pub types: std::collections::HashMap<(String, String), bool>,
1196 : /// Tenants whose `*` rule keeps [`SHORT_HISTORY`] or less.
1197 : pub tenants: std::collections::HashSet<String>,
1198 : }
1199 :
1200 : impl ShortHistory {
1201 : /// Whether a write of an Entity of `types` in `tenant` keeps short
1202 : /// history: some type's rule is short and none is longer, or no type has
1203 : /// a rule and the Tenant's `*` rule is short. An Entity with its own
1204 : /// [`HISTORY_RETENTION`] (`own`) never is: its level wins over both.
1205 : #[must_use]
1206 5029 : pub fn is_short(&self, tenant: &str, types: &[String], own: bool) -> bool {
1207 5029 : if own || (self.types.is_empty() && self.tenants.is_empty()) {
1208 4988 : return false;
1209 41 : }
1210 41 : let mut ruled = types
1211 41 : .iter()
1212 43 : .filter_map(|t| self.types.get(&(tenant.to_owned(), t.clone())))
1213 41 : .peekable();
1214 41 : if ruled.peek().is_none() {
1215 6 : return self.tenants.contains(tenant);
1216 35 : }
1217 35 : ruled.all(|short| *short)
1218 5029 : }
1219 : }
1220 :
1221 : /// The Entities of one Tenant a history retention statement reaches.
1222 : #[derive(Debug, Clone, Copy)]
1223 : pub enum HistoryScope<'a> {
1224 : /// One Entity, by its own `historyRetention`.
1225 : Entity(&'a str),
1226 : /// The Entities with this type, but not `except_ids`, which carry their
1227 : /// own retention.
1228 : Type {
1229 : /// The expanded type.
1230 : type_iri: &'a str,
1231 : /// Entities left out.
1232 : except_ids: &'a [String],
1233 : },
1234 : /// The Entities with none of `except_types` and not in `except_ids`:
1235 : /// what a Tenant's or the platform's default reaches.
1236 : Rest {
1237 : /// Types with a rule of their own.
1238 : except_types: &'a [String],
1239 : /// Entities left out.
1240 : except_ids: &'a [String],
1241 : },
1242 : }
1243 :
1244 : /// Temporal storage: the entity history (Temporal Evolution) plus the raw
1245 : /// temporal documents the 5.6.13-5.6.16 edit paths operate on.
1246 : ///
1247 : /// Internal bookkeeping (snapshot copies, delete cascades) degrades to
1248 : /// benign no-ops on a driver without temporal support; the CLIENT-facing
1249 : /// operations answer `OperationNotSupported` (422 per CIM 009
1250 : /// Table 6.3.2-1) instead.
1251 : #[async_trait::async_trait]
1252 : pub trait TemporalDriver: Send + Sync {
1253 : /// The drain: one call carries a whole request's events, in production
1254 : /// order. The default folds consecutive events of one entity into a
1255 : /// single `temporal_append` (scope changes go through `mutate`, as
1256 : /// 4.5.6 shapes them); a bulk writer overrides this and sees the batch.
1257 10 : async fn event_list(&self, evs: &[TemporalEvent]) -> Result<(), NgsiError> {
1258 : drain_events(self, evs).await
1259 10 : }
1260 : /// `false` = this deployment records no history (`NoTemporal`); the
1261 : /// write path skips recording entirely.
1262 157176 : fn supported(&self) -> bool {
1263 157176 : true
1264 157176 : }
1265 : /// Stored attribute instances of one tenant (inventory); a driver
1266 : /// without history reports 0.
1267 0 : async fn attr_instance_count(&self, tenant: &TenantId) -> Result<u64, NgsiError> {
1268 0 : let _ = tenant;
1269 : Ok(0)
1270 0 : }
1271 : /// [`Self::attr_instance_count`] for a read that answers without it when
1272 : /// the count is slow (`GET /q/tenants/{tenant}`): a store that
1273 : /// scans for the count gives up after `bound` with an error. The default
1274 : /// counts without a bound.
1275 : async fn attr_instance_count_within(
1276 : &self,
1277 : tenant: &TenantId,
1278 : bound: std::time::Duration,
1279 0 : ) -> Result<u64, NgsiError> {
1280 : let _ = bound;
1281 : self.attr_instance_count(tenant).await
1282 0 : }
1283 : /// The bytes those attribute instances take in the store; a driver
1284 : /// without history reports 0.
1285 2 : async fn attr_instance_bytes(&self, tenant: &TenantId) -> Result<u64, NgsiError> {
1286 2 : let _ = tenant;
1287 : Ok(0)
1288 2 : }
1289 : /// Retention of history in one Tenant (`/q/retention`):
1290 : /// delete at most `batch` attribute instances of the Entities `scope`
1291 : /// names whose instant (`observedAt`, else `modifiedAt`, `deletedAt`,
1292 : /// `createdAt`) is before `before`, a UTC DateTime. Returns how many were
1293 : /// deleted, or with `dry_run` would be, deleting nothing; a driver
1294 : /// without history has none.
1295 : async fn retain_history(
1296 : &self,
1297 : tenant: &TenantId,
1298 : scope: HistoryScope<'_>,
1299 : before: &str,
1300 : batch: usize,
1301 : dry_run: bool,
1302 0 : ) -> Result<u64, NgsiError> {
1303 : let _ = (tenant, scope, before, batch, dry_run);
1304 : Ok(0)
1305 0 : }
1306 : /// Retention by partition: drop every stored slice of history
1307 : /// whose rows all belong to Tenants in `floors` and lie before that
1308 : /// Tenant's floor, a UTC DateTime no rule of the Tenant keeps rows below.
1309 : /// A slice holding a row of any other Tenant stays. Returns one line per
1310 : /// slice dropped; a driver that stores history in no droppable slices
1311 : /// drops none, and the row sweep does the work.
1312 : async fn drop_retained_history(
1313 : &self,
1314 : floors: &[(String, String)],
1315 0 : ) -> Result<Vec<String>, NgsiError> {
1316 0 : let _ = floors;
1317 : Ok(Vec::new())
1318 0 : }
1319 : /// The retention rules' [`ShortHistory`], replaced whole on each reload
1320 : /// of the rules; a driver without unlogged storage ignores it.
1321 0 : fn set_short_history(&self, short: ShortHistory) {
1322 0 : let _ = short;
1323 0 : }
1324 : /// Every version of the rows of one Entity's history, by expanded Attribute name: each instance with its
1325 : /// `version` and `provenance`, a superseded one with `supersededAt`,
1326 : /// oldest first per measurement. A driver that keeps no versions has none.
1327 : async fn versions(
1328 : &self,
1329 : tenant: &TenantId,
1330 : entity_id: &str,
1331 0 : ) -> Result<serde_json::Map<String, Value>, NgsiError> {
1332 : let _ = (tenant, entity_id);
1333 : Ok(serde_json::Map::new())
1334 0 : }
1335 : /// Erasure: every row of `attrs` of one
1336 : /// Entity, the current version and every superseded one, observed at or
1337 : /// before `before` (a UTC DateTime; `None`: every row); `attrs` `None`
1338 : /// takes every Attribute, and the Entity's history itself once it holds
1339 : /// no row. Answers how many rows went; a driver without history has none.
1340 : async fn erase(
1341 : &self,
1342 : tenant: &TenantId,
1343 : entity_id: &str,
1344 : attrs: Option<&[String]>,
1345 : before: Option<&str>,
1346 0 : ) -> Result<u64, NgsiError> {
1347 : let _ = (tenant, entity_id, attrs, before);
1348 : Ok(0)
1349 0 : }
1350 : /// Remove the whole history of one tenant; nothing to do without history.
1351 0 : async fn purge_tenant(&self, tenant: &TenantId) -> Result<(), NgsiError> {
1352 0 : let _ = tenant;
1353 : Ok(())
1354 0 : }
1355 : /// Readiness of the temporal backend; the default is always ready.
1356 0 : async fn ping(&self) -> Result<(), NgsiError> {
1357 : Ok(())
1358 0 : }
1359 : /// What this temporal driver runs on, for `/q/health`; the same
1360 : /// contract as [`CurrentStateDriver::version_info`].
1361 6 : fn version_info(&self) -> Value {
1362 6 : Value::Object(serde_json::Map::new())
1363 6 : }
1364 : /// Drain: finish in-flight work and disconnect cleanly. A temporal
1365 : /// driver configured as a backend of its own holds its own pool, and the
1366 : /// shutdown path closes it here. May be called more than once — when one
1367 : /// instance serves both seams it is closed through each of them — so an
1368 : /// implementation makes it idempotent. The default has nothing to close.
1369 6 : async fn close(&self) {}
1370 : /// Auto-recording fast path: append instances, creating the meta shell
1371 : /// on first touch — and only for an entity that still exists (5.6.6
1372 : /// deletes history; an append overlapping the delete must not recreate
1373 : /// it).
1374 : async fn temporal_append(
1375 : &self,
1376 : tenant: &TenantId,
1377 : id: &str,
1378 : shell: &Value,
1379 : additions: &Value,
1380 : ) -> Result<(), NgsiError>;
1381 : /// Would `query_temporal` page EXACTLY under this `q` and range — the
1382 : /// driver's own entity verdict equal to the evaluator's on every row,
1383 : /// so a page it cuts is the page the caller would have cut? A driver
1384 : /// that pages after an in-process evaluation, or not at all, answers
1385 : /// `false`, and the caller pages the merged result itself (5.7.4.4).
1386 0 : fn q_pushdown_exact(
1387 0 : &self,
1388 0 : _q: &antares_ql::QNode,
1389 0 : _range: Option<&filter::InstanceRange<'_>>,
1390 0 : _expand: &dyn Fn(&str) -> String,
1391 0 : ) -> bool {
1392 0 : false
1393 0 : }
1394 : /// Query Temporal Evolution (5.7.4) with pushdown where possible.
1395 : async fn query_temporal(
1396 : &self,
1397 : tenant: &TenantId,
1398 : f: &filter::TemporalFilter<'_>,
1399 : ) -> Result<filter::TemporalOutcome, NgsiError>;
1400 : /// Retrieve Temporal Evolution (5.7.3) with instance pruning.
1401 : async fn get_temporal(
1402 : &self,
1403 : tenant: &TenantId,
1404 : id: &str,
1405 : f: &filter::TemporalFilter<'_>,
1406 : ) -> Result<Option<Value>, NgsiError>;
1407 : /// Raw temporal document access (the 5.6.11-5.6.16 edit/delete paths
1408 : /// and internal copies).
1409 : async fn get(&self, tenant: &TenantId, id: &str) -> Result<Option<Value>, NgsiError>;
1410 : /// Insert a temporal document; `false` if the id already exists.
1411 : async fn create(&self, tenant: &TenantId, id: &str, doc: Value) -> Result<bool, NgsiError>;
1412 : /// Insert or replace a temporal document. `true` means it was ALREADY
1413 : /// there and this call replaced it; `false` means this call created it,
1414 : /// the same polarity as the current-state seam.
1415 : async fn upsert(&self, tenant: &TenantId, id: &str, doc: Value) -> Result<bool, NgsiError>;
1416 : /// Delete an entity's whole history; `false` if it had none.
1417 : async fn delete(&self, tenant: &TenantId, id: &str) -> Result<bool, NgsiError>;
1418 : /// Every temporal document in the tenant.
1419 : async fn list(&self, tenant: &TenantId) -> Result<Vec<Value>, NgsiError>;
1420 : /// Read-modify-write of one temporal document under its row lock;
1421 : /// `None` = absent (never an insert), `Some(Err)` = rejected, not committed.
1422 : async fn mutate_boxed<'a>(
1423 : &self,
1424 : tenant: &TenantId,
1425 : id: &str,
1426 : f: MutateFn<'a>,
1427 : ) -> Result<Option<Result<(), ()>>, NgsiError>;
1428 : }
1429 :
1430 : /// Typed mutate sugar for the temporal seam, same slot trick as
1431 : /// [`CurrentStateDriverExt`].
1432 : pub trait TemporalDriverExt {
1433 : /// Typed read-modify-write of one temporal document: `None` = absent,
1434 : /// `Some(Err(e))` = the closure rejected and nothing was committed.
1435 : fn mutate<T: Send, E: Send>(
1436 : &self,
1437 : tenant: &TenantId,
1438 : id: &str,
1439 : f: impl FnOnce(&mut Value) -> Result<T, E> + Send,
1440 : ) -> impl std::future::Future<Output = Result<Option<Result<T, E>>, NgsiError>> + Send;
1441 : }
1442 :
1443 : impl<S: TemporalDriver + ?Sized> TemporalDriverExt for S {
1444 862 : async fn mutate<T: Send, E: Send>(
1445 862 : &self,
1446 862 : tenant: &TenantId,
1447 862 : id: &str,
1448 862 : f: impl FnOnce(&mut Value) -> Result<T, E> + Send,
1449 862 : ) -> Result<Option<Result<T, E>>, NgsiError> {
1450 862 : let slot = std::sync::Mutex::new(None);
1451 862 : let r = self
1452 862 : .mutate_boxed(
1453 862 : tenant,
1454 862 : id,
1455 862 : Box::new(|v| {
1456 736 : let r = f(v);
1457 736 : let flag = if r.is_ok() { Ok(()) } else { Err(()) };
1458 736 : *lock(&slot) = Some(r);
1459 736 : flag
1460 736 : }),
1461 : )
1462 862 : .await?;
1463 854 : Ok(r.and_then(|_| into_inner(slot)))
1464 862 : }
1465 : }
1466 :
1467 : /// The no-history driver: temporal OFF as a driver choice. Client-facing
1468 : /// reads answer `OperationNotSupported`; the recorder and the internal
1469 : /// bookkeeping paths degrade to no-ops.
1470 : pub struct NoTemporal;
1471 :
1472 6 : fn unsupported() -> NgsiError {
1473 6 : NgsiError::OperationNotSupported("no temporal store is configured".into())
1474 6 : }
1475 :
1476 : #[async_trait::async_trait]
1477 : impl TemporalDriver for NoTemporal {
1478 12 : fn supported(&self) -> bool {
1479 12 : false
1480 12 : }
1481 : async fn temporal_append(
1482 : &self,
1483 : _tenant: &TenantId,
1484 : _id: &str,
1485 : _shell: &Value,
1486 : _additions: &Value,
1487 2 : ) -> Result<(), NgsiError> {
1488 : Ok(())
1489 2 : }
1490 : async fn query_temporal(
1491 : &self,
1492 : _tenant: &TenantId,
1493 : _f: &filter::TemporalFilter<'_>,
1494 4 : ) -> Result<filter::TemporalOutcome, NgsiError> {
1495 : Err(unsupported())
1496 4 : }
1497 : async fn get_temporal(
1498 : &self,
1499 : _tenant: &TenantId,
1500 : _id: &str,
1501 : _f: &filter::TemporalFilter<'_>,
1502 2 : ) -> Result<Option<Value>, NgsiError> {
1503 : Err(unsupported())
1504 2 : }
1505 2 : async fn get(&self, _tenant: &TenantId, _id: &str) -> Result<Option<Value>, NgsiError> {
1506 : Ok(None)
1507 2 : }
1508 0 : async fn create(&self, _tenant: &TenantId, _id: &str, _doc: Value) -> Result<bool, NgsiError> {
1509 : Ok(false)
1510 0 : }
1511 0 : async fn upsert(&self, _tenant: &TenantId, _id: &str, _doc: Value) -> Result<bool, NgsiError> {
1512 : Ok(false)
1513 0 : }
1514 2 : async fn delete(&self, _tenant: &TenantId, _id: &str) -> Result<bool, NgsiError> {
1515 : Ok(false)
1516 2 : }
1517 2 : async fn list(&self, _tenant: &TenantId) -> Result<Vec<Value>, NgsiError> {
1518 : Ok(Vec::new())
1519 2 : }
1520 : async fn mutate_boxed<'a>(
1521 : &self,
1522 : _tenant: &TenantId,
1523 : _id: &str,
1524 : _f: MutateFn<'a>,
1525 2 : ) -> Result<Option<Result<(), ()>>, NgsiError> {
1526 : Ok(None)
1527 2 : }
1528 : }
1529 :
1530 : #[cfg(test)]
1531 : mod tests {
1532 : use super::*;
1533 :
1534 : /// A recreate whose creation stamp sorts at or below the deleted
1535 : /// incarnation's (one millisecond, a clock behind) is still admitted,
1536 : /// and so are its updates; within one incarnation an overtaken change
1537 : /// is still dropped.
1538 : #[test]
1539 2 : fn a_recreated_incarnation_is_admitted_whatever_its_stamp() {
1540 2 : let seen = NewestSeen::new(16);
1541 2 : let v = |incarnation: &str, version| ChangeVersion {
1542 12 : incarnation: incarnation.to_owned(),
1543 12 : version,
1544 12 : };
1545 2 : let old = "2026-01-01 00:00:00.500+00";
1546 2 : assert!(seen.admit("t", "urn:e", &v(old, 3)));
1547 : // the delete leaves one past the deleted row's version
1548 2 : assert!(seen.admit("t", "urn:e", &v(old, 4)));
1549 2 : let recreated = "2026-01-01 00:00:00.400+00";
1550 2 : assert!(
1551 2 : seen.admit("t", "urn:e", &v(recreated, 1)),
1552 : "the recreate was dropped"
1553 : );
1554 2 : assert!(
1555 2 : seen.admit("t", "urn:e", &v(recreated, 2)),
1556 : "its update was dropped"
1557 : );
1558 2 : assert!(
1559 2 : !seen.admit("t", "urn:e", &v(recreated, 1)),
1560 : "an overtaken change of one incarnation is still dropped"
1561 : );
1562 2 : assert!(
1563 2 : !seen.admit("t", "urn:e", &v(old, 3)),
1564 : "a late change of the superseded incarnation is still dropped"
1565 : );
1566 2 : }
1567 :
1568 : /// SECV-01: incarnations arriving out of order (A, C, then a late B)
1569 : /// never mute the live one: C's next update still goes out.
1570 : #[test]
1571 2 : fn a_late_third_incarnation_never_mutes_the_live_one() {
1572 2 : let v = |inc: &str, n: i64| ChangeVersion {
1573 24 : incarnation: inc.into(),
1574 24 : version: n,
1575 24 : };
1576 2 : let seen = NewestSeen::new(16);
1577 2 : assert!(seen.admit("t", "e", &v("A", 1)));
1578 2 : assert!(seen.admit("t", "e", &v("A", 2)), "A's delete");
1579 2 : assert!(seen.admit("t", "e", &v("C", 1)));
1580 2 : assert!(seen.admit("t", "e", &v("B", 1)), "B's change arrives late");
1581 2 : assert!(
1582 2 : seen.admit("t", "e", &v("C", 2)),
1583 : "the live Entity was muted"
1584 : );
1585 2 : assert!(
1586 2 : !seen.admit("t", "e", &v("C", 1)),
1587 : "C's own order still holds"
1588 : );
1589 2 : assert!(
1590 2 : !seen.admit("t", "e", &v("A", 1)),
1591 : "A's late update after its delete"
1592 : );
1593 8 : for i in 0..INCARNATIONS {
1594 8 : assert!(seen.admit("t", "e", &v(&format!("n{i}"), 1)));
1595 : }
1596 2 : assert!(
1597 2 : seen.admit("t", "e", &v("C", 1)),
1598 : "forgotten past the bound: admitted"
1599 : );
1600 2 : }
1601 :
1602 : /// S3-18: a change an overtaken hook delivers late is dropped, a newer
1603 : /// one, an equal one and a recreated Entity's first are admitted, and the
1604 : /// map forgets past two generations.
1605 : #[test]
1606 2 : fn newest_seen_drops_what_a_later_commit_overtook() {
1607 2 : let v = |inc: &str, n: i64| ChangeVersion {
1608 22 : incarnation: inc.into(),
1609 22 : version: n,
1610 22 : };
1611 2 : let seen = NewestSeen::new(2);
1612 2 : assert!(seen.admit("t", "e", &v("a", 2)));
1613 2 : assert!(
1614 2 : !seen.admit("t", "e", &v("a", 1)),
1615 : "the older image arrives last"
1616 : );
1617 2 : assert!(
1618 2 : seen.admit("t", "e", &v("a", 2)),
1619 : "seen twice, delivered twice"
1620 : );
1621 2 : assert!(seen.admit("u", "e", &v("a", 1)), "another Tenant's Entity");
1622 2 : assert!(seen.admit("t", "e", &v("b", 1)), "recreated after a delete");
1623 2 : assert!(
1624 2 : !seen.admit("t", "e", &v("a", 1)),
1625 : "a late change of the deleted incarnation older than one it delivered"
1626 : );
1627 8 : for i in 0..4 {
1628 8 : assert!(seen.admit("t", &format!("x{i}"), &v("a", 1)));
1629 : }
1630 2 : let held = seen
1631 2 : .gens
1632 2 : .lock()
1633 2 : .expect("map")
1634 2 : .iter()
1635 4 : .map(|g| g.len())
1636 2 : .sum::<usize>();
1637 2 : assert!(held <= 4, "two generations of two: {held}");
1638 2 : assert!(seen.admit("t", "e", &v("a", 1)), "forgotten past the bound");
1639 2 : }
1640 :
1641 : /// a short type rule wins over the Tenant's level, a longer type
1642 : /// rule among the types or the Entity's own retention keeps it logged.
1643 : #[test]
1644 2 : fn short_history_takes_the_most_specific_level() {
1645 2 : let mut s = ShortHistory::default();
1646 14 : let ty = |t: &str| vec![t.to_owned()];
1647 2 : assert!(!s.is_short("city", &ty("Bus"), false), "no rule, not short");
1648 2 : s.types.insert(("city".into(), "Bus".into()), true);
1649 2 : s.types.insert(("city".into(), "Stop".into()), false);
1650 2 : assert!(s.is_short("city", &ty("Bus"), false));
1651 2 : assert!(!s.is_short("city", &ty("Bus"), true), "own retention wins");
1652 2 : assert!(!s.is_short("town", &ty("Bus"), false), "another Tenant");
1653 2 : let both = vec!["Bus".to_owned(), "Stop".to_owned()];
1654 2 : assert!(!s.is_short("city", &both, false), "a longer type keeps it");
1655 2 : assert!(!s.is_short("city", &ty("Tram"), false), "no `*` rule");
1656 2 : s.tenants.insert("city".into());
1657 2 : assert!(s.is_short("city", &ty("Tram"), false), "the `*` rule");
1658 2 : assert!(!s.is_short("city", &ty("Stop"), false), "a type rule wins");
1659 2 : }
1660 :
1661 : /// The mode list is one source of truth: every variant round-trips
1662 : /// through its name, and the message for an unknown name lists all of
1663 : /// them — a backend added to the enum cannot be missing from either.
1664 : #[test]
1665 2 : fn every_store_mode_round_trips_and_the_error_lists_them_all() {
1666 8 : for m in StoreMode::ALL {
1667 8 : let back = m
1668 8 : .as_str()
1669 8 : .parse::<StoreMode>()
1670 8 : .unwrap_or_else(|e| panic!("{m} must parse back: {e}"));
1671 8 : assert_eq!(back, m);
1672 : }
1673 2 : let err = "mongo".parse::<StoreMode>().expect_err("unknown mode");
1674 2 : assert!(err.contains("mongo"), "{err}");
1675 8 : for m in StoreMode::ALL {
1676 8 : assert!(err.contains(m.as_str()), "the message must name {m}: {err}");
1677 : }
1678 2 : }
1679 :
1680 : /// A minimal driver proving the boxed seam round-trips typed results:
1681 : /// present row → the closure's T and E cross intact; absent → None.
1682 : struct OneDoc(std::sync::Mutex<Option<Value>>);
1683 : #[async_trait::async_trait]
1684 : impl CurrentStateDriver for OneDoc {
1685 0 : async fn ping(&self) -> Result<(), NgsiError> {
1686 : Ok(())
1687 0 : }
1688 0 : async fn close(&self) {}
1689 0 : fn set_change_hook(&self, _h: ChangeHook) {}
1690 : async fn create(
1691 : &self,
1692 : _t: &TenantId,
1693 : _k: Kind,
1694 : _id: &str,
1695 : doc: Value,
1696 2 : ) -> Result<bool, NgsiError> {
1697 : *self.0.lock().expect("lock") = Some(doc);
1698 : Ok(true)
1699 2 : }
1700 : async fn batch_create(
1701 : &self,
1702 : _t: &TenantId,
1703 : _items: Vec<(String, Value)>,
1704 0 : ) -> Result<Vec<bool>, NgsiError> {
1705 : unimplemented!()
1706 0 : }
1707 : async fn list_page(
1708 : &self,
1709 : _t: &TenantId,
1710 : _k: Kind,
1711 : _after: Option<&str>,
1712 : _limit: usize,
1713 0 : ) -> Result<Vec<Value>, NgsiError> {
1714 : unimplemented!()
1715 0 : }
1716 : async fn list_slice(
1717 : &self,
1718 : _t: &TenantId,
1719 : _k: Kind,
1720 : _offset: usize,
1721 : _limit: usize,
1722 0 : ) -> Result<(Vec<Value>, usize), NgsiError> {
1723 : unimplemented!()
1724 0 : }
1725 : async fn batch_delete(
1726 : &self,
1727 : _t: &TenantId,
1728 : _ids: &[String],
1729 0 : ) -> Result<Vec<bool>, NgsiError> {
1730 : unimplemented!()
1731 0 : }
1732 : async fn batch_upsert(
1733 : &self,
1734 : _t: &TenantId,
1735 : _items: Vec<(String, Value)>,
1736 0 : ) -> Result<Vec<bool>, NgsiError> {
1737 : unimplemented!()
1738 0 : }
1739 : async fn upsert(
1740 : &self,
1741 : _t: &TenantId,
1742 : _k: Kind,
1743 : _id: &str,
1744 : _doc: Value,
1745 0 : ) -> Result<bool, NgsiError> {
1746 : unimplemented!()
1747 0 : }
1748 : async fn get(
1749 : &self,
1750 : _t: &TenantId,
1751 : _k: Kind,
1752 : _id: &str,
1753 4 : ) -> Result<Option<Value>, NgsiError> {
1754 : Ok(self.0.lock().expect("lock").clone())
1755 4 : }
1756 0 : async fn delete(&self, _t: &TenantId, _k: Kind, _id: &str) -> Result<bool, NgsiError> {
1757 : unimplemented!()
1758 0 : }
1759 : async fn delete_entity_if(
1760 : &self,
1761 : _t: &TenantId,
1762 : _id: &str,
1763 : _keep: &(dyn for<'v> Fn(&'v Value) -> bool + Sync),
1764 0 : ) -> Result<bool, NgsiError> {
1765 : unimplemented!()
1766 0 : }
1767 0 : async fn list(&self, _t: &TenantId, _k: Kind) -> Result<Vec<Value>, NgsiError> {
1768 : unimplemented!()
1769 0 : }
1770 : async fn matching_registrations(
1771 : &self,
1772 : _t: &TenantId,
1773 : _ids: Option<&[String]>,
1774 : _types: Option<&[String]>,
1775 0 : ) -> Result<Vec<Value>, NgsiError> {
1776 : unimplemented!()
1777 0 : }
1778 : async fn query_entities(
1779 : &self,
1780 : _t: &TenantId,
1781 : _f: &filter::EntityFilter<'_>,
1782 0 : ) -> Result<filter::QueryOutcome, NgsiError> {
1783 : unimplemented!()
1784 0 : }
1785 : async fn mutate_boxed<'a>(
1786 : &self,
1787 : _t: &TenantId,
1788 : _k: Kind,
1789 : _id: &str,
1790 : f: MutateFn<'a>,
1791 6 : ) -> Result<Option<Result<(), ()>>, NgsiError> {
1792 : let mut guard = self.0.lock().expect("lock");
1793 : match guard.as_mut() {
1794 : None => Ok(None),
1795 : Some(v) => {
1796 : let mut copy = v.clone();
1797 : match f(&mut copy) {
1798 : Ok(()) => {
1799 : *v = copy;
1800 : Ok(Some(Ok(())))
1801 : }
1802 : Err(()) => Ok(Some(Err(()))),
1803 : }
1804 : }
1805 : }
1806 6 : }
1807 : async fn batch_mutate_boxed<'a>(
1808 : &self,
1809 : _t: &TenantId,
1810 : _ids: &[String],
1811 : _f: BatchMutateFn<'a>,
1812 0 : ) -> Result<Vec<Option<Result<(), ()>>>, NgsiError> {
1813 : unimplemented!()
1814 0 : }
1815 0 : async fn tenant_exists(&self, _t: &TenantId) -> Result<bool, NgsiError> {
1816 : Ok(true)
1817 0 : }
1818 0 : async fn subscription_tenants(&self) -> Result<Vec<String>, NgsiError> {
1819 : Ok(Vec::new())
1820 0 : }
1821 : async fn context_put(
1822 : &self,
1823 : _t: Option<&TenantId>,
1824 : _id: &str,
1825 : _doc: Value,
1826 0 : ) -> Result<(), NgsiError> {
1827 : Ok(())
1828 0 : }
1829 : async fn context_get(
1830 : &self,
1831 : _t: Option<&TenantId>,
1832 : _id: &str,
1833 0 : ) -> Result<Option<Value>, NgsiError> {
1834 : Ok(None)
1835 0 : }
1836 : async fn context_mutate<'a>(
1837 : &self,
1838 : _t: Option<&TenantId>,
1839 : _id: &str,
1840 : _f: MutateFn<'a>,
1841 0 : ) -> Result<Option<Result<(), ()>>, NgsiError> {
1842 : Ok(None)
1843 0 : }
1844 : async fn context_delete(
1845 : &self,
1846 : _t: Option<&TenantId>,
1847 : _id: &str,
1848 0 : ) -> Result<bool, NgsiError> {
1849 : Ok(false)
1850 0 : }
1851 0 : async fn context_list_meta(&self, _t: Option<&TenantId>) -> Result<Vec<Value>, NgsiError> {
1852 : Ok(Vec::new())
1853 0 : }
1854 : }
1855 :
1856 : #[tokio::test]
1857 2 : async fn typed_mutate_round_trips_through_the_boxed_seam() {
1858 2 : let t = TenantId::new("t").expect("tenant");
1859 2 : let d: std::sync::Arc<dyn CurrentStateDriver> =
1860 2 : std::sync::Arc::new(OneDoc(std::sync::Mutex::new(None)));
1861 : // absent → None, closure never runs
1862 2 : let r = d
1863 2 : .mutate::<u32, &str>(&t, Kind::Entity, "x", |_| panic!("must not run"))
1864 2 : .await
1865 2 : .expect("driver ok");
1866 2 : assert!(r.is_none());
1867 2 : d.create(&t, Kind::Entity, "x", serde_json::json!({"n": 1}))
1868 2 : .await
1869 2 : .expect("create");
1870 : // typed success crosses the boundary AND the write commits
1871 2 : let r = d
1872 2 : .mutate::<u32, &str>(&t, Kind::Entity, "x", |v| {
1873 2 : v["n"] = serde_json::json!(2);
1874 2 : Ok(7)
1875 2 : })
1876 2 : .await
1877 2 : .expect("driver ok");
1878 2 : assert_eq!(r, Some(Ok(7)));
1879 2 : assert_eq!(
1880 2 : d.get(&t, Kind::Entity, "x")
1881 2 : .await
1882 2 : .expect("get")
1883 2 : .expect("doc")["n"],
1884 : 2
1885 : );
1886 : // typed error crosses the boundary AND the write is discarded
1887 2 : let r = d
1888 2 : .mutate::<u32, &str>(&t, Kind::Entity, "x", |v| {
1889 2 : v["n"] = serde_json::json!(99);
1890 2 : Err("rejected")
1891 2 : })
1892 2 : .await
1893 2 : .expect("driver ok");
1894 2 : assert_eq!(r, Some(Err("rejected")));
1895 2 : assert_eq!(
1896 2 : d.get(&t, Kind::Entity, "x")
1897 2 : .await
1898 2 : .expect("get")
1899 2 : .expect("doc")["n"],
1900 2 : 2,
1901 2 : "a rejecting closure must not commit"
1902 2 : );
1903 2 : }
1904 :
1905 : /// A temporal driver that only records what the seam hands it: appends
1906 : /// as (id, additions) in call order, scope mutations on a held doc.
1907 : #[derive(Default)]
1908 : struct Recorder {
1909 : appends: std::sync::Mutex<Vec<(String, Value)>>,
1910 : doc: std::sync::Mutex<Option<Value>>,
1911 : /// the Entity whose appends this driver refuses (a full history)
1912 : refuse: Option<&'static str>,
1913 : }
1914 : #[async_trait::async_trait]
1915 : impl TemporalDriver for Recorder {
1916 : async fn temporal_append(
1917 : &self,
1918 : _t: &TenantId,
1919 : id: &str,
1920 : shell: &Value,
1921 : additions: &Value,
1922 18 : ) -> Result<(), NgsiError> {
1923 : if self.refuse == Some(id) {
1924 : return Err(NgsiError::BadRequestData("history full".into()));
1925 : }
1926 : self.appends
1927 : .lock()
1928 : .expect("lock")
1929 : .push((id.to_owned(), additions.clone()));
1930 : // a real driver creates the doc from the shell on first touch
1931 : self.doc
1932 : .lock()
1933 : .expect("lock")
1934 8 : .get_or_insert_with(|| shell.clone());
1935 : Ok(())
1936 18 : }
1937 : async fn query_temporal(
1938 : &self,
1939 : _t: &TenantId,
1940 : _f: &filter::TemporalFilter<'_>,
1941 0 : ) -> Result<filter::TemporalOutcome, NgsiError> {
1942 : unimplemented!()
1943 0 : }
1944 : async fn get_temporal(
1945 : &self,
1946 : _t: &TenantId,
1947 : _id: &str,
1948 : _f: &filter::TemporalFilter<'_>,
1949 0 : ) -> Result<Option<Value>, NgsiError> {
1950 : unimplemented!()
1951 0 : }
1952 6 : async fn get(&self, _t: &TenantId, _id: &str) -> Result<Option<Value>, NgsiError> {
1953 : Ok(self.doc.lock().expect("lock").clone())
1954 6 : }
1955 2 : async fn create(&self, _t: &TenantId, _id: &str, doc: Value) -> Result<bool, NgsiError> {
1956 : *self.doc.lock().expect("lock") = Some(doc);
1957 : Ok(true)
1958 2 : }
1959 0 : async fn upsert(&self, _t: &TenantId, _id: &str, _doc: Value) -> Result<bool, NgsiError> {
1960 : unimplemented!()
1961 0 : }
1962 0 : async fn delete(&self, _t: &TenantId, _id: &str) -> Result<bool, NgsiError> {
1963 : unimplemented!()
1964 0 : }
1965 0 : async fn list(&self, _t: &TenantId) -> Result<Vec<Value>, NgsiError> {
1966 : unimplemented!()
1967 0 : }
1968 : async fn mutate_boxed<'a>(
1969 : &self,
1970 : _t: &TenantId,
1971 : _id: &str,
1972 : f: MutateFn<'a>,
1973 8 : ) -> Result<Option<Result<(), ()>>, NgsiError> {
1974 : let mut guard = self.doc.lock().expect("lock");
1975 : match guard.as_mut() {
1976 : None => Ok(None),
1977 : Some(v) => Ok(Some(f(v))),
1978 : }
1979 8 : }
1980 : }
1981 :
1982 36 : fn ev(op: TemporalOp, id: &str, attr: &str, n: u32) -> TemporalEvent {
1983 36 : TemporalEvent {
1984 36 : op,
1985 36 : tenant: TenantId::new("t").expect("tenant"),
1986 36 : entity_id: id.into(),
1987 36 : shell: serde_json::json!({"id": id, "type": ["T"]}),
1988 36 : attr: attr.into(),
1989 36 : instance: serde_json::json!({"type": "Property", "value": n}),
1990 36 : }
1991 36 : }
1992 :
1993 : /// The drain folds one request's events into ONE append per entity run
1994 : /// — a 2-attribute entity is one call carrying both, not two — and
1995 : /// keeps production order across entities.
1996 : #[test]
1997 2 : fn fold_appends_is_one_append_per_entity_run_and_none_for_a_scope_change() {
1998 2 : let evs = [
1999 2 : ev(TemporalOp::AttrCreated, "urn:a", "speed", 1),
2000 2 : ev(TemporalOp::AttrCreated, "urn:a", "speed", 2),
2001 2 : ev(TemporalOp::AttrCreated, "urn:b", "speed", 3),
2002 2 : ev(TemporalOp::AttrCreated, "urn:a", "load", 4),
2003 2 : ];
2004 2 : let folded = fold_appends(&evs).expect("no scope change");
2005 2 : let ids: Vec<&str> = folded.iter().map(|(_, id, _, _)| *id).collect();
2006 2 : assert_eq!(ids, ["urn:a", "urn:b", "urn:a"]);
2007 2 : assert_eq!(folded[0].3["speed"].as_array().map(Vec::len), Some(2));
2008 2 : assert!(fold_appends(&[]).is_some_and(|f| f.is_empty()));
2009 2 : let scope = ev(TemporalOp::ScopeChanged, "urn:a", "scope", 5);
2010 2 : assert!(fold_appends(&[evs[0].clone(), scope]).is_none());
2011 2 : }
2012 :
2013 : #[tokio::test]
2014 2 : async fn event_list_folds_a_request_into_one_append_per_entity_run() {
2015 2 : let d = Recorder::default();
2016 2 : d.event_list(&[
2017 2 : ev(TemporalOp::AttrCreated, "urn:a", "speed", 1),
2018 2 : ev(TemporalOp::AttrCreated, "urn:a", "speed", 2),
2019 2 : ev(TemporalOp::AttrModified, "urn:a", "heading", 3),
2020 2 : ev(TemporalOp::AttrModified, "urn:b", "speed", 4),
2021 2 : ev(TemporalOp::AttrModified, "urn:a", "speed", 5),
2022 2 : ])
2023 2 : .await
2024 2 : .expect("drain ok");
2025 2 : let appends = d.appends.lock().expect("lock").clone();
2026 6 : let ids: Vec<&str> = appends.iter().map(|(id, _)| id.as_str()).collect();
2027 2 : assert_eq!(
2028 : ids,
2029 : ["urn:a", "urn:b", "urn:a"],
2030 : "one append per entity run, in order"
2031 : );
2032 2 : let first = &appends[0].1;
2033 2 : assert_eq!(first["speed"].as_array().map(Vec::len), Some(2), "{first}");
2034 2 : assert_eq!(
2035 2 : first["heading"].as_array().map(Vec::len),
2036 : Some(1),
2037 : "{first}"
2038 : );
2039 2 : assert!(
2040 2 : first.get("value").is_none(),
2041 : "instances live under their attribute: {first}"
2042 : );
2043 2 : assert_eq!(appends[2].1["speed"][0]["value"], 5);
2044 2 : }
2045 :
2046 : /// One Entity whose append the driver refuses (its history is full)
2047 : /// loses its own history of the request, never the other Entities':
2048 : /// the drain goes on and reports the refusal once it is done.
2049 : #[tokio::test]
2050 2 : async fn a_refused_append_drops_only_its_own_entity() {
2051 2 : let d = Recorder {
2052 2 : refuse: Some("urn:a"),
2053 2 : ..Recorder::default()
2054 2 : };
2055 2 : let r = d
2056 2 : .event_list(&[
2057 2 : ev(TemporalOp::AttrModified, "urn:a", "speed", 1),
2058 2 : ev(TemporalOp::AttrModified, "urn:b", "speed", 2),
2059 2 : ev(TemporalOp::AttrModified, "urn:c", "speed", 3),
2060 2 : ])
2061 2 : .await;
2062 2 : assert!(matches!(r, Err(NgsiError::BadRequestData(_))), "{r:?}");
2063 2 : let ids: Vec<String> = d
2064 2 : .appends
2065 2 : .lock()
2066 2 : .expect("lock")
2067 2 : .iter()
2068 4 : .map(|(id, _)| id.clone())
2069 2 : .collect();
2070 2 : assert_eq!(ids, ["urn:b", "urn:c"]);
2071 2 : }
2072 :
2073 : /// 4.5.6: a scope change becomes a scope instance on the held temporal
2074 : /// doc (array-of-instances form), not an attribute append.
2075 : #[tokio::test]
2076 2 : async fn event_list_records_scope_changes_as_scope_instances() {
2077 2 : let t = TenantId::new("t").expect("tenant");
2078 2 : let d = Recorder::default();
2079 2 : d.create(
2080 2 : &t,
2081 2 : "urn:a",
2082 2 : serde_json::json!({"id": "urn:a", "scope": "/old"}),
2083 2 : )
2084 2 : .await
2085 2 : .expect("create");
2086 2 : let mut scope = ev(TemporalOp::ScopeChanged, "urn:a", "scope", 0);
2087 2 : scope.instance = serde_json::json!({"type": "Property", "value": "/new",
2088 2 : "observedAt": "2026-01-01T00:00:00Z"});
2089 2 : d.event_list(&[scope]).await.expect("drain ok");
2090 2 : assert!(
2091 2 : d.appends.lock().expect("lock").is_empty(),
2092 : "no attribute append for a scope change"
2093 : );
2094 2 : let doc = d.get(&t, "urn:a").await.expect("get").expect("doc");
2095 2 : assert_eq!(doc["scope"][0]["value"], "/new", "{doc}");
2096 2 : assert_eq!(
2097 2 : doc["scope"].as_array().map(Vec::len),
2098 2 : Some(1),
2099 2 : "the plain scope became an instance array: {doc}"
2100 2 : );
2101 2 : }
2102 :
2103 : /// a scope change first in a run, with no temporal doc yet,
2104 : /// still lands: the shell is created rather than the instance dropped,
2105 : /// and additions queued before it are appended first.
2106 : #[tokio::test]
2107 2 : async fn event_list_keeps_a_scope_change_on_an_entity_without_history() {
2108 2 : let t = TenantId::new("t").expect("tenant");
2109 2 : let d = Recorder::default();
2110 2 : d.event_list(&[
2111 2 : ev(TemporalOp::ScopeChanged, "urn:a", "scope", 7),
2112 2 : ev(TemporalOp::AttrCreated, "urn:a", "speed", 1),
2113 2 : ])
2114 2 : .await
2115 2 : .expect("drain ok");
2116 2 : let doc = d.get(&t, "urn:a").await.expect("get").expect("doc");
2117 2 : assert_eq!(doc["scope"][0]["value"], 7, "{doc}");
2118 :
2119 2 : let d = Recorder::default();
2120 2 : d.event_list(&[
2121 2 : ev(TemporalOp::AttrCreated, "urn:a", "speed", 1),
2122 2 : ev(TemporalOp::ScopeChanged, "urn:a", "scope", 7),
2123 2 : ])
2124 2 : .await
2125 2 : .expect("drain ok");
2126 2 : let appends = d.appends.lock().expect("lock").clone();
2127 2 : assert_eq!(appends.len(), 1, "{appends:?}");
2128 2 : assert_eq!(appends[0].1["speed"][0]["value"], 1);
2129 2 : let doc = d.get(&t, "urn:a").await.expect("get").expect("doc");
2130 2 : assert_eq!(doc["scope"][0]["value"], 7, "{doc}");
2131 2 : }
2132 :
2133 : #[tokio::test]
2134 2 : async fn no_temporal_degrades_without_panicking() {
2135 2 : let t = TenantId::new("t").expect("tenant");
2136 2 : let d: std::sync::Arc<dyn TemporalDriver> = std::sync::Arc::new(NoTemporal);
2137 2 : assert!(!d.supported());
2138 : // client-facing reads: the spec error, not a panic
2139 2 : let e = match d
2140 2 : .query_temporal(&t, &filter::TemporalFilter::default())
2141 2 : .await
2142 : {
2143 2 : Err(e) => e,
2144 0 : Ok(_) => panic!("query_temporal on NoTemporal must be unsupported"),
2145 : };
2146 2 : assert_eq!(e.status(), 422);
2147 2 : assert_eq!(e.kind(), "OperationNotSupported");
2148 : // internal bookkeeping: benign no-ops
2149 2 : assert!(d
2150 2 : .temporal_append(&t, "x", &Value::Null, &Value::Null)
2151 2 : .await
2152 2 : .is_ok());
2153 2 : assert!(!d.delete(&t, "x").await.expect("ok"));
2154 2 : assert!(d.list(&t).await.expect("ok").is_empty());
2155 2 : assert!(d
2156 2 : .mutate::<(), ()>(&t, "x", |_| Ok(()))
2157 2 : .await
2158 2 : .expect("ok")
2159 2 : .is_none());
2160 2 : }
2161 : }
|