rete-graph 0.2.2__tar.gz → 0.2.3__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (71) hide show
  1. {rete_graph-0.2.2 → rete_graph-0.2.3}/PKG-INFO +1 -1
  2. {rete_graph-0.2.2 → rete_graph-0.2.3}/clients/python/Cargo.lock +1 -1
  3. {rete_graph-0.2.2 → rete_graph-0.2.3}/clients/python/Cargo.toml +1 -1
  4. {rete_graph-0.2.2 → rete_graph-0.2.3}/clients/python/src/lib.rs +11 -0
  5. {rete_graph-0.2.2 → rete_graph-0.2.3}/crates/rete-core/src/dict.rs +35 -9
  6. {rete_graph-0.2.2 → rete_graph-0.2.3}/crates/rete-core/src/dictionary.rs +9 -1
  7. {rete_graph-0.2.2 → rete_graph-0.2.3}/crates/rete-core/src/extbuild.rs +7 -1
  8. {rete_graph-0.2.2 → rete_graph-0.2.3}/crates/rete-core/src/file.rs +15 -0
  9. rete_graph-0.2.3/crates/rete-core/src/geo3.rs +207 -0
  10. {rete_graph-0.2.2 → rete_graph-0.2.3}/crates/rete-core/src/index.rs +30 -10
  11. {rete_graph-0.2.2 → rete_graph-0.2.3}/crates/rete-core/src/lib.rs +1 -0
  12. {rete_graph-0.2.2 → rete_graph-0.2.3}/crates/rete-core/src/sparql/expr.rs +68 -0
  13. {rete_graph-0.2.2 → rete_graph-0.2.3}/crates/rete-core/src/sparql/lower.rs +10 -0
  14. {rete_graph-0.2.2 → rete_graph-0.2.3}/crates/rete-core/src/sparql.rs +5 -0
  15. rete_graph-0.2.3/crates/rete-core/tests/properties.proptest-regressions +7 -0
  16. {rete_graph-0.2.2 → rete_graph-0.2.3}/crates/rete-core/tests/ranged.rs +50 -0
  17. {rete_graph-0.2.2 → rete_graph-0.2.3}/pyproject.toml +1 -1
  18. {rete_graph-0.2.2 → rete_graph-0.2.3}/Cargo.toml +0 -0
  19. {rete_graph-0.2.2 → rete_graph-0.2.3}/README.md +0 -0
  20. {rete_graph-0.2.2 → rete_graph-0.2.3}/clients/python/.gitignore +0 -0
  21. {rete_graph-0.2.2 → rete_graph-0.2.3}/clients/python/README.md +0 -0
  22. {rete_graph-0.2.2 → rete_graph-0.2.3}/clients/python/examples/jupyterlite-build-tutorial.ipynb +0 -0
  23. {rete_graph-0.2.2 → rete_graph-0.2.3}/clients/python/examples/jupyterlite-demo.ipynb +0 -0
  24. {rete_graph-0.2.2 → rete_graph-0.2.3}/clients/python/examples/tutorial.ipynb +0 -0
  25. {rete_graph-0.2.2 → rete_graph-0.2.3}/clients/python/src/readers.rs +0 -0
  26. {rete_graph-0.2.2 → rete_graph-0.2.3}/clients/python/tests/conftest.py +0 -0
  27. {rete_graph-0.2.2 → rete_graph-0.2.3}/clients/python/tests/test_rete_graph.py +0 -0
  28. {rete_graph-0.2.2 → rete_graph-0.2.3}/crates/rete-core/Cargo.toml +0 -0
  29. {rete_graph-0.2.2 → rete_graph-0.2.3}/crates/rete-core/LICENSE +0 -0
  30. {rete_graph-0.2.2 → rete_graph-0.2.3}/crates/rete-core/README.md +0 -0
  31. {rete_graph-0.2.2 → rete_graph-0.2.3}/crates/rete-core/examples/coherence_bench.rs +0 -0
  32. {rete_graph-0.2.2 → rete_graph-0.2.3}/crates/rete-core/examples/parallel_bench.rs +0 -0
  33. {rete_graph-0.2.2 → rete_graph-0.2.3}/crates/rete-core/examples/qbench.rs +0 -0
  34. {rete_graph-0.2.2 → rete_graph-0.2.3}/crates/rete-core/src/bgp.rs +0 -0
  35. {rete_graph-0.2.2 → rete_graph-0.2.3}/crates/rete-core/src/block_cache.rs +0 -0
  36. {rete_graph-0.2.2 → rete_graph-0.2.3}/crates/rete-core/src/geo.rs +0 -0
  37. {rete_graph-0.2.2 → rete_graph-0.2.3}/crates/rete-core/src/header.rs +0 -0
  38. {rete_graph-0.2.2 → rete_graph-0.2.3}/crates/rete-core/src/ingest.rs +0 -0
  39. {rete_graph-0.2.2 → rete_graph-0.2.3}/crates/rete-core/src/meta.rs +0 -0
  40. {rete_graph-0.2.2 → rete_graph-0.2.3}/crates/rete-core/src/parallel.rs +0 -0
  41. {rete_graph-0.2.2 → rete_graph-0.2.3}/crates/rete-core/src/pyramid.rs +0 -0
  42. {rete_graph-0.2.2 → rete_graph-0.2.3}/crates/rete-core/src/reach.rs +0 -0
  43. {rete_graph-0.2.2 → rete_graph-0.2.3}/crates/rete-core/src/reader.rs +0 -0
  44. {rete_graph-0.2.2 → rete_graph-0.2.3}/crates/rete-core/src/reason.rs +0 -0
  45. {rete_graph-0.2.2 → rete_graph-0.2.3}/crates/rete-core/src/results.rs +0 -0
  46. {rete_graph-0.2.2 → rete_graph-0.2.3}/crates/rete-core/src/row.rs +0 -0
  47. {rete_graph-0.2.2 → rete_graph-0.2.3}/crates/rete-core/src/schema_pyramid.rs +0 -0
  48. {rete_graph-0.2.2 → rete_graph-0.2.3}/crates/rete-core/src/service.rs +0 -0
  49. {rete_graph-0.2.2 → rete_graph-0.2.3}/crates/rete-core/src/shacl.rs +0 -0
  50. {rete_graph-0.2.2 → rete_graph-0.2.3}/crates/rete-core/src/sparql/aggregate.rs +0 -0
  51. {rete_graph-0.2.2 → rete_graph-0.2.3}/crates/rete-core/src/sparql/eval.rs +0 -0
  52. {rete_graph-0.2.2 → rete_graph-0.2.3}/crates/rete-core/src/sparql/path.rs +0 -0
  53. {rete_graph-0.2.2 → rete_graph-0.2.3}/crates/rete-core/src/sparql/ql.rs +0 -0
  54. {rete_graph-0.2.2 → rete_graph-0.2.3}/crates/rete-core/src/terms.rs +0 -0
  55. {rete_graph-0.2.2 → rete_graph-0.2.3}/crates/rete-core/src/text_index.rs +0 -0
  56. {rete_graph-0.2.2 → rete_graph-0.2.3}/crates/rete-core/src/tiling.rs +0 -0
  57. {rete_graph-0.2.2 → rete_graph-0.2.3}/crates/rete-core/src/triples.rs +0 -0
  58. {rete_graph-0.2.2 → rete_graph-0.2.3}/crates/rete-core/src/varint.rs +0 -0
  59. {rete_graph-0.2.2 → rete_graph-0.2.3}/crates/rete-core/tests/community_eval.rs +0 -0
  60. {rete_graph-0.2.2 → rete_graph-0.2.3}/crates/rete-core/tests/compatibility_v1.rs +0 -0
  61. {rete_graph-0.2.2 → rete_graph-0.2.3}/crates/rete-core/tests/fixtures/v1/minimal.rete +0 -0
  62. {rete_graph-0.2.2 → rete_graph-0.2.3}/crates/rete-core/tests/fixtures/v1/source.nq +0 -0
  63. {rete_graph-0.2.2 → rete_graph-0.2.3}/crates/rete-core/tests/properties.rs +0 -0
  64. {rete_graph-0.2.2 → rete_graph-0.2.3}/crates/rete-core/tests/public_api.rs +0 -0
  65. {rete_graph-0.2.2 → rete_graph-0.2.3}/crates/rete-core/tests/robustness.rs +0 -0
  66. {rete_graph-0.2.2 → rete_graph-0.2.3}/crates/rete-core/tests/roundtrip.rs +0 -0
  67. {rete_graph-0.2.2 → rete_graph-0.2.3}/crates/rete-core/tests/service_federation.rs +0 -0
  68. {rete_graph-0.2.2 → rete_graph-0.2.3}/crates/rete-core/tests/shacl_core.rs +0 -0
  69. {rete_graph-0.2.2 → rete_graph-0.2.3}/crates/rete-core/tests/sparql_integration.rs +0 -0
  70. {rete_graph-0.2.2 → rete_graph-0.2.3}/crates/rete-core/tests/sparql_nondeterministic.rs +0 -0
  71. {rete_graph-0.2.2 → rete_graph-0.2.3}/python/rete_graph/__init__.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: rete-graph
3
- Version: 0.2.2
3
+ Version: 0.2.3
4
4
  Classifier: Development Status :: 4 - Beta
5
5
  Classifier: Intended Audience :: Developers
6
6
  Classifier: Intended Audience :: Science/Research
@@ -671,7 +671,7 @@ dependencies = [
671
671
 
672
672
  [[package]]
673
673
  name = "rete-python"
674
- version = "0.2.1"
674
+ version = "0.2.3"
675
675
  dependencies = [
676
676
  "pyo3",
677
677
  "rete-core",
@@ -5,7 +5,7 @@
5
5
  name = "rete-python"
6
6
  # Keep in lockstep with pyproject.toml's [project] version — __version__
7
7
  # comes from here (CARGO_PKG_VERSION), the wheel metadata from pyproject.
8
- version = "0.2.1"
8
+ version = "0.2.3"
9
9
  edition = "2021"
10
10
  license = "Apache-2.0"
11
11
  repository = "https://github.com/caviri/rete"
@@ -127,6 +127,15 @@ impl Graph {
127
127
  }
128
128
  Ok(())
129
129
  }
130
+
131
+ /// The incompleteness verdict is PER CALL on this resident handle: reset
132
+ /// the sticky failure flags before evaluating, so one transient network
133
+ /// failure fails only the call it happened in — failed tiles/chunks are
134
+ /// never cached, so the next call simply retries them. (Same shape as the
135
+ /// browser engine's RemoteGraph entry points.)
136
+ fn fresh_verdict(&self) {
137
+ self.rete.reset_load_failures();
138
+ }
130
139
  }
131
140
 
132
141
  #[pymethods]
@@ -143,6 +152,7 @@ impl Graph {
143
152
  graph: Option<&str>,
144
153
  format: &str,
145
154
  ) -> PyResult<String> {
155
+ self.fresh_verdict();
146
156
  let out = py
147
157
  .allow_threads(|| -> Result<String, String> {
148
158
  let shapes =
@@ -169,6 +179,7 @@ impl Graph {
169
179
  /// QL entailment by query rewriting.
170
180
  #[pyo3(signature = (query, *, reason=false))]
171
181
  fn query(&self, py: Python<'_>, query: &str, reason: bool) -> PyResult<String> {
182
+ self.fresh_verdict();
172
183
  let out = py
173
184
  .allow_threads(|| {
174
185
  if reason {
@@ -287,8 +287,16 @@ impl SectionChunk {
287
287
  /// The byte offset (into `data`) of each run in this chunk, computed once by
288
288
  /// scanning the decompressed body and cached. `data` must be this chunk's
289
289
  /// body. Offset 0 is the chunk's first run (chunks are run-aligned); every
290
- /// `restart_interval` terms starts the next run.
290
+ /// `restart_interval` terms starts the next run. An EMPTY body (the
291
+ /// transient-fetch-failure sentinel — never a real chunk) is not cached, or
292
+ /// a failed chunk's empty offset table would outlive the retryable data.
291
293
  fn run_offsets(&self, data: &[u8], restart_interval: usize) -> &[usize] {
294
+ if let Some(r) = self.runs.get() {
295
+ return r;
296
+ }
297
+ if data.is_empty() {
298
+ return &[];
299
+ }
292
300
  self.runs
293
301
  .get_or_init(|| chunk_run_offsets(data, restart_interval))
294
302
  }
@@ -427,19 +435,37 @@ impl ChunkedSection {
427
435
  self.meta.term_count
428
436
  }
429
437
 
430
- /// Did any chunk fetch fail since this section was opened? (Sticky.)
438
+ /// Did any chunk fetch fail since this section was opened — or since the
439
+ /// last [`reset_load_failure`](Self::reset_load_failure)?
431
440
  pub fn load_incomplete(&self) -> bool {
432
441
  self.failed.load(Ordering::Relaxed)
433
442
  }
434
443
 
444
+ /// Forget recorded fetch failures (see `GraphIndex::reset_load_failure` —
445
+ /// the per-query reset for resident sessions). Failed chunks were never
446
+ /// cached, so the next resolution retries them.
447
+ pub fn reset_load_failure(&self) {
448
+ self.failed.store(false, Ordering::Relaxed);
449
+ }
450
+
451
+ /// A FAILED fetch records the failure and returns an empty slice WITHOUT
452
+ /// caching it, so a later resolution retries the chunk — a transient
453
+ /// network error must not permanently poison a resident session.
435
454
  fn chunk_data(&self, ci: usize) -> &[u8] {
436
- self.chunks[ci].data.get_or_init(|| match &self.loader {
437
- Some(load) => load(ci).unwrap_or_else(|| {
438
- self.failed.store(true, Ordering::Relaxed);
439
- Vec::new()
440
- }),
441
- None => Vec::new(),
442
- })
455
+ let cell = &self.chunks[ci].data;
456
+ if let Some(d) = cell.get() {
457
+ return d;
458
+ }
459
+ match &self.loader {
460
+ Some(load) => match load(ci) {
461
+ Some(bytes) => cell.get_or_init(|| bytes),
462
+ None => {
463
+ self.failed.store(true, Ordering::Relaxed);
464
+ &[]
465
+ }
466
+ },
467
+ None => cell.get_or_init(Vec::new),
468
+ }
443
469
  }
444
470
 
445
471
  /// The chunk holding `run` (chunks ascend by `first_run`; the first chunk
@@ -154,11 +154,19 @@ impl Dictionary {
154
154
  self.has_quoted_triples
155
155
  }
156
156
 
157
- /// Did any lazy chunk fetch fail since this dictionary was opened?
157
+ /// Did any lazy chunk fetch fail since this dictionary was opened — or
158
+ /// since the last [`reset_load_failure`](Self::reset_load_failure)?
158
159
  pub fn load_incomplete(&self) -> bool {
159
160
  self.sections.iter().any(|s| s.load_incomplete())
160
161
  }
161
162
 
163
+ /// Forget recorded fetch failures across every section (see
164
+ /// `GraphIndex::reset_load_failure` — the per-query reset for resident
165
+ /// sessions). Failed chunks were never cached, so they simply retry.
166
+ pub fn reset_load_failure(&self) {
167
+ self.sections.iter().for_each(|s| s.reset_load_failure());
168
+ }
169
+
162
170
  /// Batch-fault every unloaded chunk of every section (no-op for local
163
171
  /// dictionaries). Callers about to resolve *every* term — export, dump —
164
172
  /// call this once so the sweep coalesces into a few range reads instead
@@ -1621,7 +1621,13 @@ mod tests {
1621
1621
  let tmp = TmpDir { dir: spill.clone() };
1622
1622
  let global_tri = spill.join("global.tri");
1623
1623
  let codec = crate::file::writer_codec();
1624
- let run_len = ((16u64 << 30) / 2 / 24) as usize;
1624
+ // RETE_RESUME_BUDGET_MB (default 16384) sizes the sort runs — resume
1625
+ // with a smaller budget when the machine is busier than the build was
1626
+ let budget_mb: u64 = std::env::var("RETE_RESUME_BUDGET_MB")
1627
+ .ok()
1628
+ .and_then(|v| v.parse().ok())
1629
+ .unwrap_or(16384);
1630
+ let run_len = ((budget_mb << 20) / 2 / 24) as usize;
1625
1631
 
1626
1632
  let mut sections = Vec::new();
1627
1633
  for perm in crate::index::ALL_PERMS {
@@ -2408,6 +2408,21 @@ impl Rete {
2408
2408
  || self.named_graphs.iter().any(|(_, g)| g.load_incomplete())
2409
2409
  }
2410
2410
 
2411
+ /// Forget recorded lazy-fetch failures — the start-of-evaluation reset for
2412
+ /// a RESIDENT session (a browser worker holding one `Rete` across many
2413
+ /// queries): it makes [`index_incomplete`](Self::index_incomplete) a
2414
+ /// per-query verdict instead of a per-open one, so a single transient
2415
+ /// network failure no longer fails every subsequent query on the session.
2416
+ /// Sound because failed tiles/chunks are never cached — the next
2417
+ /// evaluation simply retries the fetch.
2418
+ pub fn reset_load_failures(&self) {
2419
+ self.index.reset_load_failure();
2420
+ self.dict.reset_load_failure();
2421
+ for (_, g) in &self.named_graphs {
2422
+ g.reset_load_failure();
2423
+ }
2424
+ }
2425
+
2411
2426
  fn resolve_query_pattern(
2412
2427
  &self,
2413
2428
  s: Option<&str>,
@@ -0,0 +1,207 @@
1
+ //! A 3D extension of the GeoSPARQL built-ins (`geo3`).
2
+ //!
3
+ //! GeoSPARQL (see [`crate::geo`]) is planar: its `wktLiteral` parser drops the Z
4
+ //! ordinate and every relation/distance is 2D. `geo3` adds true three-dimensional
5
+ //! topology and distance over **axis-aligned bounding boxes** (AABBs) derived from
6
+ //! 3D geometry literals — `POINT Z`, `MULTIPOINT Z`, `LINESTRING Z`,
7
+ //! `POLYHEDRALSURFACE Z`, and a compact `BOX3D(minx miny minz, maxx maxy maxz)`.
8
+ //!
9
+ //! It is deliberately AABB-level (not exact solid geometry): that is what makes it
10
+ //! cheap enough to evaluate inside a FILTER over a whole graph, and it is exactly
11
+ //! the model materialised by the z-anatomy dataset. Coordinates are an abstract
12
+ //! Cartesian 3-space in the literal's own unit (z-anatomy uses millimetres); no CRS
13
+ //! reprojection is performed. wasm-safe: `core`/`alloc` + `f64` only.
14
+ //!
15
+ //! Exposed as SPARQL functions (base `https://w3id.org/rete/geo3/function/`):
16
+ //! * `distance3D(g1, g2)` → xsd:double, min gap between the AABBs (0 if they meet)
17
+ //! * `contains3D(g1, g2)` → xsd:boolean, AABB(g1) ⊇ AABB(g2)
18
+ //! * `within3D(g1, g2)` → xsd:boolean, AABB(g1) ⊆ AABB(g2)
19
+ //! * `adjacent3D(g1, g2 [, gap])` → xsd:boolean, AABBs within `gap` (default 0) on every axis
20
+
21
+ /// Datatype IRI for a 3D WKT literal (`"POINT Z(x y z)"^^geo3:wktLiteral3D`).
22
+ pub(crate) const WKT3: &str = "https://w3id.org/rete/geo3#wktLiteral3D";
23
+ /// Datatype IRI for a bounding-box literal (`"BOX3D(...)"^^geo3:box3dLiteral`).
24
+ pub(crate) const BOX3D: &str = "https://w3id.org/rete/geo3#box3dLiteral";
25
+
26
+ const EPS: f64 = 1e-9;
27
+
28
+ /// An axis-aligned 3D bounding box (a point is the degenerate `min == max`).
29
+ #[derive(Clone, Copy, Debug, PartialEq)]
30
+ pub(crate) struct Aabb {
31
+ pub min: [f64; 3],
32
+ pub max: [f64; 3],
33
+ }
34
+
35
+ /// The 3D topological relation to test.
36
+ #[derive(Clone, Copy)]
37
+ pub(crate) enum Rel3 {
38
+ Contains,
39
+ Within,
40
+ Adjacent,
41
+ }
42
+
43
+ const KEYWORDS: &[&str] = &[
44
+ "POINT", "MULTIPOINT", "LINESTRING", "MULTILINESTRING", "POLYGON",
45
+ "MULTIPOLYGON", "POLYHEDRALSURFACE", "TIN", "GEOMETRYCOLLECTION", "BOX3D",
46
+ ];
47
+
48
+ /// Parse a 3D WKT / BOX3D lexical form into its AABB. A leading CRS URI
49
+ /// (`<…> POINT Z(…)`) is stripped and ignored. Returns `None` on an unknown
50
+ /// keyword, an EMPTY/degenerate geometry, or no finite coordinate. Coordinates
51
+ /// are grouped by the WKT commas; nested parens are flattened (only the AABB is
52
+ /// needed), and a coordinate's 3rd ordinate defaults to 0 if absent.
53
+ pub(crate) fn parse(input: &str) -> Option<Aabb> {
54
+ let mut s = input.trim();
55
+ if let Some(rest) = s.strip_prefix('<') {
56
+ let end = rest.find('>')?;
57
+ s = rest[end + 1..].trim_start();
58
+ }
59
+ let open = s.find('(')?;
60
+ // leading token before '(' is the geometry keyword; a "Z"/"M"/"ZM" dimension
61
+ // tag (if any) is a separate whitespace-delimited word and is ignored.
62
+ let head = s[..open].trim().to_ascii_uppercase();
63
+ let kw = head.split_whitespace().next()?;
64
+ if !KEYWORDS.contains(&kw) {
65
+ return None;
66
+ }
67
+ let close = s.rfind(')')?;
68
+ if close <= open {
69
+ return None;
70
+ }
71
+ let body = &s[open + 1..close];
72
+ // flatten nested rings/parens; group coordinates by comma
73
+ let flat: String = body.chars().map(|c| if c == '(' || c == ')' { ' ' } else { c }).collect();
74
+
75
+ let mut mn = [f64::INFINITY; 3];
76
+ let mut mx = [f64::NEG_INFINITY; 3];
77
+ let mut any = false;
78
+ for group in flat.split(',') {
79
+ let mut ords = [0.0_f64; 3];
80
+ let mut nread = 0usize;
81
+ for tok in group.split_whitespace() {
82
+ if nread >= 3 {
83
+ break;
84
+ }
85
+ match tok.parse::<f64>() {
86
+ Ok(v) if v.is_finite() => {
87
+ ords[nread] = v;
88
+ nread += 1;
89
+ }
90
+ _ => {} // ignore an "EMPTY" token or stray text
91
+ }
92
+ }
93
+ if nread >= 2 {
94
+ for i in 0..3 {
95
+ mn[i] = mn[i].min(ords[i]);
96
+ mx[i] = mx[i].max(ords[i]);
97
+ }
98
+ any = true;
99
+ }
100
+ }
101
+ any.then_some(Aabb { min: mn, max: mx })
102
+ }
103
+
104
+ /// Per-axis clearance between two boxes (0 if they overlap on that axis).
105
+ fn axis_gap(a_min: f64, a_max: f64, b_min: f64, b_max: f64) -> f64 {
106
+ (a_min - b_max).max(b_min - a_max).max(0.0)
107
+ }
108
+
109
+ /// Minimum Euclidean distance between the two AABBs (0 if they intersect).
110
+ /// For point geometries this is the exact point-to-point distance.
111
+ pub(crate) fn distance(a: &Aabb, b: &Aabb) -> f64 {
112
+ let mut sum = 0.0;
113
+ for i in 0..3 {
114
+ let g = axis_gap(a.min[i], a.max[i], b.min[i], b.max[i]);
115
+ sum += g * g;
116
+ }
117
+ sum.sqrt()
118
+ }
119
+
120
+ /// `a` contains `b`: b's box lies inside a's box on every axis (within EPS).
121
+ fn contains(a: &Aabb, b: &Aabb) -> bool {
122
+ (0..3).all(|i| a.min[i] <= b.min[i] + EPS && a.max[i] >= b.max[i] - EPS)
123
+ }
124
+
125
+ /// Boxes are within `gap` on every axis (gap 0 = touching/overlapping).
126
+ fn adjacent(a: &Aabb, b: &Aabb, gap: f64) -> bool {
127
+ let g = gap.max(0.0);
128
+ (0..3).all(|i| a.min[i] <= b.max[i] + g + EPS && a.max[i] >= b.min[i] - g - EPS)
129
+ }
130
+
131
+ /// Evaluate a 3D relation. `gap` is only used by `Adjacent`.
132
+ pub(crate) fn relate(rel: Rel3, a: &Aabb, b: &Aabb, gap: f64) -> bool {
133
+ match rel {
134
+ Rel3::Contains => contains(a, b),
135
+ Rel3::Within => contains(b, a),
136
+ Rel3::Adjacent => adjacent(a, b, gap),
137
+ }
138
+ }
139
+
140
+ #[cfg(test)]
141
+ mod tests {
142
+ use super::*;
143
+
144
+ fn p(s: &str) -> Aabb {
145
+ parse(s).unwrap()
146
+ }
147
+
148
+ #[test]
149
+ fn parse_point_box_multipoint() {
150
+ assert_eq!(
151
+ p("POINT Z(1 2 3)"),
152
+ Aabb { min: [1.0, 2.0, 3.0], max: [1.0, 2.0, 3.0] }
153
+ );
154
+ assert_eq!(
155
+ p("BOX3D(0 0 0, 10 20 30)"),
156
+ Aabb { min: [0.0, 0.0, 0.0], max: [10.0, 20.0, 30.0] }
157
+ );
158
+ // multipoint / linestring → bounding box over all coords
159
+ assert_eq!(
160
+ p("MULTIPOINT Z(1 1 1, 5 -2 9)"),
161
+ Aabb { min: [1.0, -2.0, 1.0], max: [5.0, 1.0, 9.0] }
162
+ );
163
+ // CRS prefix ignored; 2D point → z defaults to 0
164
+ assert_eq!(p("<urn:crs> POINT(4 5)").min, [4.0, 5.0, 0.0]);
165
+ // nested parens (a box-like polyhedral surface) flatten to the AABB
166
+ let a = p("POLYHEDRALSURFACE Z(((0 0 0, 1 0 0, 1 1 2)))");
167
+ assert_eq!(a.min, [0.0, 0.0, 0.0]);
168
+ assert_eq!(a.max, [1.0, 1.0, 2.0]);
169
+ assert!(parse("FOO(1 2 3)").is_none());
170
+ assert!(parse("POINT EMPTY").is_none());
171
+ }
172
+
173
+ #[test]
174
+ fn distance_3d() {
175
+ // axis-aligned separation on Z only
176
+ let d = distance(&p("POINT Z(0 0 0)"), &p("POINT Z(0 0 5)"));
177
+ assert!((d - 5.0).abs() < 1e-9);
178
+ // 3-4-0 -> 5 in the XY plane, plus 12 on Z -> 13
179
+ let d = distance(&p("POINT Z(0 0 0)"), &p("POINT Z(3 4 12)"));
180
+ assert!((d - 13.0).abs() < 1e-9);
181
+ // overlapping boxes -> 0
182
+ assert_eq!(
183
+ distance(&p("BOX3D(0 0 0, 10 10 10)"), &p("POINT Z(5 5 5)")),
184
+ 0.0
185
+ );
186
+ // box-to-box gap on one axis
187
+ let d = distance(&p("BOX3D(0 0 0, 1 1 1)"), &p("BOX3D(0 0 4, 1 1 5)"));
188
+ assert!((d - 3.0).abs() < 1e-9);
189
+ }
190
+
191
+ #[test]
192
+ fn relations_3d() {
193
+ let big = p("BOX3D(0 0 0, 10 10 10)");
194
+ let small = p("BOX3D(2 2 2, 4 4 4)");
195
+ assert!(relate(Rel3::Contains, &big, &small, 0.0));
196
+ assert!(!relate(Rel3::Contains, &small, &big, 0.0));
197
+ assert!(relate(Rel3::Within, &small, &big, 0.0));
198
+ // adjacency: 2 mm apart on Z, not touching at gap 0 but adjacent at gap 3
199
+ let a = p("BOX3D(0 0 0, 1 1 1)");
200
+ let b = p("BOX3D(0 0 3, 1 1 4)");
201
+ assert!(!relate(Rel3::Adjacent, &a, &b, 0.0));
202
+ assert!(relate(Rel3::Adjacent, &a, &b, 3.0));
203
+ // touching faces are adjacent at gap 0
204
+ let c = p("BOX3D(1 0 0, 2 1 1)");
205
+ assert!(relate(Rel3::Adjacent, &a, &c, 0.0));
206
+ }
207
+ }
@@ -523,11 +523,22 @@ impl GraphIndex {
523
523
  self.read_concurrency
524
524
  }
525
525
 
526
- /// Did any tile fetch fail since this index was opened? (Sticky.)
526
+ /// Did any tile fetch fail since this index was opened — or since the last
527
+ /// [`reset_load_failure`](Self::reset_load_failure)?
527
528
  pub fn load_incomplete(&self) -> bool {
528
529
  self.load_failed.load(std::sync::atomic::Ordering::Relaxed)
529
530
  }
530
531
 
532
+ /// Forget recorded fetch failures — the start-of-evaluation reset for a
533
+ /// RESIDENT session, making the incompleteness verdict per-query instead of
534
+ /// per-open (one transient network blip used to fail every later query on
535
+ /// the session). Safe because failed tiles are never cached: the next scan
536
+ /// simply retries them.
537
+ pub fn reset_load_failure(&self) {
538
+ self.load_failed
539
+ .store(false, std::sync::atomic::Ordering::Relaxed);
540
+ }
541
+
531
542
  /// True for a remote/lazy index (tiles fault in over a `RangeReader`). The
532
543
  /// join planner uses this to pick read-aware strategies: here a per-row index
533
544
  /// probe is a network round-trip, not a memory lookup, so a left-deep scan +
@@ -601,18 +612,27 @@ impl GraphIndex {
601
612
  }
602
613
 
603
614
  /// The tile's block image, faulting it in through the loader if remote.
615
+ /// A FAILED fetch records the failure and returns an empty slice WITHOUT
616
+ /// caching it, so a later evaluation retries the tile — a transient
617
+ /// network error must not permanently poison a long-lived (resident)
618
+ /// session with an empty tile masquerading as data.
604
619
  fn tile_data(&self, section: usize, tile: usize) -> &[u8] {
605
- self.sections[section][tile].data.get_or_init(|| {
606
- match &self.loader {
607
- Some(load) => load(section, tile).unwrap_or_else(|| {
620
+ let cell = &self.sections[section][tile].data;
621
+ if let Some(d) = cell.get() {
622
+ return d;
623
+ }
624
+ match &self.loader {
625
+ Some(load) => match load(section, tile) {
626
+ Some(bytes) => cell.get_or_init(|| bytes),
627
+ None => {
608
628
  self.load_failed
609
629
  .store(true, std::sync::atomic::Ordering::Relaxed);
610
- Vec::new()
611
- }),
612
- // A local tile with no data was constructed empty on purpose.
613
- None => Vec::new(),
614
- }
615
- })
630
+ &[]
631
+ }
632
+ },
633
+ // A local tile with no data was constructed empty on purpose.
634
+ None => cell.get_or_init(Vec::new),
635
+ }
616
636
  }
617
637
 
618
638
  /// Total triple count (sum of the SPO tiles' zone counts). For a remote
@@ -19,6 +19,7 @@ pub mod dictionary;
19
19
  pub mod file;
20
20
  #[doc(hidden)]
21
21
  pub mod geo;
22
+ pub mod geo3;
22
23
  #[doc(hidden)]
23
24
  pub mod header;
24
25
  #[doc(hidden)]
@@ -400,6 +400,17 @@ fn func_value(f: Builtin, args: &[FExpr], ctx: &Ctx, b: &Row) -> Option<Rc<str>>
400
400
  Some(crate::geo::GEO_WKT),
401
401
  )))
402
402
  }
403
+ // geo3 3D relations in value position → xsd:boolean.
404
+ Builtin::Geo3Contains | Builtin::Geo3Within | Builtin::Geo3Adjacent => {
405
+ let gap = args.get(2).and_then(|e| e.value(ctx, b));
406
+ let r = geo3_relation(f, &a0()?, &args.get(1)?.value(ctx, b)?, gap)?;
407
+ Some(Rc::from(bool_literal(r)))
408
+ }
409
+ // geo3:distance3D(g1, g2) → xsd:double (min AABB gap, literal's own unit).
410
+ Builtin::Geo3Distance => {
411
+ let d = crate::geo3::distance(&geo3_arg(&a0()?)?, &geo3_arg(&args.get(1)?.value(ctx, b)?)?);
412
+ Some(Rc::from(make_literal(&fmt_plain(d), None, Some(XSD_DOUBLE))))
413
+ }
403
414
  _ => None,
404
415
  }
405
416
  }
@@ -433,6 +444,35 @@ fn geo_relation(f: Builtin, t1: &str, t2: &str) -> Option<bool> {
433
444
  crate::geo::relate(rel, &wkt_arg(t1)?, &wkt_arg(t2)?)
434
445
  }
435
446
 
447
+ /// A 3D geometry literal (`geo3:wktLiteral3D` / `geo3:box3dLiteral`, leniently also
448
+ /// `geo:wktLiteral` / plain string) → its AABB. `None` for any other term (type error).
449
+ fn geo3_arg(token: &str) -> Option<crate::geo3::Aabb> {
450
+ if !token.starts_with('"') {
451
+ return None;
452
+ }
453
+ match datatype_iri(token).as_deref() {
454
+ Some(crate::geo3::WKT3)
455
+ | Some(crate::geo3::BOX3D)
456
+ | Some(crate::geo::GEO_WKT)
457
+ | Some(XSD_STRING)
458
+ | None => {}
459
+ Some(_) => return None,
460
+ }
461
+ crate::geo3::parse(&crate::terms::lexical(token))
462
+ }
463
+
464
+ /// Evaluate a geo3 relation builtin on two geometry terms (+ optional gap for adjacency).
465
+ fn geo3_relation(f: Builtin, t1: &str, t2: &str, gap: Option<Rc<str>>) -> Option<bool> {
466
+ let rel = match f {
467
+ Builtin::Geo3Contains => crate::geo3::Rel3::Contains,
468
+ Builtin::Geo3Within => crate::geo3::Rel3::Within,
469
+ Builtin::Geo3Adjacent => crate::geo3::Rel3::Adjacent,
470
+ _ => return None,
471
+ };
472
+ let g = gap.and_then(|t| as_number(&t)).unwrap_or(0.0);
473
+ Some(crate::geo3::relate(rel, &geo3_arg(t1)?, &geo3_arg(t2)?, g))
474
+ }
475
+
436
476
  /// A units IRI term (`<…/metre>` / `<…/degree>`) → [`crate::geo::Unit`].
437
477
  fn geo_unit(token: &str) -> Option<crate::geo::Unit> {
438
478
  match iri_content(token)? {
@@ -834,6 +874,13 @@ fn func_bool(f: Builtin, args: &[FExpr], ctx: &Ctx, b: &Row) -> bool {
834
874
  (Some(a), Some(c)) => geo_relation(f, &a, &c).unwrap_or(false),
835
875
  _ => false,
836
876
  },
877
+ // geo3 3D relations in FILTER context (a type error drops the row → false).
878
+ Builtin::Geo3Contains | Builtin::Geo3Within | Builtin::Geo3Adjacent => {
879
+ match (val(0), val(1)) {
880
+ (Some(a), Some(c)) => geo3_relation(f, &a, &c, val(2)).unwrap_or(false),
881
+ _ => false,
882
+ }
883
+ }
837
884
  _ => false,
838
885
  }
839
886
  }
@@ -1216,6 +1263,27 @@ mod tests {
1216
1263
  call(&ctx, Builtin::GeoDistance, &[&point, &far, "<bad-unit>"]),
1217
1264
  None
1218
1265
  );
1266
+
1267
+ // geo3 (3D extension): AABB topology + distance over POINT Z / BOX3D.
1268
+ let wkt3 = |s: &str| lit(s, "https://w3id.org/rete/geo3#wktLiteral3D");
1269
+ let box3d = |s: &str| lit(s, "https://w3id.org/rete/geo3#box3dLiteral");
1270
+ let big = box3d("BOX3D(0 0 0, 10 10 10)");
1271
+ let inner = box3d("BOX3D(2 2 2, 4 4 4)");
1272
+ let p000 = wkt3("POINT Z(0 0 0)");
1273
+ let p_far = wkt3("POINT Z(3 4 12)"); // distance 13 from origin
1274
+ assert!(boolean(Builtin::Geo3Contains, &[&big, &inner]));
1275
+ assert!(!boolean(Builtin::Geo3Contains, &[&inner, &big]));
1276
+ assert!(boolean(Builtin::Geo3Within, &[&inner, &big]));
1277
+ // adjacency: two boxes 3 apart on Z — not touching, adjacent within gap 3
1278
+ let a = box3d("BOX3D(0 0 0, 1 1 1)");
1279
+ let b = box3d("BOX3D(0 0 3, 1 1 4)");
1280
+ assert!(!boolean(Builtin::Geo3Adjacent, &[&a, &b]));
1281
+ assert!(boolean(Builtin::Geo3Adjacent, &[&a, &b, "3"]));
1282
+ assert!(!boolean(Builtin::Geo3Adjacent, &["<iri>", &b]));
1283
+ // distance3D → xsd:double "13"
1284
+ let d = call(&ctx, Builtin::Geo3Distance, &[&p000, &p_far]).unwrap();
1285
+ assert!(d.contains("13"), "distance3D result: {d}");
1286
+ assert_eq!(call(&ctx, Builtin::Geo3Distance, &[&p000, "<iri>"]), None);
1219
1287
  }
1220
1288
 
1221
1289
  #[test]
@@ -754,6 +754,11 @@ fn convert_expr(e: &Expression) -> Result<FExpr, SparqlError> {
754
754
  "http://www.opengis.net/def/function/geosparql/envelope" => {
755
755
  Builtin::GeoEnvelope
756
756
  }
757
+ // geo3: 3D extension of GeoSPARQL (see crate::geo3).
758
+ "https://w3id.org/rete/geo3/function/distance3D" => Builtin::Geo3Distance,
759
+ "https://w3id.org/rete/geo3/function/contains3D" => Builtin::Geo3Contains,
760
+ "https://w3id.org/rete/geo3/function/within3D" => Builtin::Geo3Within,
761
+ "https://w3id.org/rete/geo3/function/adjacent3D" => Builtin::Geo3Adjacent,
757
762
  _ => return Err(SparqlError::Unsupported("built-in function")),
758
763
  },
759
764
  _ => return Err(SparqlError::Unsupported("built-in function")),
@@ -1193,6 +1198,11 @@ mod tests {
1193
1198
  "<http://www.opengis.net/def/function/geosparql/sfEquals>(?a, ?b)",
1194
1199
  "<http://www.opengis.net/def/function/geosparql/distance>(?a, ?b, <http://www.opengis.net/def/uom/OGC/1.0/metre>)",
1195
1200
  "<http://www.opengis.net/def/function/geosparql/envelope>(?a)",
1201
+ "<https://w3id.org/rete/geo3/function/distance3D>(?a, ?b)",
1202
+ "<https://w3id.org/rete/geo3/function/contains3D>(?a, ?b)",
1203
+ "<https://w3id.org/rete/geo3/function/within3D>(?a, ?b)",
1204
+ "<https://w3id.org/rete/geo3/function/adjacent3D>(?a, ?b)",
1205
+ "<https://w3id.org/rete/geo3/function/adjacent3D>(?a, ?b, 5)",
1196
1206
  ] {
1197
1207
  let q = format!("SELECT ({expression} AS ?result) WHERE {{ VALUES ?v {{ 1 }} }}");
1198
1208
  assert!(parse_select(&q).is_ok(), "{expression}");
@@ -306,6 +306,11 @@ pub enum Builtin {
306
306
  GeoSfEquals,
307
307
  GeoDistance,
308
308
  GeoEnvelope,
309
+ // geo3 — 3D extension of GeoSPARQL (AABB topology + distance). See crate::geo3.
310
+ Geo3Distance,
311
+ Geo3Contains,
312
+ Geo3Within,
313
+ Geo3Adjacent,
309
314
  // RDF-star / SPARQL-star: construct + inspect quoted triples (`<<s p o>>`).
310
315
  /// `TRIPLE(s, p, o)` → the quoted triple term.
311
316
  TripleTerm,
@@ -0,0 +1,7 @@
1
+ # Seeds for failure cases proptest has generated in the past. It is
2
+ # automatically read and these particular cases re-run before any
3
+ # novel cases are generated.
4
+ #
5
+ # It is recommended to check this file in to source control so that
6
+ # everyone who runs the test benefits from these saved cases.
7
+ cc 10def222df8ad092668efb9e15e965901a8c2bbac1992c1839bc373ebb7234c2 # shrinks to specs = []
@@ -43,6 +43,10 @@ impl RecordingReader {
43
43
  fn fail_from_now(&self) {
44
44
  self.fail.store(true, Ordering::Relaxed);
45
45
  }
46
+ /// End the simulated outage — reads succeed again.
47
+ fn recover(&self) {
48
+ self.fail.store(false, Ordering::Relaxed);
49
+ }
46
50
  }
47
51
 
48
52
  impl RangeReader for RecordingReader {
@@ -572,6 +576,52 @@ fn lazy_sparql_open_surfaces_failed_tile_fetches() {
572
576
  );
573
577
  }
574
578
 
579
+ /// The incompleteness verdict is PER QUERY for a resident session: after the
580
+ /// network recovers, `reset_load_failures` clears the sticky flag and the same
581
+ /// query RE-FETCHES what failed (failed tiles/chunks are never cached as
582
+ /// empty), returning the complete answer — one transient blip must not poison
583
+ /// every later query on a long-lived handle (the browser worker's session).
584
+ #[test]
585
+ fn reset_load_failures_makes_failed_fetches_retryable() {
586
+ let image = multi_tile_image();
587
+ let expected = {
588
+ // The complete answer, computed over a healthy session.
589
+ let healthy = Rete::open_ranged_lazy(std::sync::Arc::new(RecordingReader::new(
590
+ image.clone(),
591
+ )))
592
+ .unwrap();
593
+ let q = format!("SELECT ?o WHERE {{ {} <http://ex/knows> ?o }}", mt_node(7));
594
+ match eval_query(&healthy, &q).unwrap() {
595
+ QueryOutput::Select(_, rows) => rows.len(),
596
+ _ => unreachable!(),
597
+ }
598
+ };
599
+ assert!(expected > 0, "fixture must produce rows");
600
+
601
+ let reader = std::sync::Arc::new(RecordingReader::new(image));
602
+ let rete = Rete::open_ranged_lazy(reader.clone()).unwrap();
603
+ reader.fail_from_now();
604
+ let q = format!("SELECT ?o WHERE {{ {} <http://ex/knows> ?o }}", mt_node(7));
605
+ let _ = eval_query(&rete, &q).unwrap();
606
+ assert!(rete.index_incomplete(), "outage query must be flagged");
607
+
608
+ reader.recover();
609
+ rete.reset_load_failures();
610
+ assert!(!rete.index_incomplete(), "reset must clear the verdict");
611
+ let rows = match eval_query(&rete, &q).unwrap() {
612
+ QueryOutput::Select(_, rows) => rows.len(),
613
+ _ => unreachable!(),
614
+ };
615
+ assert!(
616
+ !rete.index_incomplete(),
617
+ "recovered query must not re-flag — its fetches succeeded"
618
+ );
619
+ assert_eq!(
620
+ rows, expected,
621
+ "the retried query must return the COMPLETE answer, not a poisoned empty tile"
622
+ );
623
+ }
624
+
575
625
  #[test]
576
626
  fn routed_pattern_query_with_unknown_term_skips_the_index() {
577
627
  let image = image_with_pyramid();
@@ -4,7 +4,7 @@ build-backend = "maturin"
4
4
 
5
5
  [project]
6
6
  name = "rete-graph"
7
- version = "0.2.2"
7
+ version = "0.2.3"
8
8
  description = "Query local and remote .rete graph files with SPARQL — Python bindings for the rete engine."
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.9"
File without changes
File without changes