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(¶ms, &["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(¶ms, &["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(¶ms, &["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(¶ms, &["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 : }
|