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 :
23 : /// 5.5.6: unexpected failures (database errors, timeouts) surface as
24 : /// InternalError. The client-visible detail is deliberately generic —
25 : /// driver internals (SQL text, constraint names, connection state) go to
26 : /// the server log only.
27 : #[cfg(feature = "postgres")]
28 14 : pub(crate) fn db(e: sqlx::Error) -> NgsiError {
29 : // The store's own spec errors travel out through the same sqlx channel
30 : // (the signature is fixed by the callers), so recover them — by VALUE, so
31 : // the variant and therefore the status survive — before the generic
32 : // mapping turns them all into a 500.
33 14 : if let sqlx::Error::Configuration(b) = e {
34 8 : return match b.downcast::<NgsiError>() {
35 6 : Ok(n) => *n,
36 2 : Err(b) => {
37 2 : tracing::error!("database error: {b}");
38 2 : NgsiError::InternalError("database error".into())
39 : }
40 : };
41 6 : }
42 : // The pool handed out no connection inside its acquire timeout: the
43 : // broker is holding every connection it may hold and the caller waited
44 : // the whole wall. That is overload, not a fault — the binding answers it
45 : // 503 with Retry-After (`negotiate::ApiError::Overloaded`), so the detail
46 : // is the constant both ends name.
47 6 : if matches!(e, sqlx::Error::PoolTimedOut) {
48 2 : metrics::counter!("antares_pg_pool_timeouts_total").increment(1);
49 2 : tracing::warn!(
50 : "connection pool exhausted: no connection within the acquire timeout \
51 : (raise ANTARES_PG_POOL, or the request rate is above what this \
52 : database can serve)"
53 : );
54 2 : return NgsiError::InternalError(antares_model::error::DB_OVERLOADED.into());
55 4 : }
56 : // 5.5.2: "database timeouts" are InternalError. SQLSTATE 57014 is the
57 : // session's statement_timeout firing — named in the detail so a wall hit
58 : // reads differently from a broken query in the operator's log.
59 4 : if let sqlx::Error::Database(d) = &e {
60 0 : if d.code().as_deref() == Some("57014") {
61 0 : tracing::warn!("database statement timeout: {d}");
62 0 : return NgsiError::InternalError("database statement timeout".into());
63 0 : }
64 4 : }
65 4 : tracing::error!("database error: {e}");
66 4 : NgsiError::InternalError("database error".into())
67 14 : }
68 :
69 : /// 4.22: the read-boundary "now" — every entity read strips expired docs and
70 : /// instances against this stamp (UTC Z, millisecond precision, the same form
71 : /// the broker writes into system timestamps).
72 22595 : pub(crate) fn now_utc() -> String {
73 22595 : chrono::Utc::now().to_rfc3339_opts(chrono::SecondsFormat::Millis, true)
74 22595 : }
75 :
76 : #[cfg(feature = "postgres")]
77 62345 : fn doc_kind(kind: Kind) -> Result<DocKind, NgsiError> {
78 62345 : match kind {
79 767 : Kind::Subscription => Ok(DocKind::Subscription),
80 20949 : Kind::Registration => Ok(DocKind::Registration),
81 1643 : Kind::CSourceSubscription => Ok(DocKind::CSourceSubscription),
82 18803 : Kind::Snapshot => Ok(DocKind::Snapshot),
83 19055 : Kind::EntityMap => Ok(DocKind::EntityMap),
84 1124 : Kind::DistSub => Ok(DocKind::DistSub),
85 4 : Kind::DeadLetter => Ok(DocKind::DeadLetter),
86 : // Entities and Temporal Representations have their own tables and
87 : // never reach the document store. Every caller routes them to their
88 : // own match arm first; one that does not has mismatched its arms,
89 : // and the mistake stays inside the one request.
90 0 : Kind::Entity | Kind::Temporal => Err(NgsiError::InternalError(format!(
91 0 : "{kind:?} is not a document-store kind"
92 0 : ))),
93 : }
94 62345 : }
95 :
96 : /// Postgres backend bundle: one pool, three table-family stores, plus the
97 : /// change hook (the memory store emits its own; here the seam emits).
98 : #[cfg(feature = "postgres")]
99 : pub struct PgBackend {
100 : pub entities: PgEntityStore,
101 : pub temporal: PgTemporalStore,
102 : pub docs: PgDocStore,
103 : hook: std::sync::RwLock<Option<ChangeHook>>,
104 : /// Server and extension versions, read once at startup
105 : /// (`with_version`); `/q/health` serves this copy rather than querying
106 : /// the database on a polled endpoint.
107 : version: Value,
108 : }
109 :
110 : #[cfg(feature = "postgres")]
111 : impl PgBackend {
112 49 : pub fn new(pool: sqlx::postgres::PgPool) -> Self {
113 49 : Self {
114 49 : entities: PgEntityStore::new(pool.clone()),
115 49 : temporal: PgTemporalStore::new(pool.clone()),
116 49 : docs: PgDocStore::new(pool),
117 49 : hook: std::sync::RwLock::new(None),
118 49 : version: Value::Object(serde_json::Map::new()),
119 49 : }
120 49 : }
121 :
122 : /// Attach what the startup probe read from the server
123 : /// (`pg::version_info`), so `/q/health` can answer it without a query.
124 4 : pub fn with_version(mut self, version: Value) -> Self {
125 4 : self.version = version;
126 4 : self
127 4 : }
128 :
129 : /// Poison recovery (`into_inner`) is deliberate, the same choice the
130 : /// memory arm records: the hook runs real code over attacker-shaped JSON,
131 : /// and a panic inside it must unwind one request, not poison this lock
132 : /// and panic every later entity write until the process restarts.
133 5842 : async fn emit(&self, tenant: &TenantId, before: Option<Value>, after: Option<Value>) {
134 : // cloned out of the lock first: a read guard cannot be held across
135 : // the await the hook now costs.
136 5842 : let hook = self
137 5842 : .hook
138 5842 : .read()
139 5842 : .unwrap_or_else(std::sync::PoisonError::into_inner)
140 5842 : .clone();
141 5842 : if let Some(h) = hook {
142 576 : h(tenant, before, after).await;
143 5266 : }
144 5842 : }
145 : }
146 :
147 : /// What `antares-api` sees. No core crate names redb or sqlx.
148 : // One AnyStore exists per process — variant size difference is irrelevant.
149 : #[allow(clippy::large_enum_variant)]
150 : pub enum AnyStore {
151 : Mem(Store),
152 : #[cfg(feature = "postgres")]
153 : Pg(PgBackend),
154 : }
155 :
156 : /// A row's `id`, or `""` for one without: an id-less row sorts first and is
157 : /// never skipped past, so a keyset walk over it cannot lose it. Only the
158 : /// Postgres arm sorts rows it read whole; the memory arm's maps are already
159 : /// keyed by id.
160 : #[cfg(feature = "postgres")]
161 0 : fn row_id(v: &Value) -> &str {
162 0 : v.get("id").and_then(Value::as_str).unwrap_or_default()
163 0 : }
164 :
165 : impl AnyStore {
166 : /// Readiness ping: can the store answer a trivial request
167 : /// RIGHT NOW? Memory/file are in-process (always ready); the Pg arm runs
168 : /// `SELECT 1` so a lost database (failover, network partition) flips
169 : /// /q/ready to 503 and the Service stops routing to this pod.
170 12 : pub async fn ping(&self) -> Result<(), NgsiError> {
171 12 : match self {
172 12 : AnyStore::Mem(_) => Ok(()),
173 : #[cfg(feature = "postgres")]
174 0 : AnyStore::Pg(p) => async {
175 0 : sqlx::query("SELECT 1")
176 0 : .execute(p.docs.pool())
177 0 : .await
178 0 : .map(|_| ())
179 0 : }
180 0 : .await
181 0 : .map_err(db),
182 : }
183 12 : }
184 :
185 : /// What this store runs on, for `/q/health`. The memory and file modes
186 : /// are one backend with two durability shapes and must not read as the
187 : /// same thing; the Postgres arm serves what the startup probe read.
188 182 : pub fn version_info(&self) -> Value {
189 182 : match self {
190 166 : AnyStore::Mem(s) => serde_json::json!({
191 166 : "engine": if s.shadowed() { "redb" } else { "memory" },
192 : }),
193 : #[cfg(feature = "postgres")]
194 16 : AnyStore::Pg(p) => p.version.clone(),
195 : }
196 182 : }
197 :
198 : /// (Queued writers, peak) behind the single redb committer, reported
199 : /// only by a store that has one: `None` for the Pg arm (Postgres has no
200 : /// single-writer commit queue) and for a pure in-memory store, whose
201 : /// lock depth is not the fsync queue this number stands for.
202 94 : pub fn commit_queue(&self) -> Option<(usize, usize)> {
203 94 : match self {
204 88 : AnyStore::Mem(s) => s.shadowed().then(|| s.commit_queue()),
205 : #[cfg(feature = "postgres")]
206 6 : AnyStore::Pg(_) => None,
207 : }
208 94 : }
209 :
210 : /// The last step of the drain: close the connection pool so in-flight
211 : /// transactions finish and the server sees a clean disconnect instead of
212 : /// N abandoned backends. A no-op for the memory/file arm, whose durability
213 : /// is already commit-before-ack — there is nothing buffered to lose.
214 24 : pub async fn close(&self) {
215 24 : match self {
216 20 : AnyStore::Mem(_) => {}
217 : #[cfg(feature = "postgres")]
218 4 : AnyStore::Pg(p) => p.docs.pool().close().await,
219 : }
220 24 : }
221 :
222 566 : pub fn set_change_hook(&self, h: ChangeHook) {
223 566 : match self {
224 560 : AnyStore::Mem(s) => s.set_change_hook(h),
225 : #[cfg(feature = "postgres")]
226 6 : AnyStore::Pg(p) => {
227 6 : *p.hook
228 6 : .write()
229 6 : .unwrap_or_else(std::sync::PoisonError::into_inner) = Some(h)
230 : }
231 : }
232 566 : }
233 :
234 : /// Turn the same-tx outbox producer on (bus=nats). The memory arm has
235 : /// no outbox — the broker's wiring rejects bus=nats without a Pg store,
236 : /// so this is unreachable there by construction.
237 2 : pub fn set_outbox(
238 2 : &self,
239 2 : #[cfg_attr(not(feature = "postgres"), allow(unused_variables))] on: bool,
240 2 : ) {
241 2 : match self {
242 0 : AnyStore::Mem(_) => {}
243 : #[cfg(feature = "postgres")]
244 2 : AnyStore::Pg(p) => p.entities.set_outbox(on),
245 : }
246 2 : }
247 :
248 : /// Outbox drain: oldest-first page of pending rows `(seq, tenant, event)`.
249 744 : pub async fn outbox_peek(
250 744 : &self,
251 744 : #[cfg_attr(not(feature = "postgres"), allow(unused_variables))] limit: i64,
252 744 : ) -> Result<Vec<(i64, String, Value)>, NgsiError> {
253 744 : match self {
254 8 : AnyStore::Mem(_) => Ok(Vec::new()),
255 : #[cfg(feature = "postgres")]
256 736 : AnyStore::Pg(p) => super::pg::outbox::peek(p.docs.pool(), limit)
257 736 : .await
258 736 : .map_err(db),
259 : }
260 744 : }
261 :
262 : /// Outbox drain: delete EXACTLY the published rows (never a blanket
263 : /// `seq <= max`, which loses a row committing between peek and ack).
264 0 : pub async fn outbox_ack(
265 0 : &self,
266 0 : #[cfg_attr(not(feature = "postgres"), allow(unused_variables))] seqs: &[i64],
267 0 : ) -> Result<u64, NgsiError> {
268 0 : match self {
269 0 : AnyStore::Mem(_) => Ok(0),
270 : #[cfg(feature = "postgres")]
271 0 : AnyStore::Pg(p) => super::pg::outbox::ack(p.docs.pool(), seqs)
272 0 : .await
273 0 : .map_err(db),
274 : }
275 0 : }
276 :
277 : /// Outbox drain: keep the rows of events the bus could not carry whole.
278 : /// The memory arm has no outbox, so nothing is ever retained there.
279 0 : pub async fn outbox_retain(
280 0 : &self,
281 0 : #[cfg_attr(not(feature = "postgres"), allow(unused_variables))] tenant: &TenantId,
282 0 : #[cfg_attr(not(feature = "postgres"), allow(unused_variables))] seqs: &[i64],
283 0 : ) -> Result<u64, NgsiError> {
284 0 : match self {
285 0 : AnyStore::Mem(_) => Ok(0),
286 : #[cfg(feature = "postgres")]
287 0 : AnyStore::Pg(p) => super::pg::outbox::retain(p.docs.pool(), tenant, seqs)
288 0 : .await
289 0 : .map_err(db),
290 : }
291 0 : }
292 :
293 : /// The whole event behind a claim-check reference, `None` once reaped.
294 2 : pub async fn outbox_event(
295 2 : &self,
296 2 : #[cfg_attr(not(feature = "postgres"), allow(unused_variables))] seq: i64,
297 2 : #[cfg_attr(not(feature = "postgres"), allow(unused_variables))] tenant: &TenantId,
298 2 : ) -> Result<Option<Value>, NgsiError> {
299 2 : match self {
300 0 : AnyStore::Mem(_) => Ok(None),
301 : #[cfg(feature = "postgres")]
302 2 : AnyStore::Pg(p) => super::pg::outbox::event(p.docs.pool(), seq, tenant)
303 2 : .await
304 2 : .map_err(db),
305 : }
306 2 : }
307 :
308 : /// Create one document of `kind`; `false` when that id is already taken.
309 18927 : pub async fn create(
310 18927 : &self,
311 18927 : tenant: &TenantId,
312 18927 : kind: Kind,
313 18927 : id: &str,
314 18927 : doc: Value,
315 18927 : ) -> Result<bool, NgsiError> {
316 18927 : match self {
317 17047 : AnyStore::Mem(s) => Ok(s.create(tenant, kind, id, doc).await),
318 : #[cfg(feature = "postgres")]
319 1880 : AnyStore::Pg(p) => {
320 1880 : let created = match kind {
321 688 : Kind::Entity => p.entities.create(tenant, id, &doc).await.map_err(db)?,
322 126 : Kind::Temporal => p.temporal.create(tenant, id, &doc).await.map_err(db)?,
323 : _ => {
324 1066 : let dk = doc_kind(kind)?;
325 1066 : p.docs.create(tenant, dk, id, &doc).await.map_err(db)?
326 : }
327 : };
328 1880 : if created && kind == Kind::Entity {
329 686 : p.emit(tenant, None, Some(doc)).await;
330 1194 : }
331 1880 : Ok(created)
332 : }
333 : }
334 18927 : }
335 :
336 : /// Batch create (entities only): one multi-row statement on the Pg
337 : /// arm, per-item loop on the memory arm. Created-flags in input order.
338 66 : pub async fn batch_create(
339 66 : &self,
340 66 : tenant: &TenantId,
341 66 : items: Vec<(String, Value)>,
342 66 : ) -> Result<Vec<bool>, NgsiError> {
343 66 : match self {
344 62 : AnyStore::Mem(s) => {
345 62 : let mut out = Vec::with_capacity(items.len());
346 4936 : for (id, doc) in items {
347 4936 : out.push(s.create(tenant, Kind::Entity, &id, doc).await);
348 : }
349 62 : Ok(out)
350 : }
351 : #[cfg(feature = "postgres")]
352 4 : AnyStore::Pg(p) => {
353 4 : let flags = p.entities.batch_create(tenant, &items).await.map_err(db)?;
354 4 : for ((_, doc), created) in items.into_iter().zip(&flags) {
355 4 : if *created {
356 4 : p.emit(tenant, None, Some(doc)).await;
357 0 : }
358 : }
359 4 : Ok(flags)
360 : }
361 : }
362 66 : }
363 :
364 : /// Batch delete (entities only): deleted-flags in input order; a
365 : /// duplicate id in the input deletes once and 404s the second time,
366 : /// matching the per-item loop's semantics (5.5.11.4).
367 198 : pub async fn batch_delete(
368 198 : &self,
369 198 : tenant: &TenantId,
370 198 : ids: &[String],
371 198 : ) -> Result<Vec<bool>, NgsiError> {
372 198 : match self {
373 110 : AnyStore::Mem(s) => {
374 110 : let mut out = Vec::with_capacity(ids.len());
375 4244 : for id in ids {
376 4244 : out.push(s.delete(tenant, Kind::Entity, id).await);
377 : }
378 110 : Ok(out)
379 : }
380 : #[cfg(feature = "postgres")]
381 88 : AnyStore::Pg(p) => {
382 88 : let deleted = p.entities.batch_delete(tenant, ids).await.map_err(db)?;
383 88 : let mut prev: std::collections::HashMap<String, Value> =
384 88 : deleted.into_iter().collect();
385 88 : let mut out = Vec::with_capacity(ids.len());
386 1356 : for id in ids {
387 1356 : match prev.remove(id) {
388 1206 : Some(before) => {
389 1206 : p.emit(tenant, Some(before), None).await;
390 1206 : out.push(true);
391 : }
392 150 : None => out.push(false),
393 : }
394 : }
395 88 : Ok(out)
396 : }
397 : }
398 198 : }
399 :
400 15114 : pub async fn upsert(
401 15114 : &self,
402 15114 : tenant: &TenantId,
403 15114 : kind: Kind,
404 15114 : id: &str,
405 15114 : doc: Value,
406 15114 : ) -> Result<bool, NgsiError> {
407 15114 : match self {
408 12674 : AnyStore::Mem(s) => Ok(s.upsert(tenant, kind, id, doc).await),
409 : #[cfg(feature = "postgres")]
410 2440 : AnyStore::Pg(p) => match kind {
411 : Kind::Entity => {
412 : // replace-or-create without a pre-read: try the replace
413 : // first (captures the true before-image under the row
414 : // lock), fall back to create, and on a lost create race
415 : // replace after all.
416 2420 : let mut prev: Option<Value> = None;
417 : // one shape, two places: a closure cannot hold the await
418 : // and an async closure is not a stable language feature,
419 : // so the replace is written once and expanded twice
420 : macro_rules! replace {
421 : () => {
422 : p.entities
423 2 : .mutate(tenant, id, |d| {
424 2 : prev = Some(d.clone());
425 2 : *d = doc.clone();
426 2 : Ok::<(), std::convert::Infallible>(())
427 2 : })
428 : .await
429 : .map_err(db)?
430 : .is_some()
431 : };
432 : }
433 2420 : let mut existed = replace!();
434 2420 : if !existed {
435 2418 : if p.entities.create(tenant, id, &doc).await.map_err(db)? {
436 2418 : existed = false;
437 2418 : } else {
438 : // lost the create race — replace instead
439 0 : existed = replace!();
440 : }
441 2 : }
442 2420 : p.emit(tenant, prev, Some(doc)).await;
443 2420 : Ok(existed)
444 : }
445 : Kind::Temporal => {
446 0 : if p.temporal.create(tenant, id, &doc).await.map_err(db)? {
447 0 : Ok(false)
448 : } else {
449 0 : p.temporal
450 0 : .mutate(tenant, id, |d| {
451 0 : *d = doc.clone();
452 0 : Ok::<(), std::convert::Infallible>(())
453 0 : })
454 0 : .await
455 0 : .map_err(db)?;
456 0 : Ok(true)
457 : }
458 : }
459 : _ => {
460 20 : let dk = doc_kind(kind)?;
461 20 : p.docs.upsert(tenant, dk, id, &doc).await.map_err(db)
462 : }
463 : },
464 : }
465 15114 : }
466 :
467 20671 : pub async fn get(
468 20671 : &self,
469 20671 : tenant: &TenantId,
470 20671 : kind: Kind,
471 20671 : id: &str,
472 20671 : ) -> Result<Option<Value>, NgsiError> {
473 20671 : let doc = match self {
474 18849 : AnyStore::Mem(s) => s.get(tenant, kind, id),
475 : #[cfg(feature = "postgres")]
476 1822 : AnyStore::Pg(p) => match kind {
477 126 : Kind::Entity => p.entities.get(tenant, id).await.map_err(db)?,
478 96 : Kind::Temporal => p.temporal.get(tenant, id).await.map_err(db)?,
479 1600 : _ => p.docs.get(tenant, doc_kind(kind)?, id).await.map_err(db)?,
480 : },
481 : };
482 : // 4.22: an expired entity is invalid context — a read serves it to
483 : // no one, whichever arm stored it (the Pg sweep lags by design).
484 20671 : if kind == Kind::Entity {
485 11338 : let now = now_utc();
486 11338 : if let Some(mut d) = doc {
487 9876 : if crate::store::filter::strip_expired(&mut d, &now) {
488 14 : return Ok(None);
489 9862 : }
490 9862 : return Ok(Some(d));
491 1462 : }
492 1462 : return Ok(None);
493 9333 : }
494 9333 : Ok(doc)
495 20671 : }
496 :
497 10599 : pub async fn delete(&self, tenant: &TenantId, kind: Kind, id: &str) -> Result<bool, NgsiError> {
498 10599 : match self {
499 6219 : AnyStore::Mem(s) => Ok(s.delete(tenant, kind, id).await),
500 : #[cfg(feature = "postgres")]
501 4380 : AnyStore::Pg(p) => match kind {
502 : Kind::Entity => {
503 : // the before-image comes from the DELETE's own RETURNING —
504 : // same transaction, never a separate racy read
505 1350 : let prev = p.entities.delete(tenant, id).await.map_err(db)?;
506 1350 : let hit = prev.is_some();
507 1350 : if hit {
508 1278 : p.emit(tenant, prev, None).await;
509 72 : }
510 1350 : Ok(hit)
511 : }
512 2534 : Kind::Temporal => p.temporal.delete(tenant, id).await.map_err(db),
513 496 : _ => p.docs.delete(tenant, doc_kind(kind)?, id).await.map_err(db),
514 : },
515 : }
516 10599 : }
517 :
518 : /// Delete one entity only if `keep` accepts the stored document, read
519 : /// and removed under one lock (see the driver trait).
520 2252 : pub async fn delete_entity_if(
521 2252 : &self,
522 2252 : tenant: &TenantId,
523 2252 : id: &str,
524 2252 : keep: &(dyn for<'v> Fn(&'v Value) -> bool + Sync),
525 2252 : ) -> Result<bool, NgsiError> {
526 : // 4.22 is applied to the document `keep` judges, exactly as `get`
527 : // applies it: an expired instance is not there to be matched on.
528 2252 : let now = now_utc();
529 2252 : let judge = |d: &Value| {
530 472 : let mut d = d.clone();
531 472 : !crate::store::filter::strip_expired(&mut d, &now) && keep(&d)
532 472 : };
533 2252 : match self {
534 1164 : AnyStore::Mem(s) => Ok(s.delete_if(tenant, id, &judge).await.is_some()),
535 : #[cfg(feature = "postgres")]
536 1088 : AnyStore::Pg(p) => {
537 : // the before-image comes from the DELETE's own RETURNING —
538 : // same transaction, never a separate racy read
539 1088 : let prev = p.entities.delete_if(tenant, id, &judge).await.map_err(db)?;
540 1088 : let hit = prev.is_some();
541 1088 : if hit {
542 214 : p.emit(tenant, prev, None).await;
543 874 : }
544 1088 : Ok(hit)
545 : }
546 : }
547 2252 : }
548 :
549 10207 : pub async fn list(&self, tenant: &TenantId, kind: Kind) -> Result<Vec<Value>, NgsiError> {
550 10207 : let mut rows = match self {
551 7576 : AnyStore::Mem(s) => s.list(tenant, kind),
552 : #[cfg(feature = "postgres")]
553 2631 : AnyStore::Pg(p) => match kind {
554 18 : Kind::Entity => p.entities.list(tenant).await.map_err(db)?,
555 4 : Kind::Temporal => p.temporal.list(tenant).await.map_err(db)?,
556 2609 : _ => p.docs.list(tenant, doc_kind(kind)?).await.map_err(db)?,
557 : },
558 : };
559 10207 : if kind == Kind::Entity {
560 71 : let now = now_utc();
561 5370 : rows.retain_mut(|d| !crate::store::filter::strip_expired(d, &now));
562 10136 : }
563 10207 : Ok(rows)
564 10207 : }
565 :
566 : /// One id-ordered page of documents (see `CurrentStateDriver::list_page`).
567 61928 : pub async fn list_page(
568 61928 : &self,
569 61928 : tenant: &TenantId,
570 61928 : kind: Kind,
571 61928 : after: Option<&str>,
572 61928 : limit: usize,
573 61928 : ) -> Result<Vec<Value>, NgsiError> {
574 61928 : match self {
575 5983 : AnyStore::Mem(s) => Ok(s.list_page(tenant, kind, after, limit)),
576 : #[cfg(feature = "postgres")]
577 55945 : AnyStore::Pg(p) => match kind {
578 : // Not doc kinds: they live in their own tables, behind their
579 : // own readers, and `doc_kind` refuses them.
580 : Kind::Entity => {
581 9 : let mut rows = p
582 9 : .entities
583 9 : .list_page(tenant, after, i64::try_from(limit).unwrap_or(i64::MAX))
584 9 : .await
585 9 : .map_err(db)?;
586 : // 4.22: the statement excluded expired ENTITIES, so no
587 : // document leaves this page and its length still means
588 : // what the caller reads it as; expired INSTANCES are
589 : // stripped here, at the read boundary, as everywhere else.
590 9 : let now = now_utc();
591 1014 : rows.retain_mut(|d| !crate::store::filter::strip_expired(d, &now));
592 9 : Ok(rows)
593 : }
594 : // ponytail: temporal is sliced from the whole list, so a
595 : // tenant large enough still refuses this read for volume; the
596 : // upgrade is a keyset reader over the instance rows.
597 : // Temporal has no keyset reader of its own: its documents are
598 : // reconstructed from the instance rows, where the volume that
599 : // needs bounding is instances and not entities — the one kind
600 : // that does not yet honour `list_page`'s contract, and the
601 : // reason nothing internal walks temporal state.
602 : Kind::Temporal => {
603 0 : let mut rows = AnyStore::list(self, tenant, kind).await?;
604 0 : rows.sort_by(|a, b| row_id(a).cmp(row_id(b)));
605 0 : Ok(rows
606 0 : .into_iter()
607 0 : .skip_while(|r| after.is_some_and(|a| row_id(r) <= a))
608 0 : .take(limit)
609 0 : .collect())
610 : }
611 55936 : _ => p
612 55936 : .docs
613 55936 : .list_page(
614 55936 : tenant,
615 55936 : doc_kind(kind)?,
616 55936 : after,
617 55936 : i64::try_from(limit).unwrap_or(i64::MAX),
618 : )
619 55936 : .await
620 55936 : .map_err(db),
621 : },
622 : }
623 61928 : }
624 :
625 : /// One id-ordered window of documents and the total (see
626 : /// `CurrentStateDriver::list_slice`).
627 192 : pub async fn list_slice(
628 192 : &self,
629 192 : tenant: &TenantId,
630 192 : kind: Kind,
631 192 : offset: usize,
632 192 : limit: usize,
633 192 : ) -> Result<(Vec<Value>, usize), NgsiError> {
634 192 : match self {
635 130 : AnyStore::Mem(s) => Ok(s.list_slice(tenant, kind, offset, limit)),
636 : #[cfg(feature = "postgres")]
637 62 : AnyStore::Pg(p) => match kind {
638 : // Not doc kinds; sliced from the whole list, like `list_page`.
639 : Kind::Entity | Kind::Temporal => {
640 0 : let mut rows = AnyStore::list(self, tenant, kind).await?;
641 0 : rows.sort_by(|a, b| row_id(a).cmp(row_id(b)));
642 0 : let total = rows.len();
643 0 : Ok((rows.into_iter().skip(offset).take(limit).collect(), total))
644 : }
645 : _ => {
646 62 : let (page, total) = p
647 62 : .docs
648 62 : .list_slice(
649 62 : tenant,
650 62 : doc_kind(kind)?,
651 62 : i64::try_from(offset).unwrap_or(i64::MAX),
652 62 : i64::try_from(limit).unwrap_or(i64::MAX),
653 : )
654 62 : .await
655 62 : .map_err(db)?;
656 62 : Ok((page, usize::try_from(total).unwrap_or(usize::MAX)))
657 : }
658 : },
659 : }
660 192 : }
661 :
662 : /// 5.12 registration candidates for these entity ids / types. The Pg arm
663 : /// reads the `csource_index` rows (an indexed narrowing); `memory`/`file`
664 : /// have nothing to push into and return the same snapshot `list` does.
665 : /// Either way the result is a SUPERSET — the caller's matcher decides
666 : /// every 5.12 condition, this only avoids reading registrations that
667 : /// cannot match on id or type. `types` must be expanded plain type IRIs
668 : /// (what the registration write stored); a caller holding terms or a 4.17
669 : /// selection expression passes `None` and narrows on ids alone.
670 15924 : pub async fn matching_registrations(
671 15924 : &self,
672 15924 : tenant: &TenantId,
673 15924 : #[cfg_attr(not(feature = "postgres"), allow(unused_variables))] ids: Option<&[String]>,
674 15924 : #[cfg_attr(not(feature = "postgres"), allow(unused_variables))] types: Option<&[String]>,
675 15924 : ) -> Result<Vec<Value>, NgsiError> {
676 15924 : match self {
677 12462 : AnyStore::Mem(s) => Ok(s.list(tenant, Kind::Registration)),
678 : #[cfg(feature = "postgres")]
679 3462 : AnyStore::Pg(p) => p
680 3462 : .docs
681 3462 : .matching_registrations(tenant, ids, types)
682 3462 : .await
683 3462 : .map_err(db),
684 : }
685 15924 : }
686 :
687 : /// Query Entities with the filter pushed down where the backend
688 : /// can take it. `memory`/`file` have nothing to push into — their
689 : /// entities are already in RAM — so they return the same snapshot `list`
690 : /// does (never `decided`, never `paged`). Either way the caller applies
691 : /// the exact filter afterwards unless the outcome says SQL already did.
692 1742 : pub async fn query_entities(
693 1742 : &self,
694 1742 : tenant: &TenantId,
695 1742 : #[cfg_attr(not(feature = "postgres"), allow(unused_variables))]
696 1742 : f: &crate::store::filter::EntityFilter<'_>,
697 1742 : ) -> Result<crate::store::filter::QueryOutcome, NgsiError> {
698 1742 : let mut outcome = match self {
699 1494 : AnyStore::Mem(s) => crate::store::filter::QueryOutcome {
700 1494 : rows: s.list(tenant, Kind::Entity),
701 1494 : decided: false,
702 1494 : paged: false,
703 1494 : total: None,
704 1494 : },
705 : #[cfg(feature = "postgres")]
706 248 : AnyStore::Pg(p) => p.entities.query(tenant, f).await.map_err(db)?,
707 : };
708 : // 4.22: the Pg arm already excludes expired ENTITIES in SQL (so
709 : // paging/totals stay exact); instance stripping — and the whole job
710 : // on the memory arm — happens here at the read boundary.
711 1742 : let now = now_utc();
712 1742 : outcome
713 1742 : .rows
714 19902 : .retain_mut(|d| !crate::store::filter::strip_expired(d, &now));
715 1742 : Ok(outcome)
716 1742 : }
717 :
718 : /// Query Temporal Evolution with entity narrowing, instance-window
719 : /// pruning AND entity paging pushed down. Same
720 : /// contract: the API's window() is the arbiter; the memory arm returns
721 : /// the full snapshot, never paged.
722 566 : pub async fn query_temporal(
723 566 : &self,
724 566 : tenant: &TenantId,
725 566 : #[cfg_attr(not(feature = "postgres"), allow(unused_variables))]
726 566 : f: &crate::store::filter::TemporalFilter<'_>,
727 566 : ) -> Result<crate::store::filter::TemporalOutcome, NgsiError> {
728 566 : let mut outcome = match self {
729 336 : AnyStore::Mem(s) => crate::store::filter::TemporalOutcome {
730 336 : rows: s.list(tenant, Kind::Temporal),
731 336 : paged: false,
732 336 : total: None,
733 336 : aggregated: false,
734 336 : },
735 : #[cfg(feature = "postgres")]
736 230 : AnyStore::Pg(p) => p.temporal.query(tenant, f).await.map_err(db)?,
737 : };
738 : // 4.22 on temporal reads: the Pg arm already dropped expired ENTITIES in
739 : // SQL (paging exact); this strips expired attribute INSTANCES, and does
740 : // the whole job on the memory arm (no SQL to push into).
741 566 : let now = now_utc();
742 566 : outcome
743 566 : .rows
744 1540 : .retain_mut(|d| !crate::store::filter::strip_expired(d, &now));
745 566 : Ok(outcome)
746 566 : }
747 :
748 : /// Auto-recording fast path: append instances to an
749 : /// entity's temporal evolution, creating the meta shell on first touch.
750 : /// Pg: pure multi-row INSERT — no history read, no doc rewrite. Memory:
751 : /// the create-or-extend the mirror always did, under the store lock.
752 : /// `shell` carries the meta members; `additions` maps attr IRI →
753 : /// instance array (instanceIds already stamped by the caller).
754 : ///
755 : /// Both arms record only for an entity that still exists: 5.6.6 deletes
756 : /// the entity and then the temporal evolution recorded for it, so an
757 : /// append overlapping the delete must not recreate history nothing will
758 : /// ever clean again. An instance marked `temporal_only` never holds the
759 : /// entities (they live in another backend) and records unconditionally;
760 : /// there the delete overlap stays a window the temporal delete closes.
761 5348 : pub async fn temporal_append(
762 5348 : &self,
763 5348 : tenant: &TenantId,
764 5348 : id: &str,
765 5348 : shell: &Value,
766 5348 : additions: &Value,
767 5348 : ) -> Result<(), NgsiError> {
768 5348 : match self {
769 5104 : AnyStore::Mem(s) => {
770 5104 : if !s.temporal_only && s.get(tenant, Kind::Entity, id).is_none() {
771 6 : return Ok(());
772 5098 : }
773 5098 : if s.get(tenant, Kind::Temporal, id).is_none() {
774 : // loser of a concurrent create race just extends below
775 5018 : let _ = s.create(tenant, Kind::Temporal, id, shell.clone()).await;
776 80 : }
777 5098 : s.mutate(tenant, Kind::Temporal, id, |doc| {
778 5098 : let target = doc.as_object_mut().ok_or(())?;
779 5098 : if let Some(adds) = additions.as_object() {
780 5296 : for (k, v) in adds {
781 5296 : let incoming: Vec<Value> = v.as_array().cloned().unwrap_or_default();
782 5296 : match target.get_mut(k).and_then(Value::as_array_mut) {
783 : // same instanceId = the same instance corrected
784 : // (the pg arm's ON CONFLICT DO UPDATE)
785 74 : Some(cur) => {
786 74 : for inst in incoming {
787 74 : let iid = inst.get("instanceId");
788 90 : match cur.iter_mut().find(|c| c.get("instanceId") == iid) {
789 10 : Some(slot) => *slot = inst,
790 64 : None => cur.push(inst),
791 : }
792 : }
793 : }
794 5222 : None => {
795 5222 : target.insert(k.clone(), Value::Array(incoming));
796 5222 : }
797 : }
798 : }
799 0 : }
800 5098 : Ok::<(), ()>(())
801 5098 : })
802 5098 : .await;
803 5098 : Ok(())
804 : }
805 : #[cfg(feature = "postgres")]
806 244 : AnyStore::Pg(p) => p
807 244 : .temporal
808 244 : .append(tenant, id, shell, additions)
809 244 : .await
810 244 : .map_err(db),
811 : }
812 5348 : }
813 :
814 : /// Mark this instance as the temporal half of a mixed deployment: the
815 : /// entities live in another backend, so appends never look for them here.
816 2 : pub fn temporal_only(mut self) -> Self {
817 2 : match &mut self {
818 2 : AnyStore::Mem(s) => s.temporal_only = true,
819 : #[cfg(feature = "postgres")]
820 0 : AnyStore::Pg(p) => p.temporal.temporal_only = true,
821 : }
822 2 : self
823 2 : }
824 :
825 : /// Retrieve Temporal Evolution with the same instance pruning.
826 272 : pub async fn get_temporal(
827 272 : &self,
828 272 : tenant: &TenantId,
829 272 : id: &str,
830 272 : #[cfg_attr(not(feature = "postgres"), allow(unused_variables))]
831 272 : f: &crate::store::filter::TemporalFilter<'_>,
832 272 : ) -> Result<Option<Value>, NgsiError> {
833 272 : let mut doc = match self {
834 250 : AnyStore::Mem(s) => s.get(tenant, Kind::Temporal, id),
835 : #[cfg(feature = "postgres")]
836 22 : AnyStore::Pg(p) => p.temporal.get_range(tenant, id, f).await.map_err(db)?,
837 : };
838 : // 4.22: expired entity → None (Pg already did this in SQL); otherwise
839 : // strip expired instances.
840 272 : if let Some(d) = &mut doc {
841 234 : let now = now_utc();
842 234 : if crate::store::filter::strip_expired(d, &now) {
843 0 : return Ok(None);
844 234 : }
845 38 : }
846 272 : Ok(doc)
847 272 : }
848 :
849 4745 : pub async fn mutate<T, E>(
850 4745 : &self,
851 4745 : tenant: &TenantId,
852 4745 : kind: Kind,
853 4745 : id: &str,
854 4745 : f: impl FnOnce(&mut Value) -> Result<T, E>,
855 4745 : ) -> Result<Option<Result<T, E>>, NgsiError> {
856 4745 : match self {
857 4539 : AnyStore::Mem(s) => Ok(s.mutate(tenant, kind, id, f).await),
858 : #[cfg(feature = "postgres")]
859 206 : AnyStore::Pg(p) => match kind {
860 : Kind::Entity => {
861 : // before/after captured for the change hook's
862 : // prev_payload INSIDE the row lock — a before-image read
863 : // in its own transaction can belong to a different version
864 : // than the one the lock serialized on.
865 40 : let mut before: Option<Value> = None;
866 40 : let mut after: Option<Value> = None;
867 40 : let r = p
868 40 : .entities
869 40 : .mutate(tenant, id, |d| {
870 36 : before = Some(d.clone());
871 36 : let r = f(d);
872 36 : if r.is_ok() {
873 26 : after = Some(d.clone());
874 26 : }
875 36 : r
876 36 : })
877 40 : .await
878 40 : .map_err(db)?;
879 40 : if let (Some(Ok(_)), Some(a)) = (&r, after) {
880 26 : if before.as_ref() != Some(&a) {
881 26 : p.emit(tenant, before, Some(a)).await;
882 0 : }
883 14 : }
884 40 : Ok(r)
885 : }
886 12 : Kind::Temporal => p.temporal.mutate(tenant, id, f).await.map_err(db),
887 : _ => {
888 : // FOR UPDATE + UPDATE in one tx: a bookkeeping writeback
889 : // racing a DELETE must never resurrect the row (047_06).
890 154 : let dk = doc_kind(kind)?;
891 154 : match p.docs.mutate(tenant, dk, id, f).await.map_err(db)? {
892 70 : Some(r) => Ok(Some(r)),
893 84 : None => Ok(None),
894 : }
895 : }
896 : },
897 : }
898 4745 : }
899 :
900 : /// Batch upsert with REPLACE semantics for entities:
901 : /// one statement + one transaction on the Pg arm, per-item loop on the
902 : /// memory arm. Created-flags in input order.
903 36 : pub async fn batch_upsert(
904 36 : &self,
905 36 : tenant: &TenantId,
906 36 : items: Vec<(String, Value)>,
907 36 : ) -> Result<Vec<bool>, NgsiError> {
908 36 : match self {
909 34 : AnyStore::Mem(s) => {
910 34 : let mut out = Vec::with_capacity(items.len());
911 48 : for (id, doc) in items {
912 48 : out.push(!s.upsert(tenant, Kind::Entity, &id, doc).await);
913 : }
914 34 : Ok(out)
915 : }
916 : #[cfg(feature = "postgres")]
917 2 : AnyStore::Pg(p) => {
918 2 : let out = p
919 2 : .entities
920 2 : .batch_upsert_replace(tenant, &items)
921 2 : .await
922 2 : .map_err(db)?;
923 4 : for ((_, doc), (_, prev)) in items.iter().zip(&out) {
924 4 : p.emit(tenant, prev.clone(), Some(doc.clone())).await;
925 : }
926 2 : Ok(out.into_iter().map(|(created, _)| created).collect())
927 : }
928 : }
929 36 : }
930 :
931 : /// Batch read-modify-write for entities: one transaction + one ordered
932 : /// lock set + one multi-row writeback on the Pg arm; per-item mutate on
933 : /// the memory arm. Results align with `ids` (`None` = absent).
934 42 : pub async fn batch_mutate<E>(
935 42 : &self,
936 42 : tenant: &TenantId,
937 42 : ids: &[String],
938 42 : mut f: impl FnMut(&str, &mut Value) -> Result<(), E>,
939 42 : ) -> Result<Vec<Option<Result<(), E>>>, NgsiError> {
940 42 : match self {
941 40 : AnyStore::Mem(s) => {
942 40 : let mut out = Vec::with_capacity(ids.len());
943 4074 : for id in ids {
944 4074 : out.push(s.mutate(tenant, Kind::Entity, id, |d| f(id, d)).await);
945 : }
946 40 : Ok(out)
947 : }
948 : #[cfg(feature = "postgres")]
949 2 : AnyStore::Pg(p) => {
950 : // hook images captured inside the lock, same as single mutate
951 2 : let mut images: Vec<(Value, Value)> = Vec::new();
952 2 : let r = p
953 2 : .entities
954 4 : .batch_mutate(tenant, ids, |id, d| {
955 4 : let before = d.clone();
956 4 : let r = f(id, d);
957 4 : if r.is_ok() && *d != before {
958 4 : images.push((before, d.clone()));
959 4 : }
960 4 : r
961 4 : })
962 2 : .await
963 2 : .map_err(db)?;
964 4 : for (before, after) in images {
965 4 : p.emit(tenant, Some(before), Some(after)).await;
966 : }
967 2 : Ok(r)
968 : }
969 : }
970 42 : }
971 :
972 : /// 4.22 GC for the memory/file arm (the Pg arm's sweep lives in the
973 : /// maintenance job, mode-switched in the broker). Returns reaped count.
974 776 : pub fn sweep_expired(&self) -> usize {
975 776 : match self {
976 382 : AnyStore::Mem(s) => s.sweep_expired(&now_utc()),
977 : // 4.22 reaping on the Pg arm is `pg::maintenance`'s job
978 : // (`reap_expired_entities` / `reap_expired_instances`), which
979 : // deletes in one indexed statement instead of walking the store;
980 : // reads refuse an expired document either way.
981 : #[cfg(feature = "postgres")]
982 394 : AnyStore::Pg(_) => 0,
983 : }
984 776 : }
985 :
986 : /// 5.5.10: does the Tenant exist? The default Tenant "implicitly exists";
987 : /// others exist once implicitly created by a create operation.
988 : ///
989 : /// It answers for CLIENT tenants. A tenant the broker minted for itself
990 : /// does hold a row — every write claims one, and that row is what keeps
991 : /// the tenant enumerable to `subscription_tenants` across a restart — but
992 : /// nothing asks this about one: the wall that asks sits outside the 6.3.22
993 : /// snapshot scoping and reads the header the caller sent, which cannot
994 : /// name an internal tenant. What a Snapshot's synthetic tenant holds is
995 : /// asserted by the Snapshot document; the row only accounts for it, and
996 : /// `tenant_ids` keeps it out of the customer-account listing.
997 354 : pub async fn tenant_exists(&self, tenant: &TenantId) -> Result<bool, NgsiError> {
998 354 : if tenant.as_str() == TenantId::DEFAULT {
999 14 : return Ok(true);
1000 340 : }
1001 340 : match self {
1002 292 : AnyStore::Mem(s) => Ok(s.tenant_exists(tenant)),
1003 : #[cfg(feature = "postgres")]
1004 48 : AnyStore::Pg(p) => {
1005 48 : async {
1006 48 : let row =
1007 48 : sqlx::query_scalar::<_, i32>("SELECT 1 FROM tenants WHERE tenant_id = $1")
1008 48 : .bind(tenant.as_str())
1009 48 : .fetch_optional(p.docs.pool())
1010 48 : .await
1011 48 : .map_err(db)?;
1012 48 : Ok(row.is_some())
1013 48 : }
1014 48 : .await
1015 : }
1016 : }
1017 354 : }
1018 :
1019 : /// Every tenant name the backend knows, sorted. One cheap query: at the
1020 : /// 10 000-tenant target (ADR-0001) an inventory that counted every kind
1021 : /// of every tenant would be 7 counts per tenant on one transaction, so
1022 : /// the counts live in `tenant_stats_one` and are paid per lookup.
1023 808 : pub async fn tenant_ids(&self) -> Result<Vec<String>, NgsiError> {
1024 808 : match self {
1025 410 : AnyStore::Mem(s) => Ok(s.tenant_ids()),
1026 : #[cfg(feature = "postgres")]
1027 398 : AnyStore::Pg(p) => {
1028 398 : async {
1029 398 : let mut rows: Vec<String> =
1030 398 : sqlx::query_scalar("SELECT tenant_id FROM tenants ORDER BY 1")
1031 398 : .fetch_all(p.docs.pool())
1032 398 : .await
1033 398 : .map_err(db)?;
1034 : // The broker's own tenants claim a row like any other
1035 : // write, because that row is also the enumeration the
1036 : // notification paths walk. The inventory is the list of
1037 : // customer accounts, so they are filtered on the way out.
1038 19509 : rows.retain(|t| !TenantId::is_reserved_str(t));
1039 : // 5.5.10: the default Tenant implicitly exists, whether or
1040 : // not a row was ever written for it.
1041 4370 : if !rows.iter().any(|t| t == TenantId::DEFAULT) {
1042 0 : rows.push(TenantId::DEFAULT.to_owned());
1043 0 : rows.sort();
1044 398 : }
1045 398 : Ok(rows)
1046 398 : }
1047 398 : .await
1048 : }
1049 : }
1050 808 : }
1051 :
1052 : /// What one tenant holds; `None` when it does not exist (5.5.10 keeps
1053 : /// the default Tenant existing even when empty).
1054 26 : pub async fn tenant_stats_one(
1055 26 : &self,
1056 26 : tenant: &TenantId,
1057 26 : ) -> Result<Option<antares_store::TenantStats>, NgsiError> {
1058 26 : if !self.tenant_exists(tenant).await? {
1059 4 : return Ok(None);
1060 22 : }
1061 22 : match self {
1062 20 : AnyStore::Mem(s) => Ok(Some(s.tenant_stats_one(tenant))),
1063 : #[cfg(feature = "postgres")]
1064 2 : AnyStore::Pg(p) => {
1065 2 : async {
1066 2 : let mut tx = p.docs.pool().begin().await.map_err(db)?;
1067 : // the tenant setting is what the RLS-guarded counts see
1068 2 : crate::store::pg::set_tenant(&mut tx, tenant)
1069 2 : .await
1070 2 : .map_err(db)?;
1071 2 : let created_at: Option<String> = sqlx::query_scalar(
1072 2 : "SELECT created_at::text FROM tenants WHERE tenant_id = $1",
1073 2 : )
1074 2 : .bind(tenant.as_str())
1075 2 : .fetch_optional(&mut *tx)
1076 2 : .await
1077 2 : .map_err(db)?;
1078 2 : let c: (i64, i64, i64, i64, i64, i64, i64) = sqlx::query_as(
1079 2 : "SELECT (SELECT count(*) FROM entities WHERE tenant_id = $1),
1080 2 : (SELECT count(*) FROM subscriptions WHERE tenant_id = $1),
1081 2 : (SELECT count(*) FROM csource_registrations WHERE tenant_id = $1),
1082 2 : (SELECT count(*) FROM csource_subscriptions WHERE tenant_id = $1),
1083 2 : (SELECT count(*) FROM snapshots WHERE tenant_id = $1),
1084 2 : (SELECT count(*) FROM entity_map_docs WHERE tenant_id = $1),
1085 2 : (SELECT count(*) FROM dist_subs WHERE tenant_id = $1)",
1086 2 : )
1087 2 : .bind(tenant.as_str())
1088 2 : .fetch_one(&mut *tx)
1089 2 : .await
1090 2 : .map_err(db)?;
1091 2 : tx.commit().await.map_err(db)?;
1092 2 : Ok(Some(antares_store::TenantStats {
1093 2 : tenant: tenant.as_str().to_owned(),
1094 2 : created_at,
1095 2 : entities: c.0 as u64,
1096 2 : subscriptions: c.1 as u64,
1097 2 : registrations: c.2 as u64,
1098 2 : csource_subscriptions: c.3 as u64,
1099 2 : snapshots: c.4 as u64,
1100 2 : entity_maps: c.5 as u64,
1101 2 : dist_subs: c.6 as u64,
1102 2 : }))
1103 2 : }
1104 2 : .await
1105 : }
1106 : }
1107 26 : }
1108 :
1109 : /// Purge the current-state half of one tenant in one transaction;
1110 : /// `false` when the tenant did not exist. The default tenant's row stays.
1111 76 : pub async fn purge_tenant(&self, tenant: &TenantId) -> Result<bool, NgsiError> {
1112 76 : match self {
1113 58 : AnyStore::Mem(s) => Ok(s.purge_tenant(tenant)),
1114 : #[cfg(feature = "postgres")]
1115 18 : AnyStore::Pg(p) => {
1116 18 : async {
1117 18 : let mut tx = p.docs.pool().begin().await.map_err(db)?;
1118 18 : crate::store::pg::set_tenant(&mut tx, tenant)
1119 18 : .await
1120 18 : .map_err(db)?;
1121 18 : let known = sqlx::query_scalar::<_, i32>(
1122 18 : "SELECT 1 FROM tenants WHERE tenant_id = $1 FOR UPDATE",
1123 18 : )
1124 18 : .bind(tenant.as_str())
1125 18 : .fetch_optional(&mut *tx)
1126 18 : .await
1127 18 : .map_err(db)?
1128 18 : .is_some();
1129 18 : if !known {
1130 2 : return Ok(false);
1131 16 : }
1132 176 : for table in [
1133 16 : "entities",
1134 16 : "subscriptions",
1135 16 : "csource_subscriptions",
1136 16 : "csource_registrations",
1137 16 : "csource_index",
1138 16 : "outbox",
1139 16 : "snapshots",
1140 16 : "entity_map_docs",
1141 16 : "dist_subs",
1142 16 : "dead_letters",
1143 16 : // ADR-0021: `tenant_id` is GENERATED from the row's kind
1144 16 : // and owner, so a Cached row's is NULL and this DELETE
1145 16 : // takes only what the Tenant stored itself.
1146 16 : "jsonld_contexts",
1147 16 : ] {
1148 176 : sqlx::query(sqlx::AssertSqlSafe(format!(
1149 176 : "DELETE FROM {table} WHERE tenant_id = $1"
1150 176 : )))
1151 176 : .bind(tenant.as_str())
1152 176 : .execute(&mut *tx)
1153 176 : .await
1154 176 : .map_err(db)?;
1155 : }
1156 16 : if tenant.as_str() != TenantId::DEFAULT {
1157 16 : sqlx::query("DELETE FROM tenants WHERE tenant_id = $1")
1158 16 : .bind(tenant.as_str())
1159 16 : .execute(&mut *tx)
1160 16 : .await
1161 16 : .map_err(db)?;
1162 0 : }
1163 16 : tx.commit().await.map_err(db)?;
1164 16 : Ok(true)
1165 18 : }
1166 18 : .await
1167 : }
1168 : }
1169 76 : }
1170 :
1171 692 : pub async fn subscription_tenants(&self) -> Result<Vec<String>, NgsiError> {
1172 692 : match self {
1173 666 : AnyStore::Mem(s) => Ok(s.subscription_tenants()),
1174 : // Pg answers the superset the contract allows: every known
1175 : // tenant. Narrowing it would need a cross-tenant read of
1176 : // `subscriptions`, and that table is deliberately outside the
1177 : // `antares.service` escape — a subscription document carries
1178 : // endpoint.receiverInfo, i.e. the credentials the notification
1179 : // is sent with. The callers list per tenant under set_tenant
1180 : // afterwards, so the extra names cost one empty list each.
1181 : #[cfg(feature = "postgres")]
1182 26 : AnyStore::Pg(p) => {
1183 26 : async {
1184 26 : let rows =
1185 26 : sqlx::query_scalar::<_, String>("SELECT tenant_id FROM tenants ORDER BY 1")
1186 26 : .fetch_all(p.docs.pool())
1187 26 : .await
1188 26 : .map_err(db)?;
1189 26 : Ok(rows)
1190 26 : }
1191 26 : .await
1192 : }
1193 : }
1194 692 : }
1195 :
1196 650 : pub async fn context_put(
1197 650 : &self,
1198 650 : tenant: Option<&TenantId>,
1199 650 : id: &str,
1200 650 : doc: Value,
1201 650 : ) -> Result<(), NgsiError> {
1202 : // ADR-0021, the write half no `WHERE` on an existing row can state:
1203 : // the row that ARRIVES must belong to the caller too, or a Tenant
1204 : // could store term mappings under another Tenant's name and decide
1205 : // what that Tenant's payloads mean (5.5.7). Postgres says the same
1206 : // thing as the policy's WITH CHECK; this holds under the roles that
1207 : // bypass RLS.
1208 650 : if !antares_store::context_row_visible(&doc, tenant) {
1209 6 : return Err(NgsiError::InternalError(
1210 6 : "@context belongs to another tenant".into(),
1211 6 : ));
1212 644 : }
1213 644 : match self {
1214 486 : AnyStore::Mem(s) => s.context_put(tenant, id, doc),
1215 : #[cfg(feature = "postgres")]
1216 158 : AnyStore::Pg(p) => {
1217 158 : let kind = doc
1218 158 : .get("kind")
1219 158 : .and_then(Value::as_str)
1220 158 : .unwrap_or("Cached")
1221 158 : .to_owned();
1222 158 : p.docs
1223 158 : .context_put(tenant, id, &doc, &kind)
1224 158 : .await
1225 158 : .map_err(db)
1226 : }
1227 : }
1228 650 : }
1229 :
1230 1176 : pub async fn context_get(
1231 1176 : &self,
1232 1176 : tenant: Option<&TenantId>,
1233 1176 : id: &str,
1234 1176 : ) -> Result<Option<Value>, NgsiError> {
1235 1176 : match self {
1236 854 : AnyStore::Mem(s) => Ok(s.context_get(tenant, id)),
1237 : #[cfg(feature = "postgres")]
1238 322 : AnyStore::Pg(p) => p.docs.context_get(tenant, id).await.map_err(db),
1239 : }
1240 1176 : }
1241 :
1242 222 : pub async fn context_delete(
1243 222 : &self,
1244 222 : tenant: Option<&TenantId>,
1245 222 : id: &str,
1246 222 : ) -> Result<bool, NgsiError> {
1247 222 : match self {
1248 124 : AnyStore::Mem(s) => Ok(s.context_delete(tenant, id)),
1249 : #[cfg(feature = "postgres")]
1250 98 : AnyStore::Pg(p) => p.docs.context_delete(tenant, id).await.map_err(db),
1251 : }
1252 222 : }
1253 :
1254 218 : pub async fn context_list_meta(
1255 218 : &self,
1256 218 : tenant: Option<&TenantId>,
1257 218 : ) -> Result<Vec<Value>, NgsiError> {
1258 218 : match self {
1259 152 : AnyStore::Mem(s) => Ok(s.context_list_meta(tenant)),
1260 : #[cfg(feature = "postgres")]
1261 66 : AnyStore::Pg(p) => p.docs.context_list_meta(tenant).await.map_err(db),
1262 : }
1263 218 : }
1264 : }
1265 :
1266 : #[cfg(all(test, feature = "postgres"))]
1267 : mod db_error_tests {
1268 : #[allow(unused_imports)]
1269 : use antares_model::NgsiError;
1270 :
1271 : /// An acquire timeout is overload, not a fault, and the HTTP binding
1272 : /// recognises it by this exact detail. If the two ends ever spell it
1273 : /// differently the broker answers 500 to a condition a client could
1274 : /// have retried, so the constant is asserted rather than the words.
1275 : #[test]
1276 2 : fn a_pool_timeout_is_marked_as_overload() {
1277 2 : let pd = super::db(sqlx::Error::PoolTimedOut).to_problem_details();
1278 2 : assert_eq!(pd.detail, antares_model::error::DB_OVERLOADED);
1279 2 : assert_eq!(pd.title, "InternalError");
1280 : // and no other sqlx error borrows the mark
1281 2 : assert_ne!(
1282 2 : super::db(sqlx::Error::RowNotFound)
1283 2 : .to_problem_details()
1284 : .detail,
1285 : antares_model::error::DB_OVERLOADED
1286 : );
1287 2 : }
1288 :
1289 : /// 5.5.6 InternalError: the RFC 7807 `detail` a client sees must be
1290 : /// generic — driver internals (SQL text, row counts, connection
1291 : /// strings) belong in the server log, never in the response body.
1292 : #[test]
1293 2 : fn db_error_detail_is_generic() {
1294 2 : let pd = super::db(sqlx::Error::RowNotFound).to_problem_details();
1295 2 : assert_eq!(pd.detail, "database error");
1296 2 : assert_eq!(pd.title, "InternalError");
1297 2 : assert!(
1298 2 : !pd.detail.contains("no rows"),
1299 : "sqlx internals leaked into the client-visible detail: {}",
1300 : pd.detail
1301 : );
1302 : // a configuration error that is NOT one of ours stays generic too
1303 2 : let pd = super::db(sqlx::Error::Configuration("boom".into())).to_problem_details();
1304 2 : assert_eq!(pd.detail, "database error");
1305 2 : assert!(!pd.detail.contains("boom"), "{}", pd.detail);
1306 2 : }
1307 :
1308 : /// A spec error the store raised itself travels out through the driver
1309 : /// error channel. Rebuilding it as a fixed variant forces every one of
1310 : /// them to that variant's status — a 400 BadRequestData would reach the
1311 : /// client as a 403.
1312 : #[test]
1313 2 : fn a_store_raised_spec_error_keeps_its_own_status() {
1314 6 : for (err, kind, status) in [
1315 2 : (NgsiError::BadRequestData("x".into()), "BadRequestData", 400),
1316 2 : (NgsiError::TooManyResults("x".into()), "TooManyResults", 403),
1317 2 : (NgsiError::AlreadyExists("x".into()), "AlreadyExists", 409),
1318 2 : ] {
1319 6 : let out = super::db(sqlx::Error::Configuration(Box::new(err)));
1320 6 : assert_eq!(out.kind(), kind);
1321 6 : assert_eq!(out.status(), status, "{kind} lost its status");
1322 : }
1323 2 : }
1324 : }
1325 :
1326 : // The driver seam: `AnyStore` carries both driver interfaces, delegating to
1327 : // the inherent methods above. New backends implement the traits directly —
1328 : // this enum stays an implementation detail of the built-in backends, no
1329 : // longer the API surface.
1330 : #[async_trait::async_trait]
1331 : impl antares_store::CurrentStateDriver for AnyStore {
1332 12 : async fn ping(&self) -> Result<(), NgsiError> {
1333 : AnyStore::ping(self).await
1334 12 : }
1335 90 : fn commit_queue(&self) -> Option<(usize, usize)> {
1336 90 : AnyStore::commit_queue(self)
1337 90 : }
1338 90 : fn version_info(&self) -> Value {
1339 90 : AnyStore::version_info(self)
1340 90 : }
1341 16 : async fn close(&self) {
1342 : AnyStore::close(self).await;
1343 16 : }
1344 566 : fn set_change_hook(&self, h: super::ChangeHook) {
1345 566 : AnyStore::set_change_hook(self, h);
1346 566 : }
1347 0 : fn set_outbox(&self, on: bool) {
1348 0 : AnyStore::set_outbox(self, on);
1349 0 : }
1350 744 : async fn outbox_peek(&self, limit: i64) -> Result<Vec<(i64, String, Value)>, NgsiError> {
1351 : AnyStore::outbox_peek(self, limit).await
1352 744 : }
1353 0 : async fn outbox_ack(&self, seqs: &[i64]) -> Result<u64, NgsiError> {
1354 : AnyStore::outbox_ack(self, seqs).await
1355 0 : }
1356 0 : async fn outbox_retain(&self, tenant: &TenantId, seqs: &[i64]) -> Result<u64, NgsiError> {
1357 : AnyStore::outbox_retain(self, tenant, seqs).await
1358 0 : }
1359 2 : async fn outbox_event(&self, seq: i64, tenant: &TenantId) -> Result<Option<Value>, NgsiError> {
1360 : AnyStore::outbox_event(self, seq, tenant).await
1361 2 : }
1362 : async fn create(
1363 : &self,
1364 : tenant: &TenantId,
1365 : kind: Kind,
1366 : id: &str,
1367 : doc: Value,
1368 18539 : ) -> Result<bool, NgsiError> {
1369 : AnyStore::create(self, tenant, kind, id, doc).await
1370 18539 : }
1371 : async fn batch_create(
1372 : &self,
1373 : tenant: &TenantId,
1374 : items: Vec<(String, Value)>,
1375 66 : ) -> Result<Vec<bool>, NgsiError> {
1376 : AnyStore::batch_create(self, tenant, items).await
1377 66 : }
1378 : async fn batch_delete(
1379 : &self,
1380 : tenant: &TenantId,
1381 : ids: &[String],
1382 198 : ) -> Result<Vec<bool>, NgsiError> {
1383 : AnyStore::batch_delete(self, tenant, ids).await
1384 198 : }
1385 : async fn batch_upsert(
1386 : &self,
1387 : tenant: &TenantId,
1388 : items: Vec<(String, Value)>,
1389 36 : ) -> Result<Vec<bool>, NgsiError> {
1390 : AnyStore::batch_upsert(self, tenant, items).await
1391 36 : }
1392 : async fn upsert(
1393 : &self,
1394 : tenant: &TenantId,
1395 : kind: Kind,
1396 : id: &str,
1397 : doc: Value,
1398 15098 : ) -> Result<bool, NgsiError> {
1399 : AnyStore::upsert(self, tenant, kind, id, doc).await
1400 15098 : }
1401 : async fn get(
1402 : &self,
1403 : tenant: &TenantId,
1404 : kind: Kind,
1405 : id: &str,
1406 20335 : ) -> Result<Option<Value>, NgsiError> {
1407 : AnyStore::get(self, tenant, kind, id).await
1408 20335 : }
1409 2549 : async fn delete(&self, tenant: &TenantId, kind: Kind, id: &str) -> Result<bool, NgsiError> {
1410 : AnyStore::delete(self, tenant, kind, id).await
1411 2549 : }
1412 : async fn delete_entity_if(
1413 : &self,
1414 : tenant: &TenantId,
1415 : id: &str,
1416 : keep: &(dyn for<'v> Fn(&'v Value) -> bool + Sync),
1417 2252 : ) -> Result<bool, NgsiError> {
1418 : AnyStore::delete_entity_if(self, tenant, id, keep).await
1419 2252 : }
1420 10191 : async fn list(&self, tenant: &TenantId, kind: Kind) -> Result<Vec<Value>, NgsiError> {
1421 : AnyStore::list(self, tenant, kind).await
1422 10191 : }
1423 : async fn list_page(
1424 : &self,
1425 : tenant: &TenantId,
1426 : kind: Kind,
1427 : after: Option<&str>,
1428 : limit: usize,
1429 61928 : ) -> Result<Vec<Value>, NgsiError> {
1430 : AnyStore::list_page(self, tenant, kind, after, limit).await
1431 61928 : }
1432 : async fn list_slice(
1433 : &self,
1434 : tenant: &TenantId,
1435 : kind: Kind,
1436 : offset: usize,
1437 : limit: usize,
1438 192 : ) -> Result<(Vec<Value>, usize), NgsiError> {
1439 : AnyStore::list_slice(self, tenant, kind, offset, limit).await
1440 192 : }
1441 : async fn matching_registrations(
1442 : &self,
1443 : tenant: &TenantId,
1444 : ids: Option<&[String]>,
1445 : types: Option<&[String]>,
1446 15924 : ) -> Result<Vec<Value>, NgsiError> {
1447 : AnyStore::matching_registrations(self, tenant, ids, types).await
1448 15924 : }
1449 : async fn query_entities(
1450 : &self,
1451 : tenant: &TenantId,
1452 : f: &crate::store::filter::EntityFilter<'_>,
1453 1658 : ) -> Result<crate::store::filter::QueryOutcome, NgsiError> {
1454 : AnyStore::query_entities(self, tenant, f).await
1455 1658 : }
1456 : async fn mutate_boxed<'a>(
1457 : &self,
1458 : tenant: &TenantId,
1459 : kind: Kind,
1460 : id: &str,
1461 : f: antares_store::MutateFn<'a>,
1462 4567 : ) -> Result<Option<Result<(), ()>>, NgsiError> {
1463 : AnyStore::mutate(self, tenant, kind, id, f).await
1464 4567 : }
1465 : async fn batch_mutate_boxed<'a>(
1466 : &self,
1467 : tenant: &TenantId,
1468 : ids: &[String],
1469 : mut f: antares_store::BatchMutateFn<'a>,
1470 42 : ) -> Result<Vec<Option<Result<(), ()>>>, NgsiError> {
1471 4052 : AnyStore::batch_mutate(self, tenant, ids, |id, v| f(id, v)).await
1472 42 : }
1473 : /// The Postgres arm answers this in ONE statement (see
1474 : /// `pg::doc::record_delivery`); every other arm runs the shared rule as a
1475 : /// `mutate`, which is what the one statement is a translation of.
1476 : async fn record_delivery(
1477 : &self,
1478 : tenant: &TenantId,
1479 : kind: Kind,
1480 : id: &str,
1481 : now: &str,
1482 402 : ) -> Result<Option<antares_store::Delivery>, NgsiError> {
1483 : // `doc_kind` exists only with the Postgres arm, so its call lives
1484 : // inside the gate with it: outside the feature this function is the
1485 : // shared rule and nothing else.
1486 : #[cfg(feature = "postgres")]
1487 : {
1488 : if let (AnyStore::Pg(p), Ok(dk)) = (self, doc_kind(kind)) {
1489 : let found = p
1490 : .docs
1491 : .record_delivery(tenant, dk, id, now)
1492 : .await
1493 : .map_err(db)?;
1494 : return Ok(
1495 10 : found.map(|(doc, prev_success)| antares_store::Delivery { doc, prev_success })
1496 : );
1497 : }
1498 : }
1499 : antares_store::record_delivery_via_mutate(self, tenant, kind, id, now).await
1500 402 : }
1501 776 : async fn sweep_expired(&self) -> usize {
1502 : AnyStore::sweep_expired(self)
1503 776 : }
1504 322 : async fn tenant_exists(&self, tenant: &TenantId) -> Result<bool, NgsiError> {
1505 : AnyStore::tenant_exists(self, tenant).await
1506 322 : }
1507 690 : async fn subscription_tenants(&self) -> Result<Vec<String>, NgsiError> {
1508 : AnyStore::subscription_tenants(self).await
1509 690 : }
1510 804 : async fn tenant_ids(&self) -> Result<Vec<String>, NgsiError> {
1511 : AnyStore::tenant_ids(self).await
1512 804 : }
1513 : async fn tenant_stats_one(
1514 : &self,
1515 : tenant: &TenantId,
1516 22 : ) -> Result<Option<antares_store::TenantStats>, NgsiError> {
1517 : AnyStore::tenant_stats_one(self, tenant).await
1518 22 : }
1519 76 : async fn purge_tenant(&self, tenant: &TenantId) -> Result<bool, NgsiError> {
1520 : AnyStore::purge_tenant(self, tenant).await
1521 76 : }
1522 : async fn context_put(
1523 : &self,
1524 : tenant: Option<&TenantId>,
1525 : id: &str,
1526 : doc: Value,
1527 650 : ) -> Result<(), NgsiError> {
1528 : AnyStore::context_put(self, tenant, id, doc).await
1529 650 : }
1530 : async fn context_get(
1531 : &self,
1532 : tenant: Option<&TenantId>,
1533 : id: &str,
1534 1176 : ) -> Result<Option<Value>, NgsiError> {
1535 : AnyStore::context_get(self, tenant, id).await
1536 1176 : }
1537 222 : async fn context_delete(&self, tenant: Option<&TenantId>, id: &str) -> Result<bool, NgsiError> {
1538 : AnyStore::context_delete(self, tenant, id).await
1539 222 : }
1540 218 : async fn context_list_meta(&self, tenant: Option<&TenantId>) -> Result<Vec<Value>, NgsiError> {
1541 : AnyStore::context_list_meta(self, tenant).await
1542 218 : }
1543 : }
1544 :
1545 : #[async_trait::async_trait]
1546 : impl antares_store::TemporalDriver for AnyStore {
1547 8 : async fn close(&self) {
1548 : AnyStore::close(self).await;
1549 8 : }
1550 84 : fn version_info(&self) -> Value {
1551 84 : AnyStore::version_info(self)
1552 84 : }
1553 22 : async fn attr_instance_count(&self, tenant: &TenantId) -> Result<u64, NgsiError> {
1554 : match self {
1555 : AnyStore::Mem(s) => Ok(s.attr_instance_count(tenant)),
1556 : #[cfg(feature = "postgres")]
1557 : AnyStore::Pg(p) => {
1558 2 : async {
1559 2 : let mut tx = p.docs.pool().begin().await.map_err(db)?;
1560 2 : crate::store::pg::set_tenant(&mut tx, tenant)
1561 2 : .await
1562 2 : .map_err(db)?;
1563 2 : let n: i64 = sqlx::query_scalar(
1564 2 : "SELECT count(*) FROM attr_instances WHERE tenant_id = $1",
1565 2 : )
1566 2 : .bind(tenant.as_str())
1567 2 : .fetch_one(&mut *tx)
1568 2 : .await
1569 2 : .map_err(db)?;
1570 2 : Ok(n as u64)
1571 2 : }
1572 : .await
1573 : }
1574 : }
1575 22 : }
1576 72 : async fn purge_tenant(&self, tenant: &TenantId) -> Result<(), NgsiError> {
1577 : match self {
1578 : AnyStore::Mem(s) => {
1579 : s.purge_kinds(tenant, &[Kind::Temporal]);
1580 : Ok(())
1581 : }
1582 : #[cfg(feature = "postgres")]
1583 : AnyStore::Pg(p) => {
1584 14 : async {
1585 14 : let mut tx = p.docs.pool().begin().await.map_err(db)?;
1586 14 : crate::store::pg::set_tenant(&mut tx, tenant)
1587 14 : .await
1588 14 : .map_err(db)?;
1589 28 : for table in ["attr_instances", "temporal_entities"] {
1590 28 : sqlx::query(sqlx::AssertSqlSafe(format!(
1591 28 : "DELETE FROM {table} WHERE tenant_id = $1"
1592 28 : )))
1593 28 : .bind(tenant.as_str())
1594 28 : .execute(&mut *tx)
1595 28 : .await
1596 28 : .map_err(db)?;
1597 : }
1598 14 : tx.commit().await.map_err(db)
1599 14 : }
1600 : .await
1601 : }
1602 : }
1603 72 : }
1604 : async fn temporal_append(
1605 : &self,
1606 : tenant: &TenantId,
1607 : id: &str,
1608 : shell: &Value,
1609 : additions: &Value,
1610 5340 : ) -> Result<(), NgsiError> {
1611 : AnyStore::temporal_append(self, tenant, id, shell, additions).await
1612 5340 : }
1613 : /// Only the Postgres arm pages in SQL, and only an exact prefilter
1614 : /// makes that page the evaluator's page; the memory arm answers
1615 : /// `paged: false` and the caller pages.
1616 212 : fn q_pushdown_exact(
1617 212 : &self,
1618 212 : q: &antares_ql::QNode,
1619 212 : range: Option<&crate::store::filter::InstanceRange<'_>>,
1620 212 : expand: &dyn Fn(&str) -> String,
1621 212 : ) -> bool {
1622 212 : match self {
1623 : // the memory arm pages nothing, so the filter is not its business
1624 : AnyStore::Mem(_) => {
1625 114 : let _ = (q, range, expand);
1626 114 : false
1627 : }
1628 : #[cfg(feature = "postgres")]
1629 98 : AnyStore::Pg(_) => crate::compile::qprefilter::prefilter_exact(q, range, expand),
1630 : }
1631 212 : }
1632 : async fn query_temporal(
1633 : &self,
1634 : tenant: &TenantId,
1635 : f: &crate::store::filter::TemporalFilter<'_>,
1636 522 : ) -> Result<crate::store::filter::TemporalOutcome, NgsiError> {
1637 : AnyStore::query_temporal(self, tenant, f).await
1638 522 : }
1639 : async fn get_temporal(
1640 : &self,
1641 : tenant: &TenantId,
1642 : id: &str,
1643 : f: &crate::store::filter::TemporalFilter<'_>,
1644 268 : ) -> Result<Option<Value>, NgsiError> {
1645 : AnyStore::get_temporal(self, tenant, id, f).await
1646 268 : }
1647 332 : async fn get(&self, tenant: &TenantId, id: &str) -> Result<Option<Value>, NgsiError> {
1648 : AnyStore::get(self, tenant, Kind::Temporal, id).await
1649 332 : }
1650 296 : async fn create(&self, tenant: &TenantId, id: &str, doc: Value) -> Result<bool, NgsiError> {
1651 : AnyStore::create(self, tenant, Kind::Temporal, id, doc).await
1652 296 : }
1653 16 : async fn upsert(&self, tenant: &TenantId, id: &str, doc: Value) -> Result<bool, NgsiError> {
1654 : AnyStore::upsert(self, tenant, Kind::Temporal, id, doc).await
1655 16 : }
1656 7936 : async fn delete(&self, tenant: &TenantId, id: &str) -> Result<bool, NgsiError> {
1657 : AnyStore::delete(self, tenant, Kind::Temporal, id).await
1658 7936 : }
1659 14 : async fn list(&self, tenant: &TenantId) -> Result<Vec<Value>, NgsiError> {
1660 : AnyStore::list(self, tenant, Kind::Temporal).await
1661 14 : }
1662 : async fn mutate_boxed<'a>(
1663 : &self,
1664 : tenant: &TenantId,
1665 : id: &str,
1666 : f: antares_store::MutateFn<'a>,
1667 178 : ) -> Result<Option<Result<(), ()>>, NgsiError> {
1668 : AnyStore::mutate(self, tenant, Kind::Temporal, id, f).await
1669 178 : }
1670 : }
1671 :
1672 : #[cfg(test)]
1673 : mod temporal_only_tests {
1674 : use super::*;
1675 : use antares_model::TenantId;
1676 :
1677 4 : async fn append(store: &AnyStore) -> Option<Value> {
1678 4 : let t = TenantId::new("combo").expect("tenant");
1679 4 : let shell = serde_json::json!({"id": "urn:a", "type": ["T"]});
1680 4 : let adds = serde_json::json!({"speed": [{"instanceId": "urn:i:1", "value": 1}]});
1681 4 : store
1682 4 : .temporal_append(&t, "urn:a", &shell, &adds)
1683 4 : .await
1684 4 : .expect("append");
1685 4 : store.get(&t, Kind::Temporal, "urn:a").await.expect("get")
1686 4 : }
1687 :
1688 : /// A shared instance records only for an entity it holds (5.6.6 delete
1689 : /// overlap); the temporal half of a mixed deployment records
1690 : /// unconditionally, since the entities live elsewhere.
1691 : #[tokio::test]
1692 2 : async fn a_temporal_only_instance_records_without_the_entity() {
1693 2 : assert!(append(&AnyStore::Mem(Store::default())).await.is_none());
1694 2 : let doc = append(&AnyStore::Mem(Store::default()).temporal_only())
1695 2 : .await
1696 2 : .expect("recorded");
1697 2 : assert_eq!(doc["speed"][0]["value"], 1);
1698 2 : }
1699 : }
|