LCOV - code coverage report
Current view: top level - antares-api/src - types_attrs.rs (source / functions) Coverage Total Hit
Test: merged.info Lines: 97.3 % 1311 1275
Test Date: 2026-11-02 13:21:27 Functions: 53.4 % 668 357

            Line data    Source code
       1              : // SPDX-License-Identifier: EUPL-1.2
       2              : //! Discovery: /types and /attributes (5.7.5–5.7.10; resources 6.25–6.28).
       3              : 
       4              : use crate::negotiate::*;
       5              : use crate::state::AppState;
       6              : use antares_model::NgsiError;
       7              : use antares_store::filter::{EntityFilter, Page};
       8              : use axum::extract::{Path, State};
       9              : use axum::http::{HeaderMap, StatusCode};
      10              : use axum::response::{IntoResponse, Response};
      11              : use serde_json::{json, Value};
      12              : use std::collections::{BTreeMap, BTreeSet};
      13              : 
      14              : use crate::negotiate::CleanParams;
      15              : 
      16              : /// Which of the tenant's entities a fold needs. 5.7.5/5.7.8 describe the
      17              : /// types and attributes "for which entity instances exist within the NGSI-LD
      18              : /// system", so those two fold the whole tenant; 5.7.7 asks about ONE entity
      19              : /// type and 5.7.10 about ONE attribute, and an entity carrying neither can
      20              : /// contribute nothing to either answer.
      21              : #[derive(Clone, Copy)]
      22              : enum Narrow<'a> {
      23              :     Tenant,
      24              :     Type(&'a str),
      25              :     Attr(&'a str),
      26              : }
      27              : 
      28              : impl Narrow<'_> {
      29              :     /// The same predicate the datastore compiles, re-applied when the backend
      30              :     /// reports it did not (`decided == false`): that arm returns the tenant's
      31              :     /// rows unfiltered, and cutting THOSE at the ceiling would answer 5.7.7
      32              :     /// with ResourceNotFound for a type whose entities merely sort past it.
      33           36 :     fn matches(self, doc: &Value) -> bool {
      34           36 :         match self {
      35            8 :             Narrow::Tenant => true,
      36              :             // `types @> ARRAY[t]`
      37           16 :             Narrow::Type(t) => doc["type"]
      38           16 :                 .as_array()
      39           16 :                 .is_some_and(|a| a.iter().any(|v| v.as_str() == Some(t))),
      40              :             // `entity ?| ARRAY[a]`
      41           12 :             Narrow::Attr(a) => doc.get(a).is_some(),
      42              :         }
      43           36 :     }
      44              : }
      45              : 
      46              : /// The entities a fold reads at once. A fold used to read all of them in one
      47              : /// statement, up to the 100 000 of `ANTARES_DISCOVERY_SCAN_MAX`: a `/types`
      48              : /// over a 25 000-entity tenant held every document at once and took the
      49              : /// broker past its 1536 MiB limit within seconds of a start. Page by
      50              : /// page, a fold holds one page and the sets it folds into.
      51              : const SCAN_PAGE: usize = 500;
      52              : 
      53              : /// Hands `each` at most `max` of the entities the fold needs, as the caller's
      54              : /// narrowing shows them, and answers whether more exist. The narrowing and
      55              : /// the page are pushed into the datastore, so on a backend that takes them
      56              : /// the ceiling bounds the rows READ and it bounds them over the narrowed set:
      57              : /// a 5.7.7 request about one type reads that type, not the tenant. A backend
      58              : /// that ignores the page (`paged == false`) hands back its own row set, and
      59              : /// the predicate above plus the truncation here reproduce the same answer.
      60              : /// Asking for one row past what is still wanted is what makes the overflow
      61              : /// visible; `total`, when the backend reports it, is the exact pre-page count.
      62          384 : async fn scan_each(
      63          384 :     st: &AppState,
      64          384 :     tenant: &antares_model::TenantId,
      65          384 :     max: usize,
      66          384 :     only: Narrow<'_>,
      67          384 :     view: View<'_>,
      68          384 :     page: usize,
      69          384 :     mut each: impl FnMut(Value),
      70          384 : ) -> Result<bool, NgsiError> {
      71          384 :     let groups: Vec<Vec<String>> = match only {
      72          100 :         Narrow::Type(t) => vec![vec![t.to_owned()]],
      73          284 :         _ => Vec::new(),
      74              :     };
      75          384 :     let attrs: Vec<String> = match only {
      76          130 :         Narrow::Attr(a) => vec![a.to_owned()],
      77          254 :         _ => Vec::new(),
      78              :     };
      79              :     // `read` counts toward the ceiling; `last` is the keyset cursor.
      80              :     // A backend that ignores `after_id` hands page one back again (its first
      81              :     // id is `head`; compared for equality, so the store's collation never
      82              :     // matters): the walk then falls back to OFFSET from where it is.
      83          384 :     let mut read = 0usize;
      84          384 :     let mut last: Option<String> = None;
      85          384 :     let mut head: Option<String> = None;
      86          384 :     let mut keyset = true;
      87              :     loop {
      88          428 :         let want = max.saturating_sub(read).min(page.max(1));
      89          428 :         let f = EntityFilter {
      90          428 :             types: (!groups.is_empty()).then_some(&groups[..]),
      91          428 :             attrs: (!attrs.is_empty()).then_some(&attrs[..]),
      92          428 :             after_id: last.as_deref().filter(|_| keyset),
      93              :             page: Some(Page {
      94          428 :                 offset: match keyset {
      95          428 :                     true => 0,
      96            0 :                     false => i64::try_from(read).unwrap_or(i64::MAX),
      97              :                 },
      98          428 :                 limit: i64::try_from(want.saturating_add(1)).unwrap_or(i64::MAX),
      99              :                 count: false,
     100              :             }),
     101          428 :             ..Default::default()
     102              :         };
     103          428 :         let out = st.store.query_entities(tenant, &f).await?;
     104          428 :         let mut rows = out.rows;
     105          428 :         let first = rows.first().and_then(|d| d["id"].as_str());
     106          428 :         if out.paged && keyset && last.is_some() && first.is_some() && first == head.as_deref() {
     107            0 :             keyset = false;
     108            0 :             continue;
     109          428 :         }
     110          428 :         if head.is_none() {
     111          384 :             head = first.map(str::to_owned);
     112          384 :         }
     113          428 :         let more = if out.paged {
     114          428 :             rows.len() > want
     115              :         } else {
     116              :             // the whole (narrowed) set came back at once: the ceiling is cut here
     117            0 :             if !out.decided {
     118            0 :                 rows.retain(|d| only.matches(d));
     119            0 :             }
     120            0 :             let more = out.total.map_or(rows.len() > max, |t| {
     121            0 :                 t > i64::try_from(max).unwrap_or(i64::MAX)
     122            0 :             });
     123            0 :             rows.truncate(max);
     124            0 :             visible(st, tenant, view, rows, &mut each).await;
     125            0 :             return Ok(more);
     126              :         };
     127          428 :         rows.truncate(want);
     128          428 :         read = read.saturating_add(rows.len());
     129          428 :         last = rows
     130          428 :             .last()
     131          428 :             .and_then(|d| d["id"].as_str())
     132          428 :             .map(str::to_owned)
     133          428 :             .or(last);
     134          428 :         visible(st, tenant, view, rows, &mut each).await;
     135          428 :         if !more || want == 0 {
     136          360 :             return Ok(more);
     137           68 :         }
     138           68 :         if read >= max {
     139           24 :             return Ok(true);
     140           44 :         }
     141              :     }
     142          384 : }
     143              : 
     144              : /// discovery answers from what the narrowing selects.
     145              : /// The ceiling counts rows before the narrowing, so a narrowed fold
     146              : /// can stop short of `max` visible rows; push q into the filter if that shows up
     147          846 : async fn visible(
     148          846 :     st: &AppState,
     149          846 :     tenant: &antares_model::TenantId,
     150          846 :     view: View<'_>,
     151          846 :     rows: Vec<Value>,
     152          846 :     each: &mut impl FnMut(Value),
     153          846 : ) {
     154          846 :     let Some((r, ctx)) = view else {
     155          580 :         rows.into_iter().for_each(each);
     156          580 :         return;
     157              :     };
     158         2624 :     for d in rows {
     159         2624 :         if hides_a_type(r, &d)
     160         2610 :             || !crate::repr::policy_admits(st, tenant, ctx, r.policy_reach.as_deref(), &d).await
     161              :         {
     162          880 :             continue;
     163         1744 :         }
     164         1744 :         each(crate::repr::policy_view(&d, r));
     165              :     }
     166          846 : }
     167              : 
     168              : /// The rows of [`scan_each`] collected, for the tests that look at them.
     169              : #[cfg(test)]
     170           24 : async fn scan(
     171           24 :     st: &AppState,
     172           24 :     tenant: &antares_model::TenantId,
     173           24 :     max: usize,
     174           24 :     only: Narrow<'_>,
     175           24 :     view: View<'_>,
     176           24 : ) -> Result<(Vec<Value>, bool), NgsiError> {
     177           24 :     let mut rows = Vec::new();
     178           60 :     let more = scan_each(st, tenant, max, only, view, 2, |d| rows.push(d)).await?;
     179           24 :     Ok((rows, more))
     180           24 : }
     181              : 
     182              : /// The caller's narrowing and the @context its q is read with; `None` when
     183              : /// the engine narrowed nothing.
     184              : type View<'a> = Option<(&'a crate::repr::Repr, &'a antares_jsonld::Context)>;
     185              : 
     186              : /// Whether `omit` names one of the Entity's types. the narrowing rules read
     187              : /// `pick`/`omit` as type and Attribute names here. `omit` hides the
     188              : /// Entities of a type it names. `pick` keeps the Attributes it names, and,
     189              : /// as on a read, never hides the Entity frame.
     190         2624 : fn hides_a_type(r: &crate::repr::Repr, doc: &Value) -> bool {
     191         2624 :     let Some(omit) = &r.policy_omit else {
     192         2580 :         return false;
     193              :     };
     194           44 :     doc["type"]
     195           44 :         .as_array()
     196           44 :         .into_iter()
     197           44 :         .flatten()
     198           44 :         .filter_map(Value::as_str)
     199           74 :         .any(|t| omit.iter().any(|n| n.iri == t || n.raw == t))
     200         2624 : }
     201              : 
     202              : /// The narrowing a discovery handler applies, from the engine's Filter.
     203          400 : fn view_of(filter: &crate::policy::Filter) -> Option<crate::repr::Repr> {
     204          400 :     (!filter.is_empty()).then(|| crate::repr::narrowing(filter))
     205          400 : }
     206              : 
     207              : /// A fold that hit the scan ceiling answered from a prefix of the tenant's
     208              : /// entities: the list is a subset and a by-name lookup can miss. IETF RFC
     209              : /// 7234 5.5.1 warn-code 199 (Miscellaneous Warning) carries that fact to the
     210              : /// client in the `NGSILD-Warning` header of 6.3.17.
     211          408 : fn mark_partial(resp: &mut Response, partial: bool, alias: &str) {
     212          408 :     if partial {
     213            4 :         crate::paging::attach_warnings(
     214            4 :             resp,
     215            4 :             &[crate::federation::warning(
     216            4 :                 199,
     217            4 :                 alias,
     218            4 :                 "entity scan ceiling reached; the discovery result is incomplete",
     219            4 :             )],
     220            4 :         );
     221          404 :     }
     222          408 : }
     223              : 
     224              : use antares_model::is_meta;
     225              : 
     226              : /// type IRI → (entity count, attr IRI → attribute types seen)
     227              : type TypeStats = BTreeMap<String, (usize, BTreeMap<String, BTreeSet<String>>)>;
     228              : 
     229              : /// A datastore failure is an InternalError (Table 6.3.2-1), never an empty
     230              : /// fold — reporting "no such type" because the query failed would be a lie.
     231              : /// The second return member is true when the fold stopped at `max`, i.e. the
     232              : /// answer is a subset of the tenant's types.
     233          170 : async fn type_stats(
     234          170 :     st: &AppState,
     235          170 :     tenant: &antares_model::TenantId,
     236          170 :     max: usize,
     237          170 :     only: Narrow<'_>,
     238          170 :     view: View<'_>,
     239          170 : ) -> Result<(TypeStats, bool), NgsiError> {
     240          170 :     let mut map: TypeStats = BTreeMap::new();
     241          786 :     let partial = scan_each(st, tenant, max, only, view, SCAN_PAGE, |doc| {
     242          786 :         let mut attrs: BTreeMap<String, BTreeSet<String>> = BTreeMap::new();
     243          786 :         if let Some(o) = doc.as_object() {
     244         2680 :             for (k, v) in o {
     245         2680 :                 if is_meta(k) {
     246         1672 :                     continue;
     247         1008 :                 }
     248         1008 :                 let types: BTreeSet<String> = v
     249         1008 :                     .as_array()
     250         1008 :                     .map(|arr| {
     251         1008 :                         arr.iter()
     252         1012 :                             .filter_map(|i| i.get("type").and_then(Value::as_str))
     253         1008 :                             .map(str::to_owned)
     254         1008 :                             .collect()
     255         1008 :                     })
     256         1008 :                     .unwrap_or_default();
     257         1008 :                 attrs.entry(k.clone()).or_default().extend(types);
     258              :             }
     259            0 :         }
     260          808 :         for t in doc
     261          786 :             .get("type")
     262          786 :             .and_then(Value::as_array)
     263          786 :             .cloned()
     264          786 :             .unwrap_or_default()
     265              :         {
     266          808 :             if let Some(t) = t.as_str() {
     267          808 :                 let e = map.entry(t.to_owned()).or_default();
     268          808 :                 e.0 = e.0.saturating_add(1);
     269         1046 :                 for (a, tys) in &attrs {
     270         1046 :                     e.1.entry(a.clone())
     271         1046 :                         .or_default()
     272         1046 :                         .extend(tys.iter().cloned());
     273         1046 :                 }
     274            0 :             }
     275              :         }
     276          786 :     })
     277          170 :     .await?;
     278          170 :     Ok((map, partial))
     279          170 : }
     280              : 
     281              : /// The Entities one probe of [`type_names`] reads at a time: a type a view
     282              : /// shows at all is usually shown on its first Entity.
     283              : const PROBE_PAGE: usize = 20;
     284              : 
     285              : /// 5.7.5 without details: the types the store's type list names,
     286              : /// each kept when the caller sees at least one Entity of it, so the answer is
     287              : /// the fold's and the cost is one read of the list plus a probe per type.
     288              : /// a `/types` read folded every Entity of a 25 000-Entity view for these
     289              : /// names, 20 s and the pool. `None` when the backend keeps no type list;
     290              : /// `true` beside the names when a probe stopped at `max` Entities read.
     291           78 : async fn type_names(
     292           78 :     st: &AppState,
     293           78 :     tenant: &antares_model::TenantId,
     294           78 :     max: usize,
     295           78 :     view: View<'_>,
     296           78 : ) -> Result<Option<(Vec<String>, bool)>, NgsiError> {
     297           78 :     if view.is_none() {
     298           50 :         return Ok(st
     299           50 :             .store
     300           50 :             .entity_type_names(tenant)
     301           50 :             .await?
     302           50 :             .map(|n| (n, false)));
     303           28 :     }
     304           28 :     let Some((w, names)) = Walk::of(st, tenant, max, view).await? else {
     305            0 :         return Ok(None);
     306              :     };
     307           28 :     let mut shown = Vec::new();
     308           28 :     let mut partial = false;
     309           92 :     for name in names {
     310           92 :         match first_visible(&w, &name, None, &mut |_| true).await? {
     311           60 :             Probe::Seen => shown.push(name),
     312           32 :             Probe::None => {}
     313            0 :             Probe::Cut => partial = true,
     314              :         }
     315              :     }
     316           28 :     Ok(Some((shown, partial)))
     317           78 : }
     318              : 
     319              : /// type IRI → the Attribute names its Entities show the caller
     320              : type Shapes = BTreeMap<String, BTreeSet<String>>;
     321              : 
     322              : /// 5.7.6 and 5.7.8/5.7.9 from the store's attribute catalogue: per
     323              : /// live type, the first page of Entities the caller sees names most of its
     324              : /// Attributes, and each catalogued name that page lacks is one probe of the
     325              : /// Entities carrying it. A name the view cannot show on that type is never
     326              : /// probed. The answer is the fold's: a type is listed when the caller sees
     327              : /// one of its Entities, an Attribute when such an Entity shows it. `/attributes`
     328              : /// folded every Entity of the Tenant: 13-19 s on a deployment. `None` when the
     329              : /// backend keeps no catalogue; `true` beside the shapes when a probe stopped
     330              : /// at `max` Entities read.
     331              : /// A name no Entity carries any more stays catalogued and costs a
     332              : /// walk over its type per call; prune it with a counted catalogue if that shows up.
     333          148 : async fn type_shapes(
     334          148 :     st: &AppState,
     335          148 :     tenant: &antares_model::TenantId,
     336          148 :     max: usize,
     337          148 :     view: View<'_>,
     338          148 : ) -> Result<Option<(Shapes, bool)>, NgsiError> {
     339          148 :     let Some(pairs) = st.store.entity_attr_pairs(tenant).await? else {
     340            2 :         return Ok(None);
     341              :     };
     342          146 :     let mut catalogue = Shapes::new();
     343         1744 :     for (t, a) in pairs {
     344         1744 :         if !is_meta(&a) {
     345          696 :             catalogue.entry(t).or_default().insert(a);
     346         1058 :         }
     347              :     }
     348          146 :     let Some((w, names)) = Walk::of(st, tenant, max, view).await? else {
     349            0 :         return Ok(None);
     350              :     };
     351          146 :     let mut shapes = Shapes::new();
     352          146 :     let mut partial = false;
     353          288 :     for name in names {
     354          288 :         let mut seen = BTreeSet::new();
     355         2784 :         let mut note = |d: &Value| {
     356         2784 :             let keys = d.as_object().into_iter().flat_map(|o| o.keys());
     357         8718 :             seen.extend(keys.filter(|k| !is_meta(k)).cloned());
     358         2784 :             true
     359         2784 :         };
     360          288 :         match first_visible(&w, &name, None, &mut note).await? {
     361          276 :             Probe::Seen => {}
     362           12 :             Probe::None => continue,
     363              :             Probe::Cut => {
     364            0 :                 partial = true;
     365            0 :                 continue;
     366              :             }
     367              :         }
     368          276 :         let mut wanted = catalogue.remove(&name).unwrap_or_default();
     369          516 :         wanted.retain(|a| !seen.contains(a) && may_show(view, &name, a));
     370          276 :         for a in &wanted {
     371           20 :             let mut shows = |d: &Value| d.get(a).is_some();
     372           20 :             match first_visible(&w, &name, Some(a), &mut shows).await? {
     373           16 :                 Probe::Seen => {
     374           16 :                     seen.insert(a.clone());
     375           16 :                 }
     376            4 :                 Probe::None => {}
     377            0 :                 Probe::Cut => partial = true,
     378              :             }
     379              :         }
     380          276 :         shapes.insert(name, seen);
     381              :     }
     382          146 :     Ok(Some((shapes, partial)))
     383          148 : }
     384              : 
     385              : /// Whether the narrowing can show Attribute `attr` on an Entity of type
     386              : /// `name` at all: the projection keeps it, a rule shows it and the type does
     387              : /// not omit it. False only where [`visible`] would drop it from every such
     388              : /// Entity, so a probe for it would walk the type for nothing.
     389          102 : fn may_show(view: View<'_>, name: &str, attr: &str) -> bool {
     390          102 :     let Some(view) = view else {
     391            8 :         return true;
     392              :     };
     393           94 :     let r = view.0;
     394           94 :     if !crate::repr::policy_projected(r.policy_pick.as_deref(), r.policy_omit.as_deref(), attr) {
     395            6 :         return false;
     396           88 :     }
     397           88 :     let Some(f) = r.policy_types.as_deref() else {
     398            0 :         return true;
     399              :     };
     400           88 :     let ruled = f.rules.is_empty()
     401           88 :         || rules_of(view)
     402          208 :             .any(|rule| rule.every_attribute || rule.attributes.iter().any(|a| a == attr));
     403           88 :     ruled
     404           12 :         && !f
     405           12 :             .omit_by_type
     406           12 :             .get(name)
     407           12 :             .is_some_and(|o| o.iter().any(|a| a == attr))
     408          102 : }
     409              : 
     410              : /// Per rule of the narrowing, the types an Entity of two or more types
     411              : /// carries beside the rule's own; `None` for a rule naming no type
     412              : /// or a backend without that read.
     413          174 : async fn besides(
     414          174 :     st: &AppState,
     415          174 :     tenant: &antares_model::TenantId,
     416          174 :     view: View<'_>,
     417          174 : ) -> Result<Vec<Option<BTreeSet<String>>>, NgsiError> {
     418          174 :     let mut beside = Vec::new();
     419          174 :     let Some(view) = view else {
     420          108 :         return Ok(beside);
     421              :     };
     422           98 :     for rule in rules_of(view) {
     423           98 :         beside.push(match rule.types.is_empty() {
     424            0 :             true => None,
     425           98 :             false => st
     426           98 :                 .store
     427           98 :                 .entity_types_beside(tenant, &rule.types)
     428           98 :                 .await?
     429           98 :                 .map(|t| t.into_iter().collect::<BTreeSet<String>>()),
     430              :         });
     431              :     }
     432           66 :     Ok(beside)
     433          174 : }
     434              : 
     435              : /// The type names a discovery answer probes. A view with rules probes only
     436              : /// the names `candidates` lists, and each probe proves its type live: the
     437              : /// Tenant's whole type list, a skip scan plus a walk of the multi-typed
     438              : /// Entities per catalogued type (RLS keeps `@>` off the GIN), is read only
     439              : /// when that set is unknown. `None` when the backend keeps no type
     440              : /// list.
     441          174 : async fn probe_names(
     442          174 :     st: &AppState,
     443          174 :     tenant: &antares_model::TenantId,
     444          174 :     view: View<'_>,
     445          174 :     beside: &[Option<BTreeSet<String>>],
     446          174 : ) -> Result<Option<Vec<String>>, NgsiError> {
     447          174 :     let rules: Vec<_> = view.map(rules_of).into_iter().flatten().collect();
     448          174 :     match candidates(unruled(view), &rules, beside) {
     449           48 :         Some(names) => Ok(Some(names)),
     450          126 :         None => st.store.entity_type_names(tenant).await,
     451              :     }
     452          174 : }
     453              : 
     454              : /// What every probe of one discovery answer shares.
     455              : struct Walk<'a> {
     456              :     st: &'a AppState,
     457              :     tenant: &'a antares_model::TenantId,
     458              :     max: usize,
     459              :     view: View<'a>,
     460              :     beside: Vec<Option<BTreeSet<String>>>,
     461              : }
     462              : 
     463              : impl<'a> Walk<'a> {
     464              :     /// The walk of one discovery answer and the type names it probes;
     465              :     /// `None` when the backend keeps no type list.
     466          174 :     async fn of(
     467          174 :         st: &'a AppState,
     468          174 :         tenant: &'a antares_model::TenantId,
     469          174 :         max: usize,
     470          174 :         view: View<'a>,
     471          174 :     ) -> Result<Option<(Self, Vec<String>)>, NgsiError> {
     472          174 :         let beside = besides(st, tenant, view).await?;
     473          174 :         let Some(names) = probe_names(st, tenant, view, &beside).await? else {
     474            0 :             return Ok(None);
     475              :         };
     476          174 :         let walk = Self {
     477          174 :             st,
     478          174 :             tenant,
     479          174 :             max,
     480          174 :             view,
     481          174 :             beside,
     482          174 :         };
     483          174 :         Ok(Some((walk, names)))
     484          174 :     }
     485              : }
     486              : 
     487              : /// What a probe of one type found.
     488              : enum Probe {
     489              :     Seen,
     490              :     None,
     491              :     Cut,
     492              : }
     493              : 
     494              : /// The rules a probe follows: a linked term never holds,
     495              : /// so a rule with one shows nothing and is not probed.
     496          426 : fn rules_of<'a>(
     497          426 :     view: (&'a crate::repr::Repr, &antares_jsonld::Context),
     498          426 : ) -> impl Iterator<Item = &'a crate::policy::Rule> {
     499          426 :     view.0
     500          426 :         .policy_types
     501          426 :         .as_deref()
     502          426 :         .map_or(&[][..], |f| &f.rules[..])
     503          426 :         .iter()
     504          666 :         .filter(|r| r.q.as_deref().is_none_or(|q| !q.has_linked_paths()))
     505          426 : }
     506              : 
     507              : /// Whether the caller sees an Entity of `name` (carrying `attr`, when
     508              : /// given) that `each` counts; `each` sees every visible Entity of the page
     509              : /// that held the first counted one. Each rule of the narrowing
     510              : /// is one probe, the rule's `q` pushed down, so a condition few Entities meet
     511              : /// reads those few. A rule naming `name` probes `name` alone; a
     512              : /// rule naming other types probes `name` beside each of them, and only when
     513              : /// `beside` (per rule, from the store) says an Entity carries both: on
     514              : /// Postgres such a two-type group with no match walked the Tenant's whole
     515              : /// primary key for its `ORDER BY id LIMIT` (17 s for `list_types`).
     516              : /// A narrowing without rules, and no narrowing, is one probe of `name`.
     517              : /// [`visible`] still judges every Entity a probe reads.
     518          400 : async fn first_visible(
     519          400 :     w: &Walk<'_>,
     520          400 :     name: &str,
     521          400 :     attr: Option<&str>,
     522          400 :     each: &mut impl FnMut(&Value) -> bool,
     523          400 : ) -> Result<Probe, NgsiError> {
     524          400 :     let view = w.view;
     525          400 :     let rules: Vec<_> = view.map(rules_of).into_iter().flatten().collect();
     526          400 :     let probes = plan(name, &rules, unruled(view), &w.beside);
     527          400 :     let mut cut = false;
     528          418 :     for (groups, rule) in &probes {
     529          418 :         let one = Probing {
     530          418 :             name,
     531          418 :             groups,
     532          418 :             rule: *rule,
     533          418 :             attr,
     534          418 :         };
     535          418 :         match probe(w.st, w.tenant, w.max, &one, view, each).await? {
     536          352 :             Probe::Seen => return Ok(Probe::Seen),
     537            0 :             Probe::Cut => cut = true,
     538           66 :             Probe::None => {}
     539              :         }
     540              :     }
     541           48 :     Ok(if cut { Probe::Cut } else { Probe::None })
     542          400 : }
     543              : /// Whether the view narrows without rules: then every type is probed alone.
     544              : /// No narrowing counts as unruled.
     545          574 : fn unruled(view: View<'_>) -> bool {
     546          574 :     view.and_then(|v| v.0.policy_types.as_deref())
     547          574 :         .is_none_or(|f| f.rules.is_empty())
     548          574 : }
     549              : 
     550              : /// Every name [`plan`] can send a probe for under a view with rules: each
     551              : /// rule's types and the types the store found beside them, sorted. `None`
     552              : /// when the view has no rules or a rule's `beside` is unknown, so any
     553              : /// catalogued name may be probed.
     554          190 : fn candidates(
     555          190 :     unruled: bool,
     556          190 :     rules: &[&crate::policy::Rule],
     557          190 :     beside: &[Option<BTreeSet<String>>],
     558          190 : ) -> Option<Vec<String>> {
     559          190 :     if unruled {
     560          122 :         return None;
     561           68 :     }
     562           68 :     let mut names = BTreeSet::new();
     563          108 :     for (rule, beside) in rules.iter().zip(beside) {
     564          108 :         if rule.types.is_empty() {
     565            4 :             continue;
     566          104 :         }
     567          104 :         names.extend(rule.types.iter().cloned());
     568          104 :         names.extend(beside.as_ref()?.iter().cloned());
     569              :     }
     570           56 :     Some(names.into_iter().collect())
     571          190 : }
     572              : 
     573              : type Plan<'a> = Vec<(Vec<Vec<String>>, Option<&'a crate::policy::Rule>)>;
     574              : 
     575              : /// The probes [`first_visible`] sends for `name`: the type groups of each
     576              : /// one and the rule whose `q` and id selection it pushes down. A type no rule shows and no Entity
     577              : /// carries beside a rule's type gets none, so it reads nothing.
     578          452 : fn plan<'a>(
     579          452 :     name: &str,
     580          452 :     rules: &[&'a crate::policy::Rule],
     581          452 :     unruled: bool,
     582          452 :     beside: &[Option<BTreeSet<String>>],
     583          452 : ) -> Plan<'a> {
     584          452 :     let alone = || vec![vec![name.to_owned()]];
     585          452 :     let mut probes = Plan::new();
     586          454 :     for (i, r) in rules.iter().enumerate() {
     587          422 :         let groups = if r.types.iter().any(|t| t == name) {
     588          130 :             alone()
     589          292 :         } else if r.types.is_empty()
     590          268 :             || beside
     591          268 :                 .get(i)
     592          268 :                 .and_then(Option::as_ref)
     593          268 :                 .is_some_and(|b| !b.contains(name))
     594              :         {
     595          188 :             continue;
     596              :         } else {
     597          104 :             r.types
     598          104 :                 .iter()
     599          104 :                 .map(|t| vec![name.to_owned(), t.clone()])
     600          104 :                 .collect()
     601              :         };
     602          234 :         probes.push((groups, Some(*r)));
     603              :     }
     604          452 :     if unruled {
     605          228 :         probes.push((alone(), None));
     606          236 :     }
     607          452 :     probes
     608          452 : }
     609              : 
     610              : /// One probe's selection: the type, its groups, the rule it follows and
     611              : /// the Attribute the Entities must carry.
     612              : struct Probing<'a> {
     613              :     name: &'a str,
     614              :     groups: &'a [Vec<String>],
     615              :     rule: Option<&'a crate::policy::Rule>,
     616              :     attr: Option<&'a str>,
     617              : }
     618              : 
     619              : /// What a rule's id selection narrows a probe's read to: its ids, or the
     620              : /// literal its one id pattern forces. A view of one type and one
     621              : /// `idPattern` that few of the type's Entities match walked every Entity of
     622              : /// the type to the fold ceiling, the regex judged in [`visible`] only: 9-10 s
     623              : /// for `/types` on a deployment, the gateway's 503. Both are necessary conditions of
     624              : /// `Rule::selects_id`, which still decides; a rule with ids and patterns, or
     625              : /// with several patterns, selects by OR and narrows nothing here.
     626          442 : fn id_narrowing(rule: Option<&crate::policy::Rule>) -> (Option<Vec<&str>>, Option<(String, bool)>) {
     627          210 :     match rule {
     628          214 :         Some(r) if r.id_patterns.is_empty() && !r.ids.is_empty() => {
     629            4 :             (Some(r.ids.iter().map(String::as_str).collect()), None)
     630              :         }
     631          210 :         Some(r) if r.ids.is_empty() && r.id_patterns.len() == 1 => (
     632            8 :             None,
     633            8 :             r.id_patterns
     634            8 :                 .first()
     635            8 :                 .and_then(|p| antares_store::filter::id_pattern_required(p)),
     636              :         ),
     637          430 :         _ => (None, None),
     638              :     }
     639          442 : }
     640              : 
     641              : /// One probe of [`first_visible`] up to `max` Entities read: [`PROBE_PAGE`]
     642              : /// first, then [`SCAN_PAGE`] at a time by keyset, so a walk for an
     643              : /// Attribute few Entities carry reads each Entity once.
     644          418 : async fn probe(
     645          418 :     st: &AppState,
     646          418 :     tenant: &antares_model::TenantId,
     647          418 :     max: usize,
     648          418 :     one: &Probing<'_>,
     649          418 :     view: View<'_>,
     650          418 :     each: &mut impl FnMut(&Value) -> bool,
     651          418 : ) -> Result<Probe, NgsiError> {
     652          418 :     let ctx = crate::repr::policy_context();
     653          418 :     let expand = |t: &str| ctx.expand_key(t);
     654          418 :     let attrs: Vec<String> = one.attr.into_iter().map(str::to_owned).collect();
     655          418 :     let (ids, literal) = id_narrowing(one.rule);
     656          418 :     let prefixes = crate::policy::id_prefixes(st, tenant, one.rule).await?;
     657          418 :     let mut read = 0usize;
     658          418 :     let mut last: Option<String> = None;
     659          418 :     let mut head: Option<String> = None;
     660          418 :     let mut keyset = true;
     661          418 :     while read < max {
     662          418 :         let page = if read == 0 { PROBE_PAGE } else { SCAN_PAGE };
     663          418 :         let want = page.min(max.saturating_sub(read));
     664          418 :         let f = EntityFilter {
     665          418 :             ids: ids.as_deref(),
     666          418 :             id_literal: literal
     667          418 :                 .as_ref()
     668          418 :                 .map(|(text, anchored)| antares_store::filter::IdLiteral {
     669            4 :                     text,
     670            4 :                     anchored: *anchored,
     671            4 :                 }),
     672          418 :             id_prefixes: prefixes.as_deref(),
     673          418 :             types: Some(one.groups),
     674          418 :             attrs: one.attr.map(|_| &attrs[..]),
     675          418 :             q: one.rule.and_then(|r| r.q.as_deref()),
     676          418 :             expand: &expand,
     677          418 :             ctx: Some(ctx),
     678          418 :             after_id: last.as_deref().filter(|_| keyset),
     679              :             page: Some(Page {
     680          418 :                 offset: match keyset {
     681          418 :                     true => 0,
     682            0 :                     false => i64::try_from(read).unwrap_or(i64::MAX),
     683              :                 },
     684          418 :                 limit: i64::try_from(want).unwrap_or(i64::MAX),
     685              :                 count: false,
     686              :             }),
     687          418 :             ..Default::default()
     688              :         };
     689          418 :         let out = st.store.query_entities(tenant, &f).await?;
     690          418 :         let mut rows = out.rows;
     691          418 :         let first = rows.first().and_then(|d| d["id"].as_str());
     692              :         // a backend that ignores `after_id` hands page one back again
     693          418 :         if out.paged && keyset && last.is_some() && first.is_some() && first == head.as_deref() {
     694            0 :             keyset = false;
     695            0 :             continue;
     696          418 :         }
     697          418 :         if head.is_none() {
     698          418 :             head = first.map(str::to_owned);
     699          418 :         }
     700          418 :         if !out.decided {
     701            0 :             rows.retain(|d| {
     702            0 :                 Narrow::Type(one.name).matches(d)
     703            0 :                     && one.attr.is_none_or(|a| Narrow::Attr(a).matches(d))
     704            0 :             });
     705          418 :         }
     706              :         // a backend that ignores the page answered the whole narrowed set
     707          418 :         let done = !out.paged || rows.len() < want;
     708          418 :         rows.truncate(max.saturating_sub(read));
     709          418 :         read = read.saturating_add(if out.paged { want } else { max });
     710          418 :         last = rows
     711          418 :             .last()
     712          418 :             .and_then(|d| d["id"].as_str())
     713          418 :             .map(str::to_owned)
     714          418 :             .or(last);
     715          418 :         let mut seen = false;
     716         3206 :         visible(st, tenant, view, rows, &mut |d| seen |= each(&d)).await;
     717          418 :         if seen {
     718          352 :             return Ok(Probe::Seen);
     719           66 :         }
     720           66 :         if done {
     721           66 :             return Ok(Probe::None);
     722            0 :         }
     723              :     }
     724            0 :     Ok(Probe::Cut)
     725          418 : }
     726              : 
     727              : /// attr IRI → (count, attribute types, entity type IRIs)
     728              : type AttrStats = BTreeMap<String, (usize, BTreeSet<String>, BTreeSet<String>)>;
     729              : 
     730              : /// Same bound and same incompleteness report as `type_stats`.
     731          178 : async fn attr_stats(
     732          178 :     st: &AppState,
     733          178 :     tenant: &antares_model::TenantId,
     734          178 :     max: usize,
     735          178 :     only: Narrow<'_>,
     736          178 :     view: View<'_>,
     737          178 : ) -> Result<(AttrStats, bool), NgsiError> {
     738          178 :     let mut map: AttrStats = BTreeMap::new();
     739          636 :     let partial = scan_each(st, tenant, max, only, view, SCAN_PAGE, |doc| {
     740          636 :         let etypes: Vec<String> = doc
     741          636 :             .get("type")
     742          636 :             .and_then(Value::as_array)
     743          636 :             .cloned()
     744          636 :             .unwrap_or_default()
     745          636 :             .iter()
     746          636 :             .filter_map(Value::as_str)
     747          636 :             .map(str::to_owned)
     748          636 :             .collect();
     749          636 :         if let Some(o) = doc.as_object() {
     750         2176 :             for (k, v) in o {
     751         2176 :                 if is_meta(k) {
     752         1344 :                     continue;
     753          832 :                 }
     754          832 :                 let e = map.entry(k.clone()).or_default();
     755              :                 // Table 5.2.28-1 attributeCount: "Number of attribute
     756              :                 // instances with this attribute name" — a multi-instance
     757              :                 // attribute (4.5.5 datasetId) counts once per instance.
     758          832 :                 e.0 = e.0.saturating_add(v.as_array().map_or(1, Vec::len));
     759          832 :                 if let Some(arr) = v.as_array() {
     760          840 :                     for inst in arr {
     761          840 :                         if let Some(t) = inst.get("type").and_then(Value::as_str) {
     762          840 :                             e.1.insert(t.to_owned());
     763          840 :                         }
     764              :                     }
     765            0 :                 }
     766          832 :                 e.2.extend(etypes.iter().cloned());
     767              :             }
     768            0 :         }
     769          636 :     })
     770          178 :     .await?;
     771          178 :     Ok((map, partial))
     772          178 : }
     773              : 
     774              : /// The Attribute view of per-type shapes: each name with the types whose
     775              : /// Entities show it. 5.7.8/5.7.9 read neither count nor attribute types.
     776          102 : fn attrs_of(shapes: Shapes) -> AttrStats {
     777          102 :     let mut map = AttrStats::new();
     778          164 :     for (t, attrs) in shapes {
     779          280 :         for a in attrs {
     780          280 :             map.entry(a).or_default().2.insert(t.clone());
     781          280 :         }
     782              :     }
     783          102 :     map
     784          102 : }
     785              : 
     786              : // ---------- GET /types (5.7.5/5.7.6) ----------
     787              : 
     788              : /// 4.5.10 Entity Type List Representation, members per Table 5.2.24-1
     789              : /// (5.2.24 EntityTypeList): id a valid URI, type equal to "EntityTypeList",
     790              : /// typeList the entity type names — with details=true the 4.5.11 detailed
     791              : /// list of Table 5.2.25-1 EntityType objects (id = type FQN, type
     792              : /// "EntityType", attributeNames, typeName) (5.7.5/5.7.6).
     793          132 : pub async fn entity_types(
     794          132 :     State(st): State<AppState>,
     795          132 :     CleanParams(params): CleanParams,
     796          132 :     headers: HeaderMap,
     797          132 : ) -> Response {
     798          132 :     let go = async {
     799          132 :         let tenant = tenant_from(&headers)?;
     800          132 :         check_params(&params, &["details", "local", "count"])?;
     801          126 :         let accept = parse_accept(&headers)?;
     802          124 :         let ctx = request_context(&st.loader, &headers).await?;
     803          124 :         let filter = gate!(st, &tenant, &headers, "5.7.5").await?;
     804          124 :         let view = view_of(&filter);
     805          124 :         let view = view.as_ref().map(|r| (r, &*ctx));
     806          124 :         let details = params.get("details").map(String::as_str) == Some("true");
     807          124 :         let max = *crate::bounds::MAX_FOLD_DOCS;
     808              :         // The names come from the store's type list, the details
     809              :         // from its attribute catalogue; a backend without them
     810              :         // folds the Entities.
     811          124 :         let listed: Option<(TypeStats, bool)> = match details {
     812           46 :             true => type_shapes(&st, &tenant, max, view)
     813           46 :                 .await?
     814           46 :                 .map(|(shapes, partial)| {
     815          112 :                     let stats = shapes.into_iter().map(|(t, attrs)| {
     816              :                         (
     817          112 :                             t,
     818          150 :                             (0, attrs.into_iter().map(|a| (a, BTreeSet::new())).collect()),
     819              :                         )
     820          112 :                     });
     821           44 :                     (stats.collect(), partial)
     822           44 :                 }),
     823           78 :             false => type_names(&st, &tenant, max, view)
     824           78 :                 .await?
     825           78 :                 .map(|(names, partial)| {
     826          110 :                     let stats = names.into_iter().map(|t| (t, Default::default()));
     827           78 :                     (stats.collect(), partial)
     828           78 :                 }),
     829              :         };
     830          124 :         let (stats, partial) = match listed {
     831          122 :             Some(listed) => listed,
     832            2 :             None => type_stats(&st, &tenant, max, Narrow::Tenant, view).await?,
     833              :         };
     834          124 :         let payload = if details {
     835              :             Value::Array(
     836           46 :                 stats
     837           46 :                     .iter()
     838          118 :                     .map(|(t, (_, attrs))| {
     839          118 :                         json!({
     840          118 :                             "id": t,
     841          118 :                             "type": "EntityType",
     842          118 :                             "typeName": ctx.compact_iri(t),
     843          156 :                             "attributeNames": attrs.keys().map(|a| ctx.compact_iri(a)).collect::<Vec<_>>(),
     844              :                         })
     845          118 :                     })
     846           46 :                     .collect(),
     847              :             )
     848              :         } else {
     849           78 :             json!({
     850           78 :                 "id": format!("urn:ngsi-ld:EntityTypeList:{}", uuid::Uuid::new_v4()),
     851           78 :                 "type": "EntityTypeList",
     852          110 :                 "typeList": stats.keys().map(|t| ctx.compact_iri(t)).collect::<Vec<_>>(),
     853              :             })
     854              :         };
     855          124 :         let mut resp = respond(StatusCode::OK, payload, &ctx, accept, &tenant);
     856          124 :         mark_partial(&mut resp, partial, &st.host_alias);
     857          124 :         filter.mark(&st.host_alias, resp.headers_mut());
     858          124 :         Ok::<_, ApiError>(resp)
     859          132 :     };
     860          132 :     go.await.unwrap_or_else(|e| e.into_response())
     861          132 : }
     862              : 
     863              : // ---------- GET /types/{type} (5.7.7) ----------
     864              : 
     865              : /// 4.5.12 Entity Type Information Representation, members per Table
     866              : /// 5.2.26-1 (5.2.26 EntityTypeInfo): id = the entity type FQN, fixed type
     867              : /// "EntityTypeInfo", typeName (short name under the @context), entityCount
     868              : /// an unsigned integer, attributeDetails Attribute[] restricted to the
     869              : /// elements id/type/attributeName/attributeTypes (5.7.7).
     870           86 : pub async fn entity_type_info(
     871           86 :     State(st): State<AppState>,
     872           86 :     Path(type_name): Path<String>,
     873           86 :     CleanParams(params): CleanParams,
     874           86 :     headers: HeaderMap,
     875           86 : ) -> Response {
     876           86 :     let go = async {
     877           86 :         let tenant = tenant_from(&headers)?;
     878           86 :         check_params(&params, &["local"])?;
     879           80 :         let accept = parse_accept(&headers)?;
     880           78 :         let ctx = request_context(&st.loader, &headers).await?;
     881           78 :         let iri = ctx.expand_key(&type_name);
     882           76 :         let filter =
     883           78 :             gate!(st, &tenant, &headers, "5.7.7", types: std::slice::from_ref(&iri)).await?;
     884           76 :         let view = view_of(&filter);
     885              :         // Only instances of this type carry its entityCount and its
     886              :         // attributeDetails (5.2.26), so the fold asks the datastore for those
     887              :         // and the ceiling applies to them — a tenant of 100M entities with 50
     888              :         // Buildings answers exactly, instead of from the tenant's first page.
     889           76 :         let (stats, partial) = type_stats(
     890           76 :             &st,
     891           76 :             &tenant,
     892           76 :             *crate::bounds::MAX_FOLD_DOCS,
     893           76 :             Narrow::Type(&iri),
     894           76 :             view.as_ref().map(|r| (r, &*ctx)),
     895              :         )
     896           76 :         .await?;
     897           76 :         let Some((count, attrs)) = stats.get(&iri) else {
     898           42 :             let mut resp = ApiError::from(NgsiError::ResourceNotFound(format!(
     899           42 :                 "no entities of type {type_name}"
     900           42 :             )))
     901           42 :             .into_response();
     902           42 :             mark_partial(&mut resp, partial, &st.host_alias);
     903           42 :             return Ok(resp);
     904              :         };
     905           34 :         let attr_details: Vec<Value> = attrs
     906           34 :             .iter()
     907           68 :             .map(|(a, atypes)| {
     908           68 :                 json!({
     909           68 :                     "id": a,
     910           68 :                     "type": "Attribute",
     911           68 :                     "attributeName": ctx.compact_iri(a),
     912           68 :                     "attributeTypes": atypes.iter().cloned().collect::<Vec<_>>(),
     913              :                 })
     914           68 :             })
     915           34 :             .collect();
     916           34 :         let payload = json!({
     917           34 :             "id": iri,
     918           34 :             "type": "EntityTypeInfo",
     919           34 :             "typeName": ctx.compact_iri(&iri),
     920           34 :             "entityCount": count,
     921           34 :             "attributeDetails": attr_details,
     922              :         });
     923           34 :         let mut resp = respond(StatusCode::OK, payload, &ctx, accept, &tenant);
     924           34 :         mark_partial(&mut resp, partial, &st.host_alias);
     925           34 :         filter.mark(&st.host_alias, resp.headers_mut());
     926           34 :         Ok::<_, ApiError>(resp)
     927           86 :     };
     928           86 :     go.await.unwrap_or_else(|e| e.into_response())
     929           86 : }
     930              : 
     931              : // ---------- GET /attributes (5.7.8/5.7.9) ----------
     932              : 
     933              : /// 4.5.13 Attribute List Representation, members per Table 5.2.27-1
     934              : /// (5.2.27 AttributeList): id a valid URI, type "AttributeList",
     935              : /// attributeList of attribute names — with details=true the 4.5.14 detailed
     936              : /// list of Table 5.2.28-1 Attribute objects (id = attribute URI, type
     937              : /// "Attribute", attributeName, typeNames).
     938           98 : pub async fn attributes(
     939           98 :     State(st): State<AppState>,
     940           98 :     CleanParams(params): CleanParams,
     941           98 :     headers: HeaderMap,
     942           98 : ) -> Response {
     943           98 :     let go = async {
     944           98 :         let tenant = tenant_from(&headers)?;
     945           98 :         check_params(&params, &["details", "local", "count"])?;
     946           92 :         let accept = parse_accept(&headers)?;
     947           90 :         let ctx = request_context(&st.loader, &headers).await?;
     948           90 :         let filter = gate!(st, &tenant, &headers, "5.7.8").await?;
     949           90 :         let view = view_of(&filter);
     950           90 :         let view = view.as_ref().map(|r| (r, &*ctx));
     951           90 :         let max = *crate::bounds::MAX_FOLD_DOCS;
     952              :         // From the attribute catalogue; a backend without one folds.
     953           90 :         let (stats, partial) = match type_shapes(&st, &tenant, max, view).await? {
     954           90 :             Some((shapes, partial)) => (attrs_of(shapes), partial),
     955            0 :             None => attr_stats(&st, &tenant, max, Narrow::Tenant, view).await?,
     956              :         };
     957           90 :         let details = params.get("details").map(String::as_str) == Some("true");
     958           90 :         let payload = if details {
     959              :             Value::Array(
     960           30 :                 stats
     961           30 :                     .iter()
     962           58 :                     .map(|(a, (_, _, etypes))| {
     963           58 :                         json!({
     964           58 :                             "id": a,
     965           58 :                             "type": "Attribute",
     966           58 :                             "attributeName": ctx.compact_iri(a),
     967          100 :                             "typeNames": etypes.iter().map(|t| ctx.compact_iri(t)).collect::<Vec<_>>(),
     968              :                         })
     969           58 :                     })
     970           30 :                     .collect(),
     971              :             )
     972              :         } else {
     973           60 :             json!({
     974           60 :                 "id": format!("urn:ngsi-ld:AttributeList:{}", uuid::Uuid::new_v4()),
     975           60 :                 "type": "AttributeList",
     976           80 :                 "attributeList": stats.keys().map(|a| ctx.compact_iri(a)).collect::<Vec<_>>(),
     977              :             })
     978              :         };
     979           90 :         let mut resp = respond(StatusCode::OK, payload, &ctx, accept, &tenant);
     980           90 :         mark_partial(&mut resp, partial, &st.host_alias);
     981           90 :         filter.mark(&st.host_alias, resp.headers_mut());
     982           90 :         Ok::<_, ApiError>(resp)
     983           98 :     };
     984           98 :     go.await.unwrap_or_else(|e| e.into_response())
     985           98 : }
     986              : 
     987              : // ---------- GET /attributes/{attrId} (5.7.10) ----------
     988              : 
     989              : /// 4.5.15 Attribute Information Representation, members per Table 5.2.28-1
     990              : /// (5.2.28 Attribute): id = the attribute URI, fixed type "Attribute",
     991              : /// attributeName (short name under @context), plus the optional
     992              : /// attributeCount (unsigned integer) / attributeTypes / typeNames members.
     993          118 : pub async fn attribute_info(
     994          118 :     State(st): State<AppState>,
     995          118 :     Path(attr): Path<String>,
     996          118 :     CleanParams(params): CleanParams,
     997          118 :     headers: HeaderMap,
     998          118 : ) -> Response {
     999          118 :     let go = async {
    1000          118 :         let tenant = tenant_from(&headers)?;
    1001          118 :         check_params(&params, &["local"])?;
    1002          112 :         let accept = parse_accept(&headers)?;
    1003          110 :         let ctx = request_context(&st.loader, &headers).await?;
    1004              :         // 5.7.10 / Table 6.28.2-1: the FQN or a shortname the request
    1005              :         // @context defines. Discovery reads the tenant's attribute
    1006              :         // inventory and touches no stored document, so a name that
    1007              :         // expands to nothing an Entity carries is simply absent —
    1008              :         // ResourceNotFound below, not a rejected name.
    1009          110 :         let iri = ctx.expand_key(&attr);
    1010          110 :         let filter =
    1011          110 :             gate!(st, &tenant, &headers, "5.7.10", attrs: std::slice::from_ref(&iri)).await?;
    1012          110 :         let view = view_of(&filter);
    1013              :         // attributeCount, attributeTypes and typeNames (5.2.28) are all
    1014              :         // properties of the entities that CARRY this attribute; the rest of
    1015              :         // the tenant is not read.
    1016          110 :         let (stats, partial) = attr_stats(
    1017          110 :             &st,
    1018          110 :             &tenant,
    1019          110 :             *crate::bounds::MAX_FOLD_DOCS,
    1020          110 :             Narrow::Attr(&iri),
    1021          110 :             view.as_ref().map(|r| (r, &*ctx)),
    1022              :         )
    1023          110 :         .await?;
    1024          110 :         let Some((count, attr_types, etypes)) = stats.get(&iri) else {
    1025           84 :             let mut resp = ApiError::from(NgsiError::ResourceNotFound(format!(
    1026           84 :                 "attribute {attr} not found"
    1027           84 :             )))
    1028           84 :             .into_response();
    1029           84 :             mark_partial(&mut resp, partial, &st.host_alias);
    1030           84 :             return Ok(resp);
    1031              :         };
    1032           26 :         let payload = json!({
    1033           26 :             "id": iri,
    1034           26 :             "type": "Attribute",
    1035           26 :             "attributeName": ctx.compact_iri(&iri),
    1036           26 :             "attributeCount": count,
    1037           26 :             "attributeTypes": attr_types.iter().cloned().collect::<Vec<_>>(),
    1038           28 :             "typeNames": etypes.iter().map(|t| ctx.compact_iri(t)).collect::<Vec<_>>(),
    1039              :         });
    1040           26 :         let mut resp = respond(StatusCode::OK, payload, &ctx, accept, &tenant);
    1041           26 :         mark_partial(&mut resp, partial, &st.host_alias);
    1042           26 :         filter.mark(&st.host_alias, resp.headers_mut());
    1043           26 :         Ok::<_, ApiError>(resp)
    1044          118 :     };
    1045          118 :     go.await.unwrap_or_else(|e| e.into_response())
    1046          118 : }
    1047              : 
    1048              : #[cfg(test)]
    1049              : mod discovery_folds {
    1050              :     use super::*;
    1051              :     use crate::state::AppState;
    1052              :     use antares_model::TenantId;
    1053              :     use antares_store::Kind;
    1054              :     use serde_json::json;
    1055              : 
    1056              :     const V: &str = "https://uri.etsi.org/ngsi-ld/default-context/v";
    1057              :     const R: &str = "https://uri.etsi.org/ngsi-ld/default-context/r";
    1058              :     const BUILDING: &str = "https://uri.etsi.org/ngsi-ld/default-context/Building";
    1059              :     const SENSOR: &str = "https://uri.etsi.org/ngsi-ld/default-context/Sensor";
    1060              : 
    1061              :     /// A ceiling no test fixture reaches, so the fold sees every entity.
    1062              :     const ALL: usize = 1_000;
    1063              : 
    1064          500 :     fn tid(t: &str) -> TenantId {
    1065          500 :         TenantId::new(t).expect("tenant")
    1066          500 :     }
    1067              : 
    1068          364 :     async fn seed(st: &AppState, tenant: &str, id: &str, doc: Value) {
    1069          364 :         assert!(st
    1070          364 :             .store
    1071          364 :             .create(&tid(tenant), Kind::Entity, id, doc)
    1072          364 :             .await
    1073          364 :             .expect("create"));
    1074          364 :     }
    1075              : 
    1076              :     /// 5.2.26: entityCount is the number of entity instances of the type;
    1077              :     /// 5.2.25 attributeNames lists the attributes those instances can have —
    1078              :     /// Entity members are not among them.
    1079              :     #[tokio::test]
    1080            4 :     async fn type_stats_folds_types_and_attribute_names() {
    1081            4 :         let st = AppState::new("test".into());
    1082            4 :         seed(
    1083            4 :             &st,
    1084            4 :             "ta",
    1085            4 :             "urn:ngsi-ld:B:1",
    1086            4 :             json!({
    1087            4 :                 "id": "urn:ngsi-ld:B:1",
    1088            4 :                 "type": [BUILDING, SENSOR],
    1089            4 :                 "createdAt": "2026-01-01T00:00:00Z",
    1090            4 :                 "modifiedAt": "2026-01-01T00:00:00Z",
    1091            4 :                 "scope": ["/a/b"],
    1092            4 :                 V: [{"type": "Property", "value": 1}],
    1093            4 :                 R: [{"type": "Relationship", "object": "urn:ngsi-ld:B:2"}],
    1094            4 :             }),
    1095            4 :         )
    1096            4 :         .await;
    1097            4 :         seed(
    1098            4 :             &st,
    1099            4 :             "ta",
    1100            4 :             "urn:ngsi-ld:B:2",
    1101            4 :             json!({
    1102            4 :                 "id": "urn:ngsi-ld:B:2",
    1103            4 :                 "type": [BUILDING],
    1104            4 :                 V: [{"type": "GeoProperty", "value": {"type": "Point", "coordinates": [0, 0]}}],
    1105            4 :             }),
    1106            4 :         )
    1107            4 :         .await;
    1108            4 :         let (stats, partial) = type_stats(&st, &tid("ta"), ALL, Narrow::Tenant, None)
    1109            4 :             .await
    1110            4 :             .expect("stats");
    1111            4 :         assert!(!partial, "two entities are under any sane ceiling");
    1112            4 :         let (count, attrs) = stats.get(BUILDING).expect("Building");
    1113            4 :         assert_eq!(*count, 2, "two entities carry the Building type");
    1114            4 :         assert_eq!(stats.get(SENSOR).expect("Sensor").0, 1);
    1115            4 :         assert!(attrs.contains_key(V) && attrs.contains_key(R));
    1116           20 :         for meta in ["id", "type", "scope", "createdAt", "modifiedAt"] {
    1117           20 :             assert!(
    1118           20 :                 !attrs.contains_key(meta),
    1119            4 :                 "{meta} must not be reported as an attribute name"
    1120            4 :             );
    1121            4 :         }
    1122            4 :         // 5.2.28 attributeTypes: every attribute type an instance carried
    1123            4 :         assert!(attrs[V].contains("Property") && attrs[V].contains("GeoProperty"));
    1124            4 :     }
    1125              : 
    1126              :     /// the catalogue answer equals the fold for three grant shapes,
    1127              :     /// and a hidden Attribute or a refused type never appears in it.
    1128              :     #[tokio::test]
    1129            4 :     async fn the_catalogue_answers_what_the_fold_answers() {
    1130              :         use crate::policy::{Filter, Rule};
    1131            4 :         let st = AppState::new("test".into());
    1132           40 :         let at = |n: u32| format!("https://uri.etsi.org/ngsi-ld/default-context/a{n}");
    1133              :         // Buildings with v (some with r or a rare a1), a Building+Sensor, Sensors with a2
    1134          240 :         for n in 0..60u32 {
    1135          240 :             let id = format!("urn:ngsi-ld:B:{n:03}");
    1136          240 :             let mut doc =
    1137          240 :                 json!({"id": id, "type": [BUILDING], V: [{"type": "Property", "value": n}]});
    1138          240 :             if n % 2 == 0 {
    1139          120 :                 doc[R] = json!([{"type": "Relationship", "object": "urn:ngsi-ld:B:000"}]);
    1140          120 :             }
    1141          240 :             if n == 57 {
    1142            4 :                 doc[at(1)] = json!([{"type": "Property", "value": 1}]);
    1143          236 :             }
    1144          240 :             seed(&st, "tq", &id, doc).await;
    1145              :         }
    1146            4 :         seed(
    1147            4 :             &st,
    1148            4 :             "tq",
    1149            4 :             "urn:ngsi-ld:BS:1",
    1150            4 :             json!({"id": "urn:ngsi-ld:BS:1",
    1151            4 :             "type": [BUILDING, SENSOR], V: [{"type": "Property", "value": 99}],
    1152            4 :             at(3): [{"type": "Property", "value": 3}]}),
    1153            4 :         )
    1154            4 :         .await;
    1155           20 :         for n in 0..5u32 {
    1156           20 :             let id = format!("urn:ngsi-ld:S:{n}");
    1157           20 :             seed(
    1158           20 :                 &st,
    1159           20 :                 "tq",
    1160           20 :                 &id,
    1161           20 :                 json!({"id": id, "type": [SENSOR],
    1162           20 :                 at(2): [{"type": "Property", "value": n}]}),
    1163           20 :             )
    1164           20 :             .await;
    1165              :         }
    1166            4 :         let rule = |types: &[&str], q: Option<&str>, attrs: &[&str]| Rule {
    1167           12 :             types: types.iter().map(|t| (*t).to_owned()).collect(),
    1168            8 :             q: q.map(|q| Box::new(antares_ql::parse_q(q).expect("q"))),
    1169           32 :             attributes: attrs.iter().map(|a| (*a).to_owned()).collect(),
    1170            8 :             ..Rule::default()
    1171            8 :         };
    1172            4 :         let a1 = at(1);
    1173            4 :         let a2 = at(2);
    1174            4 :         let a3 = at(3);
    1175            4 :         let shapes = [
    1176            4 :             ("the whole view", None),
    1177            4 :             (
    1178            4 :                 "a hidden attribute",
    1179            4 :                 Some(Filter {
    1180            4 :                     rules: vec![rule(&[BUILDING, SENSOR], None, &[V, &a1, &a2, &a3])],
    1181            4 :                     ..Filter::default()
    1182            4 :                 }),
    1183            4 :             ),
    1184            4 :             (
    1185            4 :                 "a refused type",
    1186            4 :                 Some(Filter {
    1187            4 :                     rules: vec![rule(&[BUILDING], Some("v>50"), &[V, R, &a1, &a3])],
    1188            4 :                     ..Filter::default()
    1189            4 :                 }),
    1190            4 :             ),
    1191            4 :         ];
    1192            4 :         let ctx = antares_jsonld::pinned::core_context();
    1193            4 :         let t = tid("tq");
    1194           12 :         for (shape, filter) in shapes {
    1195           12 :             let repr = filter.as_ref().map(crate::repr::narrowing);
    1196           12 :             let view = repr.as_ref().map(|r| (r, &ctx));
    1197           12 :             let (fold, cut) = type_stats(&st, &t, ALL, Narrow::Tenant, view)
    1198           12 :                 .await
    1199           12 :                 .expect("fold");
    1200           12 :             let fold: Shapes = fold
    1201           12 :                 .into_iter()
    1202           24 :                 .map(|(t, (_, attrs))| (t, attrs.into_keys().collect()))
    1203           12 :                 .collect();
    1204           12 :             let (got, partial) = type_shapes(&st, &t, ALL, view)
    1205           12 :                 .await
    1206           12 :                 .expect("shapes")
    1207           12 :                 .expect("the memory store keeps a catalogue");
    1208           12 :             assert!(!cut && !partial, "{shape}");
    1209           12 :             assert_eq!(got, fold, "{shape}: /types?details=true");
    1210           12 :             let (fold_attrs, _) = attr_stats(&st, &t, ALL, Narrow::Tenant, view)
    1211           12 :                 .await
    1212           12 :                 .expect("fold");
    1213           12 :             let fold_attrs: BTreeMap<_, _> = fold_attrs
    1214           12 :                 .into_iter()
    1215           52 :                 .map(|(a, (_, _, ty))| (a, ty))
    1216           12 :                 .collect();
    1217           12 :             let got_attrs: BTreeMap<_, _> = attrs_of(got.clone())
    1218           12 :                 .into_iter()
    1219           52 :                 .map(|(a, (_, _, ty))| (a, ty))
    1220           12 :                 .collect();
    1221           12 :             assert_eq!(got_attrs, fold_attrs, "{shape}: /attributes?details=true");
    1222           12 :             match shape {
    1223           12 :                 "the whole view" => {
    1224            4 :                     assert!(got[BUILDING].contains(&a1), "a rare attribute is found");
    1225            4 :                     assert!(got[SENSOR].contains(&a3) && got[SENSOR].contains(&a2));
    1226            4 :                 }
    1227            8 :                 "a hidden attribute" => {
    1228            4 :                     assert!(
    1229            4 :                         !got_attrs.contains_key(R),
    1230            4 :                         "a hidden attribute never appears"
    1231            4 :                     );
    1232            4 :                     assert!(got_attrs.contains_key(&a1));
    1233            4 :                 }
    1234            4 :                 _ => {
    1235            4 :                     assert!(
    1236            4 :                         !got_attrs.contains_key(&a2),
    1237            4 :                         "nothing only a Sensor carries"
    1238            4 :                     );
    1239            4 :                     assert_eq!(got[SENSOR].len(), 2, "the Building+Sensor alone: {got:?}");
    1240            4 :                     assert!(
    1241            4 :                         got[BUILDING].contains(&a1),
    1242            4 :                         "a rare attribute of a shown Entity"
    1243            4 :                     );
    1244            4 :                 }
    1245            4 :             }
    1246            4 :         }
    1247            4 :     }
    1248              : 
    1249              :     /// One shared datastore, one tenant per request: an entity of another
    1250              :     /// tenant contributes to no fold (4.15 multi-tenancy).
    1251              :     #[tokio::test]
    1252            4 :     async fn stats_are_tenant_scoped() {
    1253            4 :         let st = AppState::new("test".into());
    1254            4 :         seed(
    1255            4 :             &st,
    1256            4 :             "ta",
    1257            4 :             "urn:ngsi-ld:B:1",
    1258            4 :             json!({
    1259            4 :                 "id": "urn:ngsi-ld:B:1",
    1260            4 :                 "type": [BUILDING],
    1261            4 :                 V: [{"type": "Property", "value": 1}],
    1262            4 :             }),
    1263            4 :         )
    1264            4 :         .await;
    1265            8 :         for other in ["tb", TenantId::DEFAULT] {
    1266            8 :             let t = tid(other);
    1267            8 :             assert!(
    1268            8 :                 type_stats(&st, &t, ALL, Narrow::Tenant, None)
    1269            8 :                     .await
    1270            8 :                     .expect("stats")
    1271            4 :                     .0
    1272            8 :                     .is_empty(),
    1273            4 :                 "{other} must not see another tenant's types"
    1274            4 :             );
    1275            8 :             assert!(
    1276            8 :                 attr_stats(&st, &t, ALL, Narrow::Tenant, None)
    1277            8 :                     .await
    1278            8 :                     .expect("stats")
    1279            4 :                     .0
    1280            8 :                     .is_empty(),
    1281            4 :                 "{other} must not see another tenant's attributes"
    1282            4 :             );
    1283            4 :         }
    1284            4 :         assert!(type_stats(&st, &tid("ta"), ALL, Narrow::Tenant, None)
    1285            4 :             .await
    1286            4 :             .expect("stats")
    1287            4 :             .0
    1288            4 :             .contains_key(BUILDING));
    1289            4 :     }
    1290              : 
    1291              :     /// Table 5.2.28-1: attributeCount is the "number of attribute instances
    1292              :     /// with this attribute name" — multi-instance attributes (4.5.5
    1293              :     /// datasetId) count once per instance, not once per entity.
    1294              :     #[tokio::test]
    1295            4 :     async fn attr_stats_counts_attribute_instances() {
    1296            4 :         let st = AppState::new("test".into());
    1297            4 :         seed(
    1298            4 :             &st,
    1299            4 :             "ta",
    1300            4 :             "urn:ngsi-ld:B:1",
    1301            4 :             json!({
    1302            4 :                 "id": "urn:ngsi-ld:B:1",
    1303            4 :                 "type": [BUILDING],
    1304            4 :                 V: [
    1305            4 :                     {"type": "Property", "value": 1,
    1306            4 :                      "datasetId": "urn:ngsi-ld:ds:1"},
    1307            4 :                     {"type": "Property", "value": 2,
    1308            4 :                      "datasetId": "urn:ngsi-ld:ds:2"},
    1309            4 :                 ],
    1310            4 :             }),
    1311            4 :         )
    1312            4 :         .await;
    1313            4 :         seed(
    1314            4 :             &st,
    1315            4 :             "ta",
    1316            4 :             "urn:ngsi-ld:B:2",
    1317            4 :             json!({
    1318            4 :                 "id": "urn:ngsi-ld:B:2",
    1319            4 :                 "type": [SENSOR],
    1320            4 :                 V: [{"type": "Property", "value": 3}],
    1321            4 :             }),
    1322            4 :         )
    1323            4 :         .await;
    1324            4 :         let (stats, _) = attr_stats(&st, &tid("ta"), ALL, Narrow::Tenant, None)
    1325            4 :             .await
    1326            4 :             .expect("stats");
    1327            4 :         let (count, atypes, etypes) = stats.get(V).expect("v");
    1328            4 :         assert_eq!(*count, 3, "two instances on B:1 plus one on B:2");
    1329            4 :         assert!(atypes.contains("Property"));
    1330            4 :         assert_eq!(etypes.len(), 2, "both entity types are reported");
    1331            4 :         assert!(
    1332            4 :             !stats.contains_key("createdAt") && !stats.contains_key("id"),
    1333            4 :             "Entity members must not appear in the attribute fold"
    1334            4 :         );
    1335            4 :     }
    1336              : 
    1337              :     /// 4.8 expiresAt: an expired entity no longer exists, and an expired
    1338              :     /// attribute instance is gone with it — neither is discoverable.
    1339              :     #[tokio::test]
    1340            4 :     async fn expired_entities_and_attributes_are_not_discoverable() {
    1341            4 :         let st = AppState::new("test".into());
    1342            4 :         seed(
    1343            4 :             &st,
    1344            4 :             "ta",
    1345            4 :             "urn:ngsi-ld:B:1",
    1346            4 :             json!({
    1347            4 :                 "id": "urn:ngsi-ld:B:1",
    1348            4 :                 "type": [BUILDING],
    1349            4 :                 "expiresAt": "2000-01-01T00:00:00Z",
    1350            4 :                 V: [{"type": "Property", "value": 1}],
    1351            4 :             }),
    1352            4 :         )
    1353            4 :         .await;
    1354            4 :         seed(
    1355            4 :             &st,
    1356            4 :             "ta",
    1357            4 :             "urn:ngsi-ld:B:2",
    1358            4 :             json!({
    1359            4 :                 "id": "urn:ngsi-ld:B:2",
    1360            4 :                 "type": [SENSOR],
    1361            4 :                 V: [{"type": "Property", "value": 1,
    1362            4 :                      "expiresAt": "2000-01-01T00:00:00Z"}],
    1363            4 :                 R: [{"type": "Relationship", "object": "urn:ngsi-ld:B:1"}],
    1364            4 :             }),
    1365            4 :         )
    1366            4 :         .await;
    1367            4 :         let (stats, _) = type_stats(&st, &tid("ta"), ALL, Narrow::Tenant, None)
    1368            4 :             .await
    1369            4 :             .expect("stats");
    1370            4 :         assert!(
    1371            4 :             !stats.contains_key(BUILDING),
    1372              :             "the expired entity must not appear in the type list"
    1373              :         );
    1374            4 :         let (count, attrs) = stats.get(SENSOR).expect("Sensor");
    1375            4 :         assert_eq!(*count, 1);
    1376            4 :         assert!(
    1377            4 :             !attrs.contains_key(V),
    1378              :             "the expired attribute instance must not appear"
    1379              :         );
    1380            4 :         assert!(attrs.contains_key(R));
    1381            4 :         assert!(!attr_stats(&st, &tid("ta"), ALL, Narrow::Tenant, None)
    1382            4 :             .await
    1383            4 :             .expect("stats")
    1384            4 :             .0
    1385            4 :             .contains_key(V));
    1386            4 :     }
    1387              : 
    1388              :     /// A tenant with no entities has no types and no attributes — an empty
    1389              :     /// fold, not an error, and never "incomplete".
    1390              :     #[tokio::test]
    1391            4 :     async fn empty_tenant_folds_to_nothing() {
    1392            4 :         let st = AppState::new("test".into());
    1393            8 :         for t in ["ta", TenantId::DEFAULT] {
    1394            8 :             let (types, partial) = type_stats(&st, &tid(t), ALL, Narrow::Tenant, None)
    1395            8 :                 .await
    1396            8 :                 .expect("stats");
    1397            8 :             assert!(types.is_empty() && !partial);
    1398            8 :             let (attrs, partial) = attr_stats(&st, &tid(t), ALL, Narrow::Tenant, None)
    1399            8 :                 .await
    1400            8 :                 .expect("stats");
    1401            8 :             assert!(attrs.is_empty() && !partial);
    1402            8 :             let (rows, partial) = scan(&st, &tid(t), ALL, Narrow::Tenant, None)
    1403            8 :                 .await
    1404            8 :                 .expect("scan");
    1405            8 :             assert!(rows.is_empty() && !partial);
    1406            4 :         }
    1407            4 :     }
    1408              : 
    1409              :     /// The fold reads at most `max` entities and says so when the tenant
    1410              :     /// holds more, instead of silently answering from a prefix.
    1411              :     #[tokio::test]
    1412            4 :     async fn scan_stops_at_the_ceiling_and_reports_it() {
    1413            4 :         let st = AppState::new("test".into());
    1414           20 :         for i in 0..5 {
    1415           20 :             let id = format!("urn:ngsi-ld:B:{i}");
    1416           20 :             let mut doc = serde_json::Map::new();
    1417           20 :             doc.insert("id".into(), Value::String(id.clone()));
    1418           20 :             doc.insert("type".into(), json!([format!("{BUILDING}{i}")]));
    1419           20 :             doc.insert(format!("{V}{i}"), json!([{"type": "Property", "value": i}]));
    1420           20 :             seed(&st, "ta", &id, Value::Object(doc)).await;
    1421              :         }
    1422           16 :         for (max, want) in [(1, 1), (4, 4), (5, 5), (6, 5)] {
    1423           16 :             let (rows, partial) = scan(&st, &tid("ta"), max, Narrow::Tenant, None)
    1424           16 :                 .await
    1425           16 :                 .expect("scan");
    1426           16 :             assert_eq!(rows.len(), want, "max {max}");
    1427           16 :             assert_eq!(partial, max < 5, "max {max} incompleteness");
    1428              :         }
    1429              :         // the folds inherit the bound: one entity read, one type, one attr
    1430            4 :         let (types, partial) = type_stats(&st, &tid("ta"), 1, Narrow::Tenant, None)
    1431            4 :             .await
    1432            4 :             .expect("stats");
    1433            4 :         assert_eq!(types.len(), 1, "the fold read past its ceiling");
    1434            4 :         assert!(partial, "a truncated type fold must report itself");
    1435            4 :         let (attrs, partial) = attr_stats(&st, &tid("ta"), 1, Narrow::Tenant, None)
    1436            4 :             .await
    1437            4 :             .expect("stats");
    1438            4 :         assert_eq!(attrs.len(), 1, "the fold read past its ceiling");
    1439            4 :         assert!(partial, "a truncated attribute fold must report itself");
    1440              :         // at the exact size the answer is complete, so nothing is flagged
    1441            4 :         let (types, partial) = type_stats(&st, &tid("ta"), 5, Narrow::Tenant, None)
    1442            4 :             .await
    1443            4 :             .expect("stats");
    1444            4 :         assert_eq!(types.len(), 5);
    1445            4 :         assert!(!partial);
    1446            4 :     }
    1447              : 
    1448              :     /// the fold walks by keyset, one page after the last id it read,
    1449              :     /// so every Entity is handed over exactly once and in id order, also when
    1450              :     /// a page holds a single row (an OFFSET walk re-read every skipped row:
    1451              :     /// 57 s for `/types?details=true` over one production tenant).
    1452              :     #[tokio::test]
    1453            4 :     async fn a_keyset_walk_hands_each_entity_over_once() {
    1454            4 :         let st = AppState::new("test".into());
    1455           20 :         for i in [3, 0, 4, 1, 2] {
    1456           20 :             let id = format!("urn:ngsi-ld:B:{i}");
    1457           20 :             seed(&st, "ta", &id, json!({"id": id, "type": [BUILDING]})).await;
    1458            4 :         }
    1459           12 :         for page in [1, 2, 500] {
    1460           12 :             let mut ids = Vec::new();
    1461           60 :             let more = scan_each(&st, &tid("ta"), ALL, Narrow::Tenant, None, page, |d| {
    1462           60 :                 ids.push(d["id"].as_str().map(str::to_owned));
    1463           60 :             })
    1464           12 :             .await
    1465           12 :             .expect("scan");
    1466           60 :             let want: Vec<_> = (0..5).map(|i| Some(format!("urn:ngsi-ld:B:{i}"))).collect();
    1467           12 :             assert_eq!(ids, want, "page {page}");
    1468           12 :             assert!(!more, "page {page}: the whole tenant was read");
    1469            4 :         }
    1470            4 :     }
    1471              : 
    1472              :     /// A complete answer carries no warning; a truncated one carries exactly
    1473              :     /// one 199 (RFC 7234 5.5.1) naming this broker.
    1474              :     #[test]
    1475            4 :     fn mark_partial_emits_one_199_only_when_truncated() {
    1476            4 :         let mut resp = StatusCode::OK.into_response();
    1477            4 :         mark_partial(&mut resp, false, "broker-a");
    1478            4 :         assert!(resp.headers().get("NGSILD-Warning").is_none());
    1479            4 :         mark_partial(&mut resp, true, "broker-a");
    1480            4 :         let vals: Vec<_> = resp.headers().get_all("NGSILD-Warning").iter().collect();
    1481            4 :         assert_eq!(vals.len(), 1);
    1482            4 :         let v = vals[0].to_str().expect("ascii");
    1483            4 :         assert!(v.starts_with("199 broker-a \"") && v.ends_with('"'), "{v}");
    1484            4 :         assert!(v.contains("incomplete"), "{v}");
    1485            4 :     }
    1486              : 
    1487              :     /// The narrowing predicate selects exactly what the datastore's
    1488              :     /// `types @> ARRAY[t]` / `entity ?| ARRAY[a]` select, and nothing else.
    1489              :     #[test]
    1490            4 :     fn narrow_selects_only_its_own_entities() {
    1491            4 :         let doc = json!({
    1492            4 :             "id": "urn:ngsi-ld:B:1",
    1493            4 :             "type": [BUILDING],
    1494            4 :             V: [{"type": "Property", "value": 1}],
    1495              :         });
    1496            4 :         assert!(Narrow::Tenant.matches(&doc));
    1497            4 :         assert!(Narrow::Tenant.matches(&json!({})));
    1498            4 :         assert!(Narrow::Type(BUILDING).matches(&doc));
    1499            4 :         assert!(Narrow::Attr(V).matches(&doc));
    1500              :         // the negatives: a type this entity does not carry, an attribute it
    1501              :         // does not carry, a prefix of either, and a typeless document
    1502            4 :         assert!(!Narrow::Type(SENSOR).matches(&doc));
    1503            4 :         assert!(!Narrow::Attr(R).matches(&doc));
    1504            4 :         assert!(!Narrow::Type("https://uri.etsi.org/ngsi-ld/default-context/Build").matches(&doc));
    1505            4 :         assert!(!Narrow::Type(BUILDING).matches(&json!({"id": "urn:ngsi-ld:B:2"})));
    1506            4 :         assert!(!Narrow::Attr(V).matches(&json!({"type": [BUILDING]})));
    1507            4 :     }
    1508              : 
    1509              :     /// 5.7.7/5.7.10 name ONE type / ONE attribute, so the ceiling applies to
    1510              :     /// the entities that carry it: a type sitting past the ceiling in the
    1511              :     /// tenant's row order is still found, with an exact entityCount and no
    1512              :     /// incompleteness warning. Under the tenant-wide fold the same ceiling
    1513              :     /// hides it — that is the difference the narrowing buys.
    1514              :     #[tokio::test]
    1515            4 :     async fn narrowed_folds_reach_past_the_tenant_wide_ceiling() {
    1516            4 :         let st = AppState::new("test".into());
    1517           20 :         for i in 0..5 {
    1518           20 :             let id = format!("urn:ngsi-ld:B:{i}");
    1519           20 :             let mut doc = serde_json::Map::new();
    1520           20 :             doc.insert("id".into(), Value::String(id.clone()));
    1521           20 :             doc.insert("type".into(), json!([format!("{BUILDING}{i}")]));
    1522           20 :             doc.insert(format!("{V}{i}"), json!([{"type": "Property", "value": i}]));
    1523           20 :             seed(&st, "ta", &id, Value::Object(doc)).await;
    1524              :         }
    1525            4 :         let last_type = format!("{BUILDING}4");
    1526            4 :         let last_attr = format!("{V}4");
    1527              : 
    1528              :         // tenant-wide, ceiling 1: the last entity is not read at all
    1529            4 :         let (wide, partial) = type_stats(&st, &tid("ta"), 1, Narrow::Tenant, None)
    1530            4 :             .await
    1531            4 :             .expect("stats");
    1532            4 :         assert!(!wide.contains_key(&last_type), "the ceiling hid the type");
    1533            4 :         assert!(partial);
    1534              : 
    1535              :         // narrowed to that type, same ceiling: found, exact, complete
    1536            4 :         let (stats, partial) = type_stats(&st, &tid("ta"), 1, Narrow::Type(&last_type), None)
    1537            4 :             .await
    1538            4 :             .expect("stats");
    1539            4 :         let (count, attrs) = stats.get(&last_type).expect("the narrowed type");
    1540            4 :         assert_eq!(*count, 1, "one instance carries it");
    1541            4 :         assert!(!partial, "the narrowed set fits under the ceiling");
    1542            4 :         assert!(attrs.contains_key(&last_attr));
    1543           16 :         for i in 0..4 {
    1544           16 :             assert!(
    1545           16 :                 !stats.contains_key(&format!("{BUILDING}{i}")),
    1546              :                 "no other type may be folded into a 5.7.7 answer"
    1547              :             );
    1548              :         }
    1549              : 
    1550              :         // and the same for one attribute (5.7.10)
    1551            4 :         let (wide, partial) = attr_stats(&st, &tid("ta"), 1, Narrow::Tenant, None)
    1552            4 :             .await
    1553            4 :             .expect("stats");
    1554            4 :         assert!(!wide.contains_key(&last_attr), "the ceiling hid the attr");
    1555            4 :         assert!(partial);
    1556            4 :         let (stats, partial) = attr_stats(&st, &tid("ta"), 1, Narrow::Attr(&last_attr), None)
    1557            4 :             .await
    1558            4 :             .expect("stats");
    1559            4 :         let (count, atypes, etypes) = stats.get(&last_attr).expect("the narrowed attr");
    1560            4 :         assert_eq!(*count, 1);
    1561            4 :         assert!(!partial);
    1562            4 :         assert!(atypes.contains("Property"));
    1563            4 :         assert_eq!(etypes.iter().cloned().collect::<Vec<_>>(), vec![last_type]);
    1564           16 :         for i in 0..4 {
    1565           16 :             assert!(
    1566           16 :                 !stats.contains_key(&format!("{V}{i}")),
    1567              :                 "no other attribute may be folded into a 5.7.10 answer"
    1568              :             );
    1569              :         }
    1570              : 
    1571              :         // a narrowing that matches nothing is an empty fold, not a prefix of
    1572              :         // the tenant — this is what makes 5.7.7 answer ResourceNotFound
    1573            4 :         let (stats, partial) = type_stats(&st, &tid("ta"), 1, Narrow::Type(SENSOR), None)
    1574            4 :             .await
    1575            4 :             .expect("stats");
    1576            4 :         assert!(stats.is_empty() && !partial);
    1577            4 :         let (stats, partial) = attr_stats(&st, &tid("ta"), 1, Narrow::Attr(R), None)
    1578            4 :             .await
    1579            4 :             .expect("stats");
    1580            4 :         assert!(stats.is_empty() && !partial);
    1581            4 :     }
    1582              : 
    1583              :     /// A narrowed fold is tenant-scoped like the tenant-wide one (4.15): the
    1584              :     /// type exists, but only for the tenant that created its instances.
    1585              :     #[tokio::test]
    1586            4 :     async fn narrowed_folds_are_tenant_scoped() {
    1587            4 :         let st = AppState::new("test".into());
    1588            4 :         seed(
    1589            4 :             &st,
    1590            4 :             "ta",
    1591            4 :             "urn:ngsi-ld:B:1",
    1592            4 :             json!({
    1593            4 :                 "id": "urn:ngsi-ld:B:1",
    1594            4 :                 "type": [BUILDING],
    1595            4 :                 V: [{"type": "Property", "value": 1}],
    1596            4 :             }),
    1597            4 :         )
    1598            4 :         .await;
    1599            8 :         for other in ["tb", TenantId::DEFAULT] {
    1600            8 :             let t = tid(other);
    1601            8 :             assert!(
    1602            8 :                 type_stats(&st, &t, ALL, Narrow::Type(BUILDING), None)
    1603            8 :                     .await
    1604            8 :                     .expect("stats")
    1605            4 :                     .0
    1606            8 :                     .is_empty(),
    1607            4 :                 "{other} must not see another tenant's type"
    1608            4 :             );
    1609            8 :             assert!(
    1610            8 :                 attr_stats(&st, &t, ALL, Narrow::Attr(V), None)
    1611            8 :                     .await
    1612            8 :                     .expect("stats")
    1613            4 :                     .0
    1614            8 :                     .is_empty(),
    1615            4 :                 "{other} must not see another tenant's attribute"
    1616            4 :             );
    1617            4 :         }
    1618            4 :         assert_eq!(
    1619            4 :             type_stats(&st, &tid("ta"), ALL, Narrow::Type(BUILDING), None)
    1620            4 :                 .await
    1621            4 :                 .expect("stats")
    1622            4 :                 .0
    1623            4 :                 .get(BUILDING)
    1624            4 :                 .expect("Building")
    1625            4 :                 .0,
    1626            4 :             1
    1627            4 :         );
    1628            4 :     }
    1629              : 
    1630              :     /// 5.7.5.4: the answer lists the types "for which entity instances exist
    1631              :     /// within the NGSI-LD system" — the entity written a moment ago included.
    1632              :     /// No fold result may be carried over a write.
    1633              :     #[tokio::test]
    1634            4 :     async fn a_freshly_created_type_is_in_the_very_next_fold() {
    1635            4 :         let st = AppState::new("test".into());
    1636            4 :         let t = tid("ta");
    1637            4 :         assert!(type_stats(&st, &t, ALL, Narrow::Tenant, None)
    1638            4 :             .await
    1639            4 :             .expect("stats")
    1640              :             .0
    1641            4 :             .is_empty());
    1642            4 :         seed(
    1643            4 :             &st,
    1644            4 :             "ta",
    1645            4 :             "urn:ngsi-ld:B:1",
    1646            4 :             json!({"id": "urn:ngsi-ld:B:1", "type": [BUILDING],
    1647            4 :                    V: [{"type": "Property", "value": 1}]}),
    1648            4 :         )
    1649            4 :         .await;
    1650            4 :         let (stats, _) = type_stats(&st, &t, ALL, Narrow::Tenant, None)
    1651            4 :             .await
    1652            4 :             .expect("stats");
    1653            4 :         assert!(stats.contains_key(BUILDING), "the new type must be visible");
    1654            4 :         assert!(!stats.contains_key(SENSOR));
    1655            4 :         assert!(attr_stats(&st, &t, ALL, Narrow::Tenant, None)
    1656            4 :             .await
    1657            4 :             .expect("stats")
    1658              :             .0
    1659            4 :             .contains_key(V));
    1660              :         // a second type on a second write, and a narrowed fold sees it too
    1661            4 :         seed(
    1662            4 :             &st,
    1663            4 :             "ta",
    1664            4 :             "urn:ngsi-ld:B:2",
    1665            4 :             json!({"id": "urn:ngsi-ld:B:2", "type": [SENSOR],
    1666            4 :                    R: [{"type": "Relationship", "object": "urn:ngsi-ld:B:1"}]}),
    1667            4 :         )
    1668            4 :         .await;
    1669            4 :         let (stats, _) = type_stats(&st, &t, ALL, Narrow::Tenant, None)
    1670            4 :             .await
    1671            4 :             .expect("stats");
    1672            4 :         assert!(stats.contains_key(BUILDING) && stats.contains_key(SENSOR));
    1673            4 :         assert!(type_stats(&st, &t, ALL, Narrow::Type(SENSOR), None)
    1674            4 :             .await
    1675            4 :             .expect("stats")
    1676              :             .0
    1677            4 :             .contains_key(SENSOR));
    1678            4 :         assert!(attr_stats(&st, &t, ALL, Narrow::Attr(R), None)
    1679            4 :             .await
    1680            4 :             .expect("stats")
    1681              :             .0
    1682            4 :             .contains_key(R));
    1683              :         // and a deleted type disappears from the next fold just as fast
    1684            4 :         assert!(st
    1685            4 :             .store
    1686            4 :             .delete(&t, Kind::Entity, "urn:ngsi-ld:B:2")
    1687            4 :             .await
    1688            4 :             .expect("delete"));
    1689            4 :         let (stats, _) = type_stats(&st, &t, ALL, Narrow::Tenant, None)
    1690            4 :             .await
    1691            4 :             .expect("stats");
    1692            4 :         assert!(
    1693            4 :             !stats.contains_key(SENSOR),
    1694            4 :             "a type with no instances left must not be listed"
    1695            4 :         );
    1696            4 :     }
    1697              : }
    1698              : 
    1699              : #[cfg(test)]
    1700              : mod probe_plan {
    1701              :     use super::*;
    1702              :     use crate::policy::Rule;
    1703              : 
    1704           48 :     fn rule(types: &[&str], q: Option<&str>) -> Rule {
    1705              :         Rule {
    1706           48 :             types: types.iter().map(|t| (*t).to_owned()).collect(),
    1707           48 :             q: q.map(|q| Box::new(antares_ql::parse_q(q).expect("q"))),
    1708           48 :             ..Rule::default()
    1709              :         }
    1710           48 :     }
    1711              : 
    1712           20 :     fn groups(p: &Plan<'_>) -> Vec<Vec<Vec<String>>> {
    1713           20 :         p.iter().map(|(g, _)| g.clone()).collect()
    1714           20 :     }
    1715              : 
    1716           16 :     fn set(types: &[&str]) -> Option<BTreeSet<String>> {
    1717           24 :         Some(types.iter().map(|t| (*t).to_owned()).collect())
    1718           16 :     }
    1719              : 
    1720              :     /// The view of `types_list_pg`: Cars in zone `a`, every Bus; one Car is a
    1721              :     /// Tree too. A type no Entity carries beside a rule's type is not probed
    1722              :     /// at all: that empty two-type group walked a whole Tenant, and
    1723              :     /// a count of probes cannot flake on a slow runner the way a clock does.
    1724              :     #[test]
    1725            4 :     fn a_type_beside_no_rule_type_sends_no_probe() {
    1726            4 :         let (car, bus) = (rule(&["Car"], Some("zone==\"a\"")), rule(&["Bus"], None));
    1727            4 :         let rules = [&car, &bus];
    1728            4 :         let beside = [set(&["Car", "Tree"]), set(&["Bus"])];
    1729            8 :         for name in ["Stop", "Lamp"] {
    1730            8 :             assert!(plan(name, &rules, false, &beside).is_empty(), "{name}");
    1731              :         }
    1732            4 :         let car_plan = plan("Car", &rules, false, &beside);
    1733            4 :         assert_eq!(groups(&car_plan), [vec![vec!["Car".to_owned()]]]);
    1734            4 :         assert!(
    1735            4 :             car_plan[0].1.and_then(|r| r.q.as_deref()).is_some(),
    1736              :             "the rule's q is pushed down"
    1737              :         );
    1738            4 :         assert_eq!(
    1739            4 :             groups(&plan("Tree", &rules, false, &beside)),
    1740            4 :             [vec![vec!["Tree".to_owned(), "Car".to_owned()]]]
    1741              :         );
    1742            4 :         assert_eq!(
    1743            4 :             groups(&plan("Bus", &rules, false, &beside)),
    1744            4 :             [vec![vec!["Bus".to_owned()]]]
    1745              :         );
    1746            4 :     }
    1747              : 
    1748              :     /// a rule's ids, or its one id pattern's literal, narrow the
    1749              :     /// probe's read; ids beside patterns, or two patterns, select by OR and
    1750              :     /// narrow nothing.
    1751              :     #[test]
    1752            4 :     fn a_rules_id_selection_narrows_its_probe() {
    1753            4 :         let by = |ids: &[&str], patterns: &[&str]| Rule {
    1754           20 :             ids: ids.iter().map(|i| (*i).to_owned()).collect(),
    1755           20 :             id_patterns: patterns.iter().map(|p| (*p).to_owned()).collect(),
    1756           20 :             ..rule(&["KPI"], None)
    1757           20 :         };
    1758            4 :         let kpi = by(&[], &["^urn:ngsi-ld:KPI:[a-z]+:metro:stat-[a-z]+$"]);
    1759            4 :         assert_eq!(
    1760            4 :             id_narrowing(Some(&kpi)),
    1761            4 :             (None, Some((":metro:stat-".to_owned(), false)))
    1762              :         );
    1763            4 :         let two = by(&["urn:a", "urn:b"], &[]);
    1764            4 :         assert_eq!(
    1765            4 :             id_narrowing(Some(&two)),
    1766            4 :             (Some(vec!["urn:a", "urn:b"]), None)
    1767              :         );
    1768           12 :         for wide in [
    1769            4 :             by(&["urn:a"], &["urn:b.*"]),
    1770            4 :             by(&[], &["urn:a.*", "urn:b.*"]),
    1771            4 :             by(&[], &[]),
    1772            4 :         ] {
    1773           12 :             assert_eq!(id_narrowing(Some(&wide)), (None, None));
    1774              :         }
    1775            4 :         assert_eq!(id_narrowing(None), (None, None));
    1776            4 :     }
    1777              : 
    1778              :     /// A store without the `beside` read still probes beside each rule's
    1779              :     /// types, so it answers right, only slower; no rules is one bare probe.
    1780              :     #[test]
    1781            4 :     fn without_beside_every_rule_type_is_probed_and_no_rules_probe_alone() {
    1782            4 :         let car = rule(&["Car"], None);
    1783            4 :         assert_eq!(
    1784            4 :             groups(&plan("Stop", &[&car], false, &[None])),
    1785            4 :             [vec![vec!["Stop".to_owned(), "Car".to_owned()]]]
    1786              :         );
    1787            4 :         assert_eq!(
    1788            4 :             groups(&plan("Stop", &[], true, &[])),
    1789            4 :             [vec![vec!["Stop".to_owned()]]]
    1790              :         );
    1791            4 :         assert!(plan("Stop", &[&rule(&[], None)], false, &[None]).is_empty());
    1792            4 :     }
    1793              : 
    1794              :     /// A view with rules lists only the names its probes can show, so the
    1795              :     /// Tenant's whole type list goes unread; every name `plan`
    1796              :     /// probes is among them. No rules, or an unknown `beside`, lists none.
    1797              :     #[test]
    1798            4 :     fn a_ruled_view_reads_candidates_instead_of_the_type_list() {
    1799            4 :         let (car, bus, none) = (rule(&["Car"], None), rule(&["Bus"], None), rule(&[], None));
    1800            4 :         let rules = [&car, &bus, &none];
    1801            4 :         let beside = [set(&["Car", "Tree"]), set(&["Bus"]), None];
    1802            4 :         let names = candidates(false, &rules, &beside).expect("known");
    1803            4 :         assert_eq!(names, ["Bus", "Car", "Tree"]);
    1804           20 :         for name in ["Bus", "Car", "Lamp", "Stop", "Tree"] {
    1805           20 :             let probed = !plan(name, &rules, false, &beside).is_empty();
    1806           20 :             assert_eq!(probed, names.contains(&name.to_owned()), "{name}");
    1807              :         }
    1808            4 :         assert_eq!(candidates(true, &[], &[]), None);
    1809            4 :         assert_eq!(candidates(false, &[&car], &[None]), None);
    1810            4 :         assert_eq!(candidates(false, &[], &[]), Some(Vec::new()));
    1811            4 :     }
    1812              : }
        

Generated by: LCOV version 2.0-1