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.
- {rete_graph-0.2.2 → rete_graph-0.2.3}/PKG-INFO +1 -1
- {rete_graph-0.2.2 → rete_graph-0.2.3}/clients/python/Cargo.lock +1 -1
- {rete_graph-0.2.2 → rete_graph-0.2.3}/clients/python/Cargo.toml +1 -1
- {rete_graph-0.2.2 → rete_graph-0.2.3}/clients/python/src/lib.rs +11 -0
- {rete_graph-0.2.2 → rete_graph-0.2.3}/crates/rete-core/src/dict.rs +35 -9
- {rete_graph-0.2.2 → rete_graph-0.2.3}/crates/rete-core/src/dictionary.rs +9 -1
- {rete_graph-0.2.2 → rete_graph-0.2.3}/crates/rete-core/src/extbuild.rs +7 -1
- {rete_graph-0.2.2 → rete_graph-0.2.3}/crates/rete-core/src/file.rs +15 -0
- rete_graph-0.2.3/crates/rete-core/src/geo3.rs +207 -0
- {rete_graph-0.2.2 → rete_graph-0.2.3}/crates/rete-core/src/index.rs +30 -10
- {rete_graph-0.2.2 → rete_graph-0.2.3}/crates/rete-core/src/lib.rs +1 -0
- {rete_graph-0.2.2 → rete_graph-0.2.3}/crates/rete-core/src/sparql/expr.rs +68 -0
- {rete_graph-0.2.2 → rete_graph-0.2.3}/crates/rete-core/src/sparql/lower.rs +10 -0
- {rete_graph-0.2.2 → rete_graph-0.2.3}/crates/rete-core/src/sparql.rs +5 -0
- rete_graph-0.2.3/crates/rete-core/tests/properties.proptest-regressions +7 -0
- {rete_graph-0.2.2 → rete_graph-0.2.3}/crates/rete-core/tests/ranged.rs +50 -0
- {rete_graph-0.2.2 → rete_graph-0.2.3}/pyproject.toml +1 -1
- {rete_graph-0.2.2 → rete_graph-0.2.3}/Cargo.toml +0 -0
- {rete_graph-0.2.2 → rete_graph-0.2.3}/README.md +0 -0
- {rete_graph-0.2.2 → rete_graph-0.2.3}/clients/python/.gitignore +0 -0
- {rete_graph-0.2.2 → rete_graph-0.2.3}/clients/python/README.md +0 -0
- {rete_graph-0.2.2 → rete_graph-0.2.3}/clients/python/examples/jupyterlite-build-tutorial.ipynb +0 -0
- {rete_graph-0.2.2 → rete_graph-0.2.3}/clients/python/examples/jupyterlite-demo.ipynb +0 -0
- {rete_graph-0.2.2 → rete_graph-0.2.3}/clients/python/examples/tutorial.ipynb +0 -0
- {rete_graph-0.2.2 → rete_graph-0.2.3}/clients/python/src/readers.rs +0 -0
- {rete_graph-0.2.2 → rete_graph-0.2.3}/clients/python/tests/conftest.py +0 -0
- {rete_graph-0.2.2 → rete_graph-0.2.3}/clients/python/tests/test_rete_graph.py +0 -0
- {rete_graph-0.2.2 → rete_graph-0.2.3}/crates/rete-core/Cargo.toml +0 -0
- {rete_graph-0.2.2 → rete_graph-0.2.3}/crates/rete-core/LICENSE +0 -0
- {rete_graph-0.2.2 → rete_graph-0.2.3}/crates/rete-core/README.md +0 -0
- {rete_graph-0.2.2 → rete_graph-0.2.3}/crates/rete-core/examples/coherence_bench.rs +0 -0
- {rete_graph-0.2.2 → rete_graph-0.2.3}/crates/rete-core/examples/parallel_bench.rs +0 -0
- {rete_graph-0.2.2 → rete_graph-0.2.3}/crates/rete-core/examples/qbench.rs +0 -0
- {rete_graph-0.2.2 → rete_graph-0.2.3}/crates/rete-core/src/bgp.rs +0 -0
- {rete_graph-0.2.2 → rete_graph-0.2.3}/crates/rete-core/src/block_cache.rs +0 -0
- {rete_graph-0.2.2 → rete_graph-0.2.3}/crates/rete-core/src/geo.rs +0 -0
- {rete_graph-0.2.2 → rete_graph-0.2.3}/crates/rete-core/src/header.rs +0 -0
- {rete_graph-0.2.2 → rete_graph-0.2.3}/crates/rete-core/src/ingest.rs +0 -0
- {rete_graph-0.2.2 → rete_graph-0.2.3}/crates/rete-core/src/meta.rs +0 -0
- {rete_graph-0.2.2 → rete_graph-0.2.3}/crates/rete-core/src/parallel.rs +0 -0
- {rete_graph-0.2.2 → rete_graph-0.2.3}/crates/rete-core/src/pyramid.rs +0 -0
- {rete_graph-0.2.2 → rete_graph-0.2.3}/crates/rete-core/src/reach.rs +0 -0
- {rete_graph-0.2.2 → rete_graph-0.2.3}/crates/rete-core/src/reader.rs +0 -0
- {rete_graph-0.2.2 → rete_graph-0.2.3}/crates/rete-core/src/reason.rs +0 -0
- {rete_graph-0.2.2 → rete_graph-0.2.3}/crates/rete-core/src/results.rs +0 -0
- {rete_graph-0.2.2 → rete_graph-0.2.3}/crates/rete-core/src/row.rs +0 -0
- {rete_graph-0.2.2 → rete_graph-0.2.3}/crates/rete-core/src/schema_pyramid.rs +0 -0
- {rete_graph-0.2.2 → rete_graph-0.2.3}/crates/rete-core/src/service.rs +0 -0
- {rete_graph-0.2.2 → rete_graph-0.2.3}/crates/rete-core/src/shacl.rs +0 -0
- {rete_graph-0.2.2 → rete_graph-0.2.3}/crates/rete-core/src/sparql/aggregate.rs +0 -0
- {rete_graph-0.2.2 → rete_graph-0.2.3}/crates/rete-core/src/sparql/eval.rs +0 -0
- {rete_graph-0.2.2 → rete_graph-0.2.3}/crates/rete-core/src/sparql/path.rs +0 -0
- {rete_graph-0.2.2 → rete_graph-0.2.3}/crates/rete-core/src/sparql/ql.rs +0 -0
- {rete_graph-0.2.2 → rete_graph-0.2.3}/crates/rete-core/src/terms.rs +0 -0
- {rete_graph-0.2.2 → rete_graph-0.2.3}/crates/rete-core/src/text_index.rs +0 -0
- {rete_graph-0.2.2 → rete_graph-0.2.3}/crates/rete-core/src/tiling.rs +0 -0
- {rete_graph-0.2.2 → rete_graph-0.2.3}/crates/rete-core/src/triples.rs +0 -0
- {rete_graph-0.2.2 → rete_graph-0.2.3}/crates/rete-core/src/varint.rs +0 -0
- {rete_graph-0.2.2 → rete_graph-0.2.3}/crates/rete-core/tests/community_eval.rs +0 -0
- {rete_graph-0.2.2 → rete_graph-0.2.3}/crates/rete-core/tests/compatibility_v1.rs +0 -0
- {rete_graph-0.2.2 → rete_graph-0.2.3}/crates/rete-core/tests/fixtures/v1/minimal.rete +0 -0
- {rete_graph-0.2.2 → rete_graph-0.2.3}/crates/rete-core/tests/fixtures/v1/source.nq +0 -0
- {rete_graph-0.2.2 → rete_graph-0.2.3}/crates/rete-core/tests/properties.rs +0 -0
- {rete_graph-0.2.2 → rete_graph-0.2.3}/crates/rete-core/tests/public_api.rs +0 -0
- {rete_graph-0.2.2 → rete_graph-0.2.3}/crates/rete-core/tests/robustness.rs +0 -0
- {rete_graph-0.2.2 → rete_graph-0.2.3}/crates/rete-core/tests/roundtrip.rs +0 -0
- {rete_graph-0.2.2 → rete_graph-0.2.3}/crates/rete-core/tests/service_federation.rs +0 -0
- {rete_graph-0.2.2 → rete_graph-0.2.3}/crates/rete-core/tests/shacl_core.rs +0 -0
- {rete_graph-0.2.2 → rete_graph-0.2.3}/crates/rete-core/tests/sparql_integration.rs +0 -0
- {rete_graph-0.2.2 → rete_graph-0.2.3}/crates/rete-core/tests/sparql_nondeterministic.rs +0 -0
- {rete_graph-0.2.2 → rete_graph-0.2.3}/python/rete_graph/__init__.py +0 -0
|
@@ -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.
|
|
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
|
|
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
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
606
|
-
|
|
607
|
-
|
|
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
|
-
|
|
611
|
-
}
|
|
612
|
-
|
|
613
|
-
|
|
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
|
|
@@ -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();
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{rete_graph-0.2.2 → rete_graph-0.2.3}/clients/python/examples/jupyterlite-build-tutorial.ipynb
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|