rete-graph 0.3.0__tar.gz → 0.3.2__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.3.0 → rete_graph-0.3.2}/Cargo.toml +1 -1
- {rete_graph-0.3.0 → rete_graph-0.3.2}/PKG-INFO +31 -1
- {rete_graph-0.3.0/clients/python → rete_graph-0.3.2}/README.md +30 -0
- {rete_graph-0.3.0 → rete_graph-0.3.2}/clients/python/Cargo.lock +2 -2
- {rete_graph-0.3.0 → rete_graph-0.3.2}/clients/python/Cargo.toml +1 -1
- {rete_graph-0.3.0 → rete_graph-0.3.2/clients/python}/README.md +30 -0
- {rete_graph-0.3.0 → rete_graph-0.3.2}/clients/python/src/lib.rs +168 -7
- {rete_graph-0.3.0 → rete_graph-0.3.2}/clients/python/tests/conftest.py +56 -0
- rete_graph-0.3.2/clients/python/tests/test_rete_graph.py +575 -0
- {rete_graph-0.3.0 → rete_graph-0.3.2}/crates/rete-core/src/dict.rs +2 -7
- {rete_graph-0.3.0 → rete_graph-0.3.2}/crates/rete-core/src/extbuild.rs +0 -1
- {rete_graph-0.3.0 → rete_graph-0.3.2}/crates/rete-core/src/file.rs +26 -32
- {rete_graph-0.3.0 → rete_graph-0.3.2}/crates/rete-core/src/index.rs +10 -6
- {rete_graph-0.3.0 → rete_graph-0.3.2}/crates/rete-core/src/sparql/aggregate.rs +67 -13
- {rete_graph-0.3.0 → rete_graph-0.3.2}/crates/rete-core/src/sparql/lower.rs +67 -0
- {rete_graph-0.3.0 → rete_graph-0.3.2}/pyproject.toml +1 -1
- {rete_graph-0.3.0 → rete_graph-0.3.2}/python/rete_graph/__init__.py +161 -2
- rete_graph-0.3.0/clients/python/tests/test_rete_graph.py +0 -308
- {rete_graph-0.3.0 → rete_graph-0.3.2}/clients/python/.gitignore +0 -0
- {rete_graph-0.3.0 → rete_graph-0.3.2}/clients/python/examples/agents/README.md +0 -0
- {rete_graph-0.3.0 → rete_graph-0.3.2}/clients/python/examples/agents/langchain_agent.py +0 -0
- {rete_graph-0.3.0 → rete_graph-0.3.2}/clients/python/examples/agents/pydantic_ai_agent.py +0 -0
- {rete_graph-0.3.0 → rete_graph-0.3.2}/clients/python/examples/agents/rete_tools.py +0 -0
- {rete_graph-0.3.0 → rete_graph-0.3.2}/clients/python/examples/jupyterlite-build-tutorial.ipynb +0 -0
- {rete_graph-0.3.0 → rete_graph-0.3.2}/clients/python/examples/jupyterlite-demo.ipynb +0 -0
- {rete_graph-0.3.0 → rete_graph-0.3.2}/clients/python/examples/tutorial.ipynb +0 -0
- {rete_graph-0.3.0 → rete_graph-0.3.2}/clients/python/src/readers.rs +0 -0
- {rete_graph-0.3.0 → rete_graph-0.3.2}/crates/rete-core/Cargo.toml +0 -0
- {rete_graph-0.3.0 → rete_graph-0.3.2}/crates/rete-core/LICENSE +0 -0
- {rete_graph-0.3.0 → rete_graph-0.3.2}/crates/rete-core/README.md +0 -0
- {rete_graph-0.3.0 → rete_graph-0.3.2}/crates/rete-core/examples/coherence_bench.rs +0 -0
- {rete_graph-0.3.0 → rete_graph-0.3.2}/crates/rete-core/examples/parallel_bench.rs +0 -0
- {rete_graph-0.3.0 → rete_graph-0.3.2}/crates/rete-core/examples/qbench.rs +0 -0
- {rete_graph-0.3.0 → rete_graph-0.3.2}/crates/rete-core/src/bgp.rs +0 -0
- {rete_graph-0.3.0 → rete_graph-0.3.2}/crates/rete-core/src/block_cache.rs +0 -0
- {rete_graph-0.3.0 → rete_graph-0.3.2}/crates/rete-core/src/dictionary.rs +0 -0
- {rete_graph-0.3.0 → rete_graph-0.3.2}/crates/rete-core/src/geo.rs +0 -0
- {rete_graph-0.3.0 → rete_graph-0.3.2}/crates/rete-core/src/geo3.rs +0 -0
- {rete_graph-0.3.0 → rete_graph-0.3.2}/crates/rete-core/src/header.rs +0 -0
- {rete_graph-0.3.0 → rete_graph-0.3.2}/crates/rete-core/src/ingest.rs +0 -0
- {rete_graph-0.3.0 → rete_graph-0.3.2}/crates/rete-core/src/lib.rs +0 -0
- {rete_graph-0.3.0 → rete_graph-0.3.2}/crates/rete-core/src/meta.rs +0 -0
- {rete_graph-0.3.0 → rete_graph-0.3.2}/crates/rete-core/src/parallel.rs +0 -0
- {rete_graph-0.3.0 → rete_graph-0.3.2}/crates/rete-core/src/pyramid.rs +0 -0
- {rete_graph-0.3.0 → rete_graph-0.3.2}/crates/rete-core/src/reach.rs +0 -0
- {rete_graph-0.3.0 → rete_graph-0.3.2}/crates/rete-core/src/reader.rs +0 -0
- {rete_graph-0.3.0 → rete_graph-0.3.2}/crates/rete-core/src/reason.rs +0 -0
- {rete_graph-0.3.0 → rete_graph-0.3.2}/crates/rete-core/src/results.rs +0 -0
- {rete_graph-0.3.0 → rete_graph-0.3.2}/crates/rete-core/src/row.rs +0 -0
- {rete_graph-0.3.0 → rete_graph-0.3.2}/crates/rete-core/src/schema_pyramid.rs +0 -0
- {rete_graph-0.3.0 → rete_graph-0.3.2}/crates/rete-core/src/service.rs +0 -0
- {rete_graph-0.3.0 → rete_graph-0.3.2}/crates/rete-core/src/shacl.rs +0 -0
- {rete_graph-0.3.0 → rete_graph-0.3.2}/crates/rete-core/src/sparql/eval.rs +0 -0
- {rete_graph-0.3.0 → rete_graph-0.3.2}/crates/rete-core/src/sparql/expr.rs +0 -0
- {rete_graph-0.3.0 → rete_graph-0.3.2}/crates/rete-core/src/sparql/path.rs +0 -0
- {rete_graph-0.3.0 → rete_graph-0.3.2}/crates/rete-core/src/sparql/ql.rs +0 -0
- {rete_graph-0.3.0 → rete_graph-0.3.2}/crates/rete-core/src/sparql.rs +0 -0
- {rete_graph-0.3.0 → rete_graph-0.3.2}/crates/rete-core/src/terms.rs +0 -0
- {rete_graph-0.3.0 → rete_graph-0.3.2}/crates/rete-core/src/text_index.rs +0 -0
- {rete_graph-0.3.0 → rete_graph-0.3.2}/crates/rete-core/src/tiling.rs +0 -0
- {rete_graph-0.3.0 → rete_graph-0.3.2}/crates/rete-core/src/triples.rs +0 -0
- {rete_graph-0.3.0 → rete_graph-0.3.2}/crates/rete-core/src/varint.rs +0 -0
- {rete_graph-0.3.0 → rete_graph-0.3.2}/crates/rete-core/tests/community_eval.rs +0 -0
- {rete_graph-0.3.0 → rete_graph-0.3.2}/crates/rete-core/tests/compatibility_v1.rs +0 -0
- {rete_graph-0.3.0 → rete_graph-0.3.2}/crates/rete-core/tests/fixtures/v1/minimal.rete +0 -0
- {rete_graph-0.3.0 → rete_graph-0.3.2}/crates/rete-core/tests/fixtures/v1/source.nq +0 -0
- {rete_graph-0.3.0 → rete_graph-0.3.2}/crates/rete-core/tests/properties.proptest-regressions +0 -0
- {rete_graph-0.3.0 → rete_graph-0.3.2}/crates/rete-core/tests/properties.rs +0 -0
- {rete_graph-0.3.0 → rete_graph-0.3.2}/crates/rete-core/tests/public_api.rs +0 -0
- {rete_graph-0.3.0 → rete_graph-0.3.2}/crates/rete-core/tests/ranged.rs +0 -0
- {rete_graph-0.3.0 → rete_graph-0.3.2}/crates/rete-core/tests/robustness.rs +0 -0
- {rete_graph-0.3.0 → rete_graph-0.3.2}/crates/rete-core/tests/roundtrip.rs +0 -0
- {rete_graph-0.3.0 → rete_graph-0.3.2}/crates/rete-core/tests/service_federation.rs +0 -0
- {rete_graph-0.3.0 → rete_graph-0.3.2}/crates/rete-core/tests/shacl_core.rs +0 -0
- {rete_graph-0.3.0 → rete_graph-0.3.2}/crates/rete-core/tests/sparql_integration.rs +0 -0
- {rete_graph-0.3.0 → rete_graph-0.3.2}/crates/rete-core/tests/sparql_nondeterministic.rs +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: rete-graph
|
|
3
|
-
Version: 0.3.
|
|
3
|
+
Version: 0.3.2
|
|
4
4
|
Classifier: Development Status :: 4 - Beta
|
|
5
5
|
Classifier: Intended Audience :: Developers
|
|
6
6
|
Classifier: Intended Audience :: Science/Research
|
|
@@ -98,6 +98,36 @@ g.info(), g.quads, g.content_hash()
|
|
|
98
98
|
SPARQL 1.1 `SERVICE` federation works out of the box — join a `.rete` file
|
|
99
99
|
against any public SPARQL endpoint in one query.
|
|
100
100
|
|
|
101
|
+
### Stream every quad out
|
|
102
|
+
|
|
103
|
+
`iter_quads()` is a generator over the whole dataset — default graph first,
|
|
104
|
+
then each named graph — walked in batches, so **peak memory is bounded by one
|
|
105
|
+
batch no matter how big the file is**:
|
|
106
|
+
|
|
107
|
+
```python
|
|
108
|
+
for s, p, o, graph in g.iter_quads(): # N-Triples term tokens; graph is
|
|
109
|
+
... # None for the default graph
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
`to_nquads()` serializes the same walk to a path or any open stream, which is
|
|
113
|
+
what makes handing a `.rete` to another RDF toolchain a three-liner:
|
|
114
|
+
|
|
115
|
+
```python
|
|
116
|
+
g.to_nquads("out.nq") # or a StringIO / gzip.open(...) / sys.stdout
|
|
117
|
+
store = pyoxigraph.Store()
|
|
118
|
+
store.bulk_load(path="out.nq", format=pyoxigraph.RdfFormat.N_QUADS)
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
Measured on `figshare.rete` (221 MB, 15.9 M quads): `to_nquads` streams the
|
|
122
|
+
whole file in **941 MiB peak RSS in 25 s**, of which only ~20 MiB is the batch
|
|
123
|
+
— the rest is the engine's faulted index/dictionary, and it is flat in the
|
|
124
|
+
quad count. `list(g.iter_quads())` on the same file needs many GB, and
|
|
125
|
+
`rete export` needs > 1.1 GiB. Pass `batch_size=` to trade: a small batch
|
|
126
|
+
makes a *peek* at a remote graph much cheaper (5 quads for 1.1 MB of range
|
|
127
|
+
reads at `batch_size=1`, versus 44 MB at the default 10 000), a large one
|
|
128
|
+
speeds up a full walk. Scope with `graph=`: `rete.DEFAULT_GRAPH` for the
|
|
129
|
+
unnamed default graph alone, or an IRI for one named graph.
|
|
130
|
+
|
|
101
131
|
### Prepare a `.rete` step by step
|
|
102
132
|
|
|
103
133
|
The lazy `Builder` configures a build — sources, embedded Dataset Card,
|
|
@@ -70,6 +70,36 @@ g.info(), g.quads, g.content_hash()
|
|
|
70
70
|
SPARQL 1.1 `SERVICE` federation works out of the box — join a `.rete` file
|
|
71
71
|
against any public SPARQL endpoint in one query.
|
|
72
72
|
|
|
73
|
+
### Stream every quad out
|
|
74
|
+
|
|
75
|
+
`iter_quads()` is a generator over the whole dataset — default graph first,
|
|
76
|
+
then each named graph — walked in batches, so **peak memory is bounded by one
|
|
77
|
+
batch no matter how big the file is**:
|
|
78
|
+
|
|
79
|
+
```python
|
|
80
|
+
for s, p, o, graph in g.iter_quads(): # N-Triples term tokens; graph is
|
|
81
|
+
... # None for the default graph
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
`to_nquads()` serializes the same walk to a path or any open stream, which is
|
|
85
|
+
what makes handing a `.rete` to another RDF toolchain a three-liner:
|
|
86
|
+
|
|
87
|
+
```python
|
|
88
|
+
g.to_nquads("out.nq") # or a StringIO / gzip.open(...) / sys.stdout
|
|
89
|
+
store = pyoxigraph.Store()
|
|
90
|
+
store.bulk_load(path="out.nq", format=pyoxigraph.RdfFormat.N_QUADS)
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
Measured on `figshare.rete` (221 MB, 15.9 M quads): `to_nquads` streams the
|
|
94
|
+
whole file in **941 MiB peak RSS in 25 s**, of which only ~20 MiB is the batch
|
|
95
|
+
— the rest is the engine's faulted index/dictionary, and it is flat in the
|
|
96
|
+
quad count. `list(g.iter_quads())` on the same file needs many GB, and
|
|
97
|
+
`rete export` needs > 1.1 GiB. Pass `batch_size=` to trade: a small batch
|
|
98
|
+
makes a *peek* at a remote graph much cheaper (5 quads for 1.1 MB of range
|
|
99
|
+
reads at `batch_size=1`, versus 44 MB at the default 10 000), a large one
|
|
100
|
+
speeds up a full walk. Scope with `graph=`: `rete.DEFAULT_GRAPH` for the
|
|
101
|
+
unnamed default graph alone, or an IRI for one named graph.
|
|
102
|
+
|
|
73
103
|
### Prepare a `.rete` step by step
|
|
74
104
|
|
|
75
105
|
The lazy `Builder` configures a build — sources, embedded Dataset Card,
|
|
@@ -602,7 +602,7 @@ checksum = "cab834c73d247e67f4fae452806d17d3c7501756d98c8808d7c9c7aa7d18f973"
|
|
|
602
602
|
|
|
603
603
|
[[package]]
|
|
604
604
|
name = "rete-core"
|
|
605
|
-
version = "0.3.
|
|
605
|
+
version = "0.3.2"
|
|
606
606
|
dependencies = [
|
|
607
607
|
"blake3",
|
|
608
608
|
"getrandom 0.2.17",
|
|
@@ -622,7 +622,7 @@ dependencies = [
|
|
|
622
622
|
|
|
623
623
|
[[package]]
|
|
624
624
|
name = "rete-python"
|
|
625
|
-
version = "0.3.
|
|
625
|
+
version = "0.3.2"
|
|
626
626
|
dependencies = [
|
|
627
627
|
"pyo3",
|
|
628
628
|
"rete-core",
|
|
@@ -6,7 +6,7 @@ name = "rete-python"
|
|
|
6
6
|
# Kept in lockstep with pyproject.toml's [project] version, and with the engine's
|
|
7
7
|
# major.minor, by scripts/sync_versions.py (--check runs in CI). __version__ comes
|
|
8
8
|
# from here (CARGO_PKG_VERSION), the wheel metadata from pyproject.
|
|
9
|
-
version = "0.3.
|
|
9
|
+
version = "0.3.2"
|
|
10
10
|
edition = "2021"
|
|
11
11
|
license = "Apache-2.0"
|
|
12
12
|
repository = "https://github.com/caviri/rete"
|
|
@@ -70,6 +70,36 @@ g.info(), g.quads, g.content_hash()
|
|
|
70
70
|
SPARQL 1.1 `SERVICE` federation works out of the box — join a `.rete` file
|
|
71
71
|
against any public SPARQL endpoint in one query.
|
|
72
72
|
|
|
73
|
+
### Stream every quad out
|
|
74
|
+
|
|
75
|
+
`iter_quads()` is a generator over the whole dataset — default graph first,
|
|
76
|
+
then each named graph — walked in batches, so **peak memory is bounded by one
|
|
77
|
+
batch no matter how big the file is**:
|
|
78
|
+
|
|
79
|
+
```python
|
|
80
|
+
for s, p, o, graph in g.iter_quads(): # N-Triples term tokens; graph is
|
|
81
|
+
... # None for the default graph
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
`to_nquads()` serializes the same walk to a path or any open stream, which is
|
|
85
|
+
what makes handing a `.rete` to another RDF toolchain a three-liner:
|
|
86
|
+
|
|
87
|
+
```python
|
|
88
|
+
g.to_nquads("out.nq") # or a StringIO / gzip.open(...) / sys.stdout
|
|
89
|
+
store = pyoxigraph.Store()
|
|
90
|
+
store.bulk_load(path="out.nq", format=pyoxigraph.RdfFormat.N_QUADS)
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
Measured on `figshare.rete` (221 MB, 15.9 M quads): `to_nquads` streams the
|
|
94
|
+
whole file in **941 MiB peak RSS in 25 s**, of which only ~20 MiB is the batch
|
|
95
|
+
— the rest is the engine's faulted index/dictionary, and it is flat in the
|
|
96
|
+
quad count. `list(g.iter_quads())` on the same file needs many GB, and
|
|
97
|
+
`rete export` needs > 1.1 GiB. Pass `batch_size=` to trade: a small batch
|
|
98
|
+
makes a *peek* at a remote graph much cheaper (5 quads for 1.1 MB of range
|
|
99
|
+
reads at `batch_size=1`, versus 44 MB at the default 10 000), a large one
|
|
100
|
+
speeds up a full walk. Scope with `graph=`: `rete.DEFAULT_GRAPH` for the
|
|
101
|
+
unnamed default graph alone, or an IRI for one named graph.
|
|
102
|
+
|
|
73
103
|
### Prepare a `.rete` step by step
|
|
74
104
|
|
|
75
105
|
The lazy `Builder` configures a build — sources, embedded Dataset Card,
|
|
@@ -18,8 +18,8 @@ use pyo3::prelude::*;
|
|
|
18
18
|
use pyo3::types::PyBytes;
|
|
19
19
|
use rete_core::{
|
|
20
20
|
eval_query, eval_query_reasoned, results_envelope_json, schema_classes, schema_summary,
|
|
21
|
-
validate_shacl, BlockCacheReader, CountingReader, DataGraph,
|
|
22
|
-
ShaclShapes, DEFAULT_BLOCK,
|
|
21
|
+
validate_shacl, BlockCacheReader, CountingReader, DataGraph, IndexPermutation, RangeReader,
|
|
22
|
+
Rete, ReteGraph, ShaclShapes, DEFAULT_BLOCK,
|
|
23
23
|
};
|
|
24
24
|
#[cfg(not(target_os = "emscripten"))]
|
|
25
25
|
use rete_core::{parse_sparql_json_results, Binding, ServiceClient};
|
|
@@ -35,6 +35,18 @@ const JSON_SCHEMA_VERSION: u8 = 1;
|
|
|
35
35
|
#[cfg(not(target_os = "emscripten"))]
|
|
36
36
|
const MAX_SERVICE_RESPONSE: u64 = 256 * 1024 * 1024;
|
|
37
37
|
|
|
38
|
+
/// Quads resolved per [`Graph::dump_batch`] call — the memory bound of the
|
|
39
|
+
/// whole lazy dump path (`Graph.iter_quads` / `Graph.to_nquads` upstream).
|
|
40
|
+
/// 10 000 triples of typical terms is a few MB of Python objects in flight:
|
|
41
|
+
/// small enough to be noise beside the interpreter, large enough that the
|
|
42
|
+
/// per-call cost (one GIL round trip, one coalesced dictionary prefetch)
|
|
43
|
+
/// amortizes to nothing. The caller may override it.
|
|
44
|
+
const DUMP_BATCH: usize = 10_000;
|
|
45
|
+
|
|
46
|
+
/// One [`Graph::dump_batch`] answer: the batch's `(s, p, o)` term tokens, the
|
|
47
|
+
/// cursor to resume from, and whether the graph is exhausted.
|
|
48
|
+
type DumpBatch = (Vec<(String, String, String)>, u32, bool);
|
|
49
|
+
|
|
38
50
|
/// `SERVICE <endpoint> { … }` transport: SPARQL Protocol over blocking HTTP,
|
|
39
51
|
/// the native twin of the CLI's `HttpServiceClient`. Native-only — a SERVICE
|
|
40
52
|
/// block on the Pyodide build reports the engine's no-client error.
|
|
@@ -136,6 +148,130 @@ impl Graph {
|
|
|
136
148
|
fn fresh_verdict(&self) {
|
|
137
149
|
self.rete.reset_load_failures();
|
|
138
150
|
}
|
|
151
|
+
|
|
152
|
+
/// Resolve one bounded slice of a graph's triples — the engine-side half of
|
|
153
|
+
/// the lazy dump. Returns `(triples, next_cursor, done)`.
|
|
154
|
+
///
|
|
155
|
+
/// The cursor is a **subject id**, and that is the whole trick. The engine's
|
|
156
|
+
/// constant-memory walk (`Rete::dump_each`) is *push*-based: it drives the
|
|
157
|
+
/// scan itself and hands each triple to a callback. Python wants a *pull*
|
|
158
|
+
/// API (a generator), and inverting push into pull needs the scan's stack
|
|
159
|
+
/// held between calls — either a thread (impossible on the Pyodide wheel,
|
|
160
|
+
/// which has no threads) or a self-referential `#[pyclass]` holding an
|
|
161
|
+
/// iterator that borrows the `Rete` it is stored next to (`unsafe`, plus an
|
|
162
|
+
/// `Arc` refactor of this struct). Both are a lot of machinery for a dump.
|
|
163
|
+
///
|
|
164
|
+
/// So instead the scan is made *resumable*: SPO tiles are ordered by
|
|
165
|
+
/// subject id, so "all triples of subject `sid`, for `sid` ascending" visits
|
|
166
|
+
/// exactly the same tiles in exactly the same order as the full scan, and
|
|
167
|
+
/// the entire resume state collapses to one `u32` the caller keeps. No
|
|
168
|
+
/// thread, no `unsafe`, no borrow held across calls, works identically on
|
|
169
|
+
/// the Pyodide build — and unlike a `(offset, limit)` API it is O(n) overall
|
|
170
|
+
/// rather than O(n²/limit), because nothing is ever re-scanned.
|
|
171
|
+
///
|
|
172
|
+
/// Memory is bounded by `max_quads` (plus, at most, one subject's fan-out —
|
|
173
|
+
/// a batch is cut at a subject boundary, never inside one). Work per call is
|
|
174
|
+
/// bounded too, by a probe budget: a sparse named graph whose few subjects
|
|
175
|
+
/// are spread over a huge id space returns early with `done = false` instead
|
|
176
|
+
/// of grinding through absent ids, and the caller simply asks again.
|
|
177
|
+
fn dump_batch_inner(&self, graph: Option<&str>, cursor: u32, max_quads: usize) -> DumpBatch {
|
|
178
|
+
let index = match graph {
|
|
179
|
+
None => self.rete.default_index(),
|
|
180
|
+
// An unknown graph IRI is an empty dump, not an error — same as
|
|
181
|
+
// `Rete::dump_each`.
|
|
182
|
+
Some(g) => match self.rete.graph_index(g) {
|
|
183
|
+
Some(i) => i,
|
|
184
|
+
None => return (Vec::new(), cursor, true),
|
|
185
|
+
},
|
|
186
|
+
};
|
|
187
|
+
// Tile leading ranges are known WITHOUT fetching a tile (they live in
|
|
188
|
+
// the section directory), so both the span and the gap jumps below are
|
|
189
|
+
// free on a lazy/remote open.
|
|
190
|
+
let tiles = index.tile_sections()[IndexPermutation::Spo.section_index()];
|
|
191
|
+
let (Some(first), Some(last)) = (
|
|
192
|
+
tiles.first().map(|t| t.leading_range().0),
|
|
193
|
+
tiles.last().map(|t| t.leading_range().1),
|
|
194
|
+
) else {
|
|
195
|
+
return (Vec::new(), cursor, true); // empty graph
|
|
196
|
+
};
|
|
197
|
+
|
|
198
|
+
let mut sid = cursor.max(first);
|
|
199
|
+
let mut probes = max_quads.saturating_mul(4).max(1 << 16);
|
|
200
|
+
let mut ids: Vec<(u32, u32, u32)> = Vec::new();
|
|
201
|
+
let mut exhausted = false;
|
|
202
|
+
while sid <= last && probes > 0 {
|
|
203
|
+
probes -= 1;
|
|
204
|
+
let hits = index.match_pattern((Some(sid), None, None));
|
|
205
|
+
if hits.is_empty() {
|
|
206
|
+
// No triples at this id. If it also falls in a hole BETWEEN
|
|
207
|
+
// tiles, jump straight to the next tile's first id instead of
|
|
208
|
+
// stepping through the gap one absent id at a time.
|
|
209
|
+
match next_tile_start(tiles, sid) {
|
|
210
|
+
None => {
|
|
211
|
+
exhausted = true;
|
|
212
|
+
break;
|
|
213
|
+
}
|
|
214
|
+
Some(next) if next > sid => {
|
|
215
|
+
sid = next;
|
|
216
|
+
continue;
|
|
217
|
+
}
|
|
218
|
+
Some(_) => {}
|
|
219
|
+
}
|
|
220
|
+
} else {
|
|
221
|
+
ids.extend(hits);
|
|
222
|
+
}
|
|
223
|
+
if sid == u32::MAX {
|
|
224
|
+
exhausted = true;
|
|
225
|
+
break;
|
|
226
|
+
}
|
|
227
|
+
sid += 1;
|
|
228
|
+
// Cut on a subject boundary: `max_quads` is the floor of a batch,
|
|
229
|
+
// never a hard truncation, so no subject is ever split across two
|
|
230
|
+
// batches (which would force a re-scan to resume).
|
|
231
|
+
if ids.len() >= max_quads {
|
|
232
|
+
break;
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
let done = exhausted || sid > last;
|
|
236
|
+
|
|
237
|
+
// One coalesced dictionary fault for the whole batch, then resolve.
|
|
238
|
+
// Deliberately NOT `Dictionary::prefetch_all` (what `dump_each` uses):
|
|
239
|
+
// on a lazy open that would pull the *entire* dictionary — gigabytes on
|
|
240
|
+
// a big file — before the first quad, so `islice(g.iter_quads(), 5)`
|
|
241
|
+
// would cost as much as a full dump. Per-batch prefetch keeps a partial
|
|
242
|
+
// walk proportional to what it actually read.
|
|
243
|
+
let dict = self.rete.dictionary();
|
|
244
|
+
if !ids.is_empty() {
|
|
245
|
+
let mut nodes = Vec::with_capacity(ids.len() * 2);
|
|
246
|
+
let mut preds = Vec::with_capacity(ids.len());
|
|
247
|
+
for &(s, p, o) in &ids {
|
|
248
|
+
nodes.push(dict.subject_node(s));
|
|
249
|
+
nodes.push(dict.object_node(o));
|
|
250
|
+
preds.push(p);
|
|
251
|
+
}
|
|
252
|
+
dict.prefetch_terms(&nodes, &preds);
|
|
253
|
+
}
|
|
254
|
+
let triples = ids
|
|
255
|
+
.into_iter()
|
|
256
|
+
.filter_map(|(s, p, o)| {
|
|
257
|
+
Some((
|
|
258
|
+
dict.subject_term(s)?,
|
|
259
|
+
dict.predicate_term(p)?,
|
|
260
|
+
dict.object_term(o)?,
|
|
261
|
+
))
|
|
262
|
+
})
|
|
263
|
+
.collect();
|
|
264
|
+
(triples, sid, done)
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
/// The first subject id at or after `sid` that any SPO tile can hold, or `None`
|
|
269
|
+
/// when `sid` is past the last tile. Tiles are ascending by leading range, so
|
|
270
|
+
/// this is a binary search; it returns `sid` itself when a tile already covers
|
|
271
|
+
/// it (an ordinary "this subject has no triples" step).
|
|
272
|
+
fn next_tile_start(tiles: &[rete_core::index::Tile], sid: u32) -> Option<u32> {
|
|
273
|
+
let i = tiles.partition_point(|t| t.leading_range().1 < sid);
|
|
274
|
+
tiles.get(i).map(|t| t.leading_range().0.max(sid))
|
|
139
275
|
}
|
|
140
276
|
|
|
141
277
|
#[pymethods]
|
|
@@ -155,13 +291,10 @@ impl Graph {
|
|
|
155
291
|
self.fresh_verdict();
|
|
156
292
|
let out = py
|
|
157
293
|
.allow_threads(|| -> Result<String, String> {
|
|
158
|
-
let shapes =
|
|
159
|
-
ShaclShapes::parse_turtle(shapes_turtle).map_err(|e| e.to_string())?;
|
|
294
|
+
let shapes = ShaclShapes::parse_turtle(shapes_turtle).map_err(|e| e.to_string())?;
|
|
160
295
|
let report = match graph {
|
|
161
296
|
None => validate_shacl(&ReteGraph::new(&self.rete), &shapes),
|
|
162
|
-
Some(g) =>
|
|
163
|
-
validate_shacl(&DataGraph::from_rete(&self.rete, Some(g)), &shapes)
|
|
164
|
-
}
|
|
297
|
+
Some(g) => validate_shacl(&DataGraph::from_rete(&self.rete, Some(g)), &shapes),
|
|
165
298
|
};
|
|
166
299
|
Ok(match format {
|
|
167
300
|
"ttl" => report.to_turtle(),
|
|
@@ -196,6 +329,31 @@ impl Graph {
|
|
|
196
329
|
))
|
|
197
330
|
}
|
|
198
331
|
|
|
332
|
+
/// One bounded slice of a lazy triple dump — the primitive the pure-Python
|
|
333
|
+
/// `Graph.iter_quads()` / `Graph.to_nquads()` drive in a loop.
|
|
334
|
+
///
|
|
335
|
+
/// `graph` is `None` for the default graph or a named-graph token
|
|
336
|
+
/// (`<iri>`); `cursor` is 0 on the first call and the value returned by the
|
|
337
|
+
/// previous one afterwards. Returns `(triples, next_cursor, done)` where
|
|
338
|
+
/// `triples` holds at most `max_quads` (+ one subject's fan-out) canonical
|
|
339
|
+
/// N-Triples term tokens. See `Graph::dump_batch_inner` for why the cursor
|
|
340
|
+
/// is a subject id.
|
|
341
|
+
#[pyo3(signature = (graph=None, cursor=0, max_quads=DUMP_BATCH))]
|
|
342
|
+
fn dump_batch(
|
|
343
|
+
&self,
|
|
344
|
+
py: Python<'_>,
|
|
345
|
+
graph: Option<&str>,
|
|
346
|
+
cursor: u32,
|
|
347
|
+
max_quads: usize,
|
|
348
|
+
) -> PyResult<DumpBatch> {
|
|
349
|
+
self.fresh_verdict();
|
|
350
|
+
let out = py.allow_threads(|| self.dump_batch_inner(graph, cursor, max_quads.max(1)));
|
|
351
|
+
// A batch computed over a half-fetched index/dictionary would silently
|
|
352
|
+
// drop quads from the dump; refuse it like every other entry point.
|
|
353
|
+
self.incomplete_guard()?;
|
|
354
|
+
Ok(out)
|
|
355
|
+
}
|
|
356
|
+
|
|
199
357
|
/// Header summary as JSON:
|
|
200
358
|
/// `{"quads": N, "terms": N, "pyramidLevels": N, "namedGraphs": N}`.
|
|
201
359
|
fn info(&self) -> String {
|
|
@@ -484,6 +642,9 @@ fn _rete(m: &Bound<'_, PyModule>) -> PyResult<()> {
|
|
|
484
642
|
m.add_function(wrap_pyfunction!(open_reader, m)?)?;
|
|
485
643
|
m.add_function(wrap_pyfunction!(build, m)?)?;
|
|
486
644
|
m.add_function(wrap_pyfunction!(build_dataset, m)?)?;
|
|
645
|
+
// Single source of truth for the dump batch size, so the pure-Python
|
|
646
|
+
// wrapper's default cannot drift from the binding's.
|
|
647
|
+
m.add("DUMP_BATCH", DUMP_BATCH)?;
|
|
487
648
|
m.add("__version__", env!("CARGO_PKG_VERSION"))?;
|
|
488
649
|
// The binding's version tracks the binding; this reports the engine actually
|
|
489
650
|
// compiled into the wheel, which is what "does my install support X?" means.
|
|
@@ -86,3 +86,59 @@ def rete_bytes(nt_text) -> bytes:
|
|
|
86
86
|
import rete_graph as rete
|
|
87
87
|
|
|
88
88
|
return rete.build(nt_text)
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
NQ = """\
|
|
92
|
+
<http://example.org/alice> <http://www.w3.org/2000/01/rdf-schema#label> "Alice" .
|
|
93
|
+
<http://example.org/bob> <http://www.w3.org/2000/01/rdf-schema#label> "Bob" .
|
|
94
|
+
<http://example.org/alice> <http://example.org/knows> <http://example.org/bob> <http://example.org/g1> .
|
|
95
|
+
<http://example.org/alice> <http://example.org/since> "2019"^^<http://www.w3.org/2001/XMLSchema#gYear> <http://example.org/g1> .
|
|
96
|
+
<http://example.org/bob> <http://example.org/knows> <http://example.org/alice> <http://example.org/g2> .
|
|
97
|
+
"""
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
@pytest.fixture(scope="session")
|
|
101
|
+
def nq_text() -> str:
|
|
102
|
+
"""Two default-graph triples plus two named graphs (2 + 1 quads)."""
|
|
103
|
+
return NQ
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
@pytest.fixture(scope="session")
|
|
107
|
+
def nq_bytes(nq_text) -> bytes:
|
|
108
|
+
import rete_graph as rete
|
|
109
|
+
|
|
110
|
+
return rete.build(nq_text, "nq")
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
#: Triples per subject in the generated fixtures below — enough fan-out that
|
|
114
|
+
#: batch boundaries land inside a subject's group as well as between subjects.
|
|
115
|
+
FANOUT = 5
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def _generated_graph(subjects: int) -> bytes:
|
|
119
|
+
"""A ``subjects * FANOUT``-triple graph with long-ish literals, so that
|
|
120
|
+
holding every quad is visibly different from streaming them."""
|
|
121
|
+
import rete_graph as rete
|
|
122
|
+
|
|
123
|
+
filler = "x" * 60
|
|
124
|
+
lines = [
|
|
125
|
+
f"<http://example.org/s{s}> <http://example.org/p{i}> "
|
|
126
|
+
f'"value {s}-{i} {filler}" .'
|
|
127
|
+
for s in range(subjects)
|
|
128
|
+
for i in range(FANOUT)
|
|
129
|
+
]
|
|
130
|
+
return rete.build("\n".join(lines) + "\n")
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
@pytest.fixture(scope="session")
|
|
134
|
+
def big_rete_bytes() -> bytes:
|
|
135
|
+
"""40 000 quads (~0.6 MB): too many to be interesting one at a time, few
|
|
136
|
+
enough to walk several times in a test."""
|
|
137
|
+
return _generated_graph(8_000)
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
@pytest.fixture(scope="session")
|
|
141
|
+
def multiblock_rete_bytes() -> bytes:
|
|
142
|
+
"""200 000 quads (~3.4 MB): large enough to span many read-cache blocks,
|
|
143
|
+
which is what makes "a ranged dump is not a download" measurable."""
|
|
144
|
+
return _generated_graph(40_000)
|