rete-graph 0.2.1__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.1 → rete_graph-0.2.3}/Cargo.toml +1 -1
- {rete_graph-0.2.1 → rete_graph-0.2.3}/PKG-INFO +1 -1
- {rete_graph-0.2.1 → rete_graph-0.2.3}/clients/python/Cargo.lock +1 -1
- {rete_graph-0.2.1 → rete_graph-0.2.3}/clients/python/Cargo.toml +1 -1
- rete_graph-0.2.3/clients/python/examples/jupyterlite-build-tutorial.ipynb +315 -0
- {rete_graph-0.2.1 → rete_graph-0.2.3}/clients/python/examples/jupyterlite-demo.ipynb +1 -1
- {rete_graph-0.2.1 → rete_graph-0.2.3}/clients/python/src/lib.rs +45 -1
- {rete_graph-0.2.1 → rete_graph-0.2.3}/clients/python/tests/test_rete_graph.py +37 -0
- {rete_graph-0.2.1 → rete_graph-0.2.3}/crates/rete-core/src/bgp.rs +152 -2
- {rete_graph-0.2.1 → rete_graph-0.2.3}/crates/rete-core/src/block_cache.rs +4 -0
- {rete_graph-0.2.1 → rete_graph-0.2.3}/crates/rete-core/src/dict.rs +44 -9
- {rete_graph-0.2.1 → rete_graph-0.2.3}/crates/rete-core/src/dictionary.rs +65 -1
- rete_graph-0.2.3/crates/rete-core/src/extbuild.rs +1686 -0
- {rete_graph-0.2.1 → rete_graph-0.2.3}/crates/rete-core/src/file.rs +33 -2
- rete_graph-0.2.3/crates/rete-core/src/geo3.rs +207 -0
- {rete_graph-0.2.1 → rete_graph-0.2.3}/crates/rete-core/src/index.rs +77 -11
- {rete_graph-0.2.1 → rete_graph-0.2.3}/crates/rete-core/src/lib.rs +3 -0
- {rete_graph-0.2.1 → rete_graph-0.2.3}/crates/rete-core/src/reader.rs +17 -0
- {rete_graph-0.2.1 → rete_graph-0.2.3}/crates/rete-core/src/sparql/eval.rs +275 -17
- {rete_graph-0.2.1 → rete_graph-0.2.3}/crates/rete-core/src/sparql/expr.rs +68 -0
- {rete_graph-0.2.1 → rete_graph-0.2.3}/crates/rete-core/src/sparql/lower.rs +10 -0
- {rete_graph-0.2.1 → rete_graph-0.2.3}/crates/rete-core/src/sparql.rs +5 -0
- {rete_graph-0.2.1 → rete_graph-0.2.3}/crates/rete-core/src/text_index.rs +28 -0
- rete_graph-0.2.3/crates/rete-core/tests/properties.proptest-regressions +7 -0
- {rete_graph-0.2.1 → rete_graph-0.2.3}/crates/rete-core/tests/ranged.rs +50 -0
- {rete_graph-0.2.1 → rete_graph-0.2.3}/crates/rete-core/tests/sparql_integration.rs +140 -0
- {rete_graph-0.2.1 → rete_graph-0.2.3}/pyproject.toml +1 -1
- {rete_graph-0.2.1 → rete_graph-0.2.3}/python/rete_graph/__init__.py +16 -0
- {rete_graph-0.2.1 → rete_graph-0.2.3}/README.md +0 -0
- {rete_graph-0.2.1 → rete_graph-0.2.3}/clients/python/.gitignore +0 -0
- {rete_graph-0.2.1 → rete_graph-0.2.3}/clients/python/README.md +0 -0
- {rete_graph-0.2.1 → rete_graph-0.2.3}/clients/python/examples/tutorial.ipynb +0 -0
- {rete_graph-0.2.1 → rete_graph-0.2.3}/clients/python/src/readers.rs +0 -0
- {rete_graph-0.2.1 → rete_graph-0.2.3}/clients/python/tests/conftest.py +0 -0
- {rete_graph-0.2.1 → rete_graph-0.2.3}/crates/rete-core/Cargo.toml +0 -0
- {rete_graph-0.2.1 → rete_graph-0.2.3}/crates/rete-core/LICENSE +0 -0
- {rete_graph-0.2.1 → rete_graph-0.2.3}/crates/rete-core/README.md +0 -0
- {rete_graph-0.2.1 → rete_graph-0.2.3}/crates/rete-core/examples/coherence_bench.rs +0 -0
- {rete_graph-0.2.1 → rete_graph-0.2.3}/crates/rete-core/examples/parallel_bench.rs +0 -0
- {rete_graph-0.2.1 → rete_graph-0.2.3}/crates/rete-core/examples/qbench.rs +0 -0
- {rete_graph-0.2.1 → rete_graph-0.2.3}/crates/rete-core/src/geo.rs +0 -0
- {rete_graph-0.2.1 → rete_graph-0.2.3}/crates/rete-core/src/header.rs +0 -0
- {rete_graph-0.2.1 → rete_graph-0.2.3}/crates/rete-core/src/ingest.rs +0 -0
- {rete_graph-0.2.1 → rete_graph-0.2.3}/crates/rete-core/src/meta.rs +0 -0
- {rete_graph-0.2.1 → rete_graph-0.2.3}/crates/rete-core/src/parallel.rs +0 -0
- {rete_graph-0.2.1 → rete_graph-0.2.3}/crates/rete-core/src/pyramid.rs +0 -0
- {rete_graph-0.2.1 → rete_graph-0.2.3}/crates/rete-core/src/reach.rs +0 -0
- {rete_graph-0.2.1 → rete_graph-0.2.3}/crates/rete-core/src/reason.rs +0 -0
- {rete_graph-0.2.1 → rete_graph-0.2.3}/crates/rete-core/src/results.rs +0 -0
- {rete_graph-0.2.1 → rete_graph-0.2.3}/crates/rete-core/src/row.rs +0 -0
- {rete_graph-0.2.1 → rete_graph-0.2.3}/crates/rete-core/src/schema_pyramid.rs +0 -0
- {rete_graph-0.2.1 → rete_graph-0.2.3}/crates/rete-core/src/service.rs +0 -0
- {rete_graph-0.2.1 → rete_graph-0.2.3}/crates/rete-core/src/shacl.rs +0 -0
- {rete_graph-0.2.1 → rete_graph-0.2.3}/crates/rete-core/src/sparql/aggregate.rs +0 -0
- {rete_graph-0.2.1 → rete_graph-0.2.3}/crates/rete-core/src/sparql/path.rs +0 -0
- {rete_graph-0.2.1 → rete_graph-0.2.3}/crates/rete-core/src/sparql/ql.rs +0 -0
- {rete_graph-0.2.1 → rete_graph-0.2.3}/crates/rete-core/src/terms.rs +0 -0
- {rete_graph-0.2.1 → rete_graph-0.2.3}/crates/rete-core/src/tiling.rs +0 -0
- {rete_graph-0.2.1 → rete_graph-0.2.3}/crates/rete-core/src/triples.rs +0 -0
- {rete_graph-0.2.1 → rete_graph-0.2.3}/crates/rete-core/src/varint.rs +0 -0
- {rete_graph-0.2.1 → rete_graph-0.2.3}/crates/rete-core/tests/community_eval.rs +0 -0
- {rete_graph-0.2.1 → rete_graph-0.2.3}/crates/rete-core/tests/compatibility_v1.rs +0 -0
- {rete_graph-0.2.1 → rete_graph-0.2.3}/crates/rete-core/tests/fixtures/v1/minimal.rete +0 -0
- {rete_graph-0.2.1 → rete_graph-0.2.3}/crates/rete-core/tests/fixtures/v1/source.nq +0 -0
- {rete_graph-0.2.1 → rete_graph-0.2.3}/crates/rete-core/tests/properties.rs +0 -0
- {rete_graph-0.2.1 → rete_graph-0.2.3}/crates/rete-core/tests/public_api.rs +0 -0
- {rete_graph-0.2.1 → rete_graph-0.2.3}/crates/rete-core/tests/robustness.rs +0 -0
- {rete_graph-0.2.1 → rete_graph-0.2.3}/crates/rete-core/tests/roundtrip.rs +0 -0
- {rete_graph-0.2.1 → rete_graph-0.2.3}/crates/rete-core/tests/service_federation.rs +0 -0
- {rete_graph-0.2.1 → rete_graph-0.2.3}/crates/rete-core/tests/shacl_core.rs +0 -0
- {rete_graph-0.2.1 → rete_graph-0.2.3}/crates/rete-core/tests/sparql_nondeterministic.rs +0 -0
|
@@ -14,7 +14,7 @@ members = [
|
|
|
14
14
|
# release gates (--locked checks, feature matrix, coverage floors) stay
|
|
15
15
|
# untouched by binding-only changes. Future clients (R, Go) follow the same
|
|
16
16
|
# pattern — clients/ consumes crates/, never the other way around.
|
|
17
|
-
exclude = ["fuzz", "clients/python"]
|
|
17
|
+
exclude = ["fuzz", "clients/python", "clients/r/src/rust"]
|
|
18
18
|
|
|
19
19
|
[workspace.package]
|
|
20
20
|
version = "1.0.0-rc.1"
|
|
@@ -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"
|
|
@@ -0,0 +1,315 @@
|
|
|
1
|
+
{
|
|
2
|
+
"cells": [
|
|
3
|
+
{
|
|
4
|
+
"cell_type": "markdown",
|
|
5
|
+
"id": "intro",
|
|
6
|
+
"metadata": {},
|
|
7
|
+
"source": [
|
|
8
|
+
"# Anatomy of a `.rete` — build one from an rdflib graph\n",
|
|
9
|
+
"\n",
|
|
10
|
+
"This notebook builds a **knowledge graph with a small ontology** in\n",
|
|
11
|
+
"[rdflib](https://rdflib.readthedocs.io), turns it into a **`.rete` file**,\n",
|
|
12
|
+
"and then dissects every part of that file: the **dictionary**, the **triple\n",
|
|
13
|
+
"indexes**, the **pyramid**, the **Dataset Card**, and the **embedded example\n",
|
|
14
|
+
"queries**. Everything runs in your browser tab (Pyodide + JupyterLite — no\n",
|
|
15
|
+
"server anywhere).\n",
|
|
16
|
+
"\n",
|
|
17
|
+
"Project: <https://github.com/caviri/rete> · docs:\n",
|
|
18
|
+
"[Python API](https://caviri.github.io/rete/python.html) ·\n",
|
|
19
|
+
"[format spec](https://caviri.github.io/rete/SPEC.html)"
|
|
20
|
+
]
|
|
21
|
+
},
|
|
22
|
+
{
|
|
23
|
+
"cell_type": "code",
|
|
24
|
+
"execution_count": null,
|
|
25
|
+
"id": "install",
|
|
26
|
+
"metadata": {},
|
|
27
|
+
"outputs": [],
|
|
28
|
+
"source": [
|
|
29
|
+
"%pip install rete-graph rdflib pandas\n",
|
|
30
|
+
"\n",
|
|
31
|
+
"import pandas as pd\n",
|
|
32
|
+
"import rdflib\n",
|
|
33
|
+
"import rete_graph as rete\n",
|
|
34
|
+
"print(f\"rete-graph {rete.__version__} · rdflib {rdflib.__version__}\")"
|
|
35
|
+
]
|
|
36
|
+
},
|
|
37
|
+
{
|
|
38
|
+
"cell_type": "markdown",
|
|
39
|
+
"id": "kg-md",
|
|
40
|
+
"metadata": {},
|
|
41
|
+
"source": [
|
|
42
|
+
"## 1. A small KG + ontology, in rdflib\n",
|
|
43
|
+
"\n",
|
|
44
|
+
"Eight bodies of the solar system, and — this is the part that makes the rest\n",
|
|
45
|
+
"of the notebook interesting — a tiny **ontology** on top: `Planet` and\n",
|
|
46
|
+
"`Moon` are both `rdfs:subClassOf` **`CelestialBody`**, and the `orbits`\n",
|
|
47
|
+
"property declares its domain and range. No instance is ever typed as a\n",
|
|
48
|
+
"`CelestialBody` directly; later, the reasoner will *infer* that they all are."
|
|
49
|
+
]
|
|
50
|
+
},
|
|
51
|
+
{
|
|
52
|
+
"cell_type": "code",
|
|
53
|
+
"execution_count": null,
|
|
54
|
+
"id": "kg",
|
|
55
|
+
"metadata": {},
|
|
56
|
+
"outputs": [],
|
|
57
|
+
"source": [
|
|
58
|
+
"from rdflib import RDF, RDFS, XSD, Literal, Namespace\n",
|
|
59
|
+
"\n",
|
|
60
|
+
"SOL = Namespace(\"https://example.org/solar/\")\n",
|
|
61
|
+
"kg = rdflib.Graph()\n",
|
|
62
|
+
"\n",
|
|
63
|
+
"# --- the ontology (TBox) ---\n",
|
|
64
|
+
"for cls in (\"CelestialBody\", \"Planet\", \"Moon\"):\n",
|
|
65
|
+
" kg.add((SOL[cls], RDF.type, RDFS.Class))\n",
|
|
66
|
+
"kg.add((SOL.Planet, RDFS.subClassOf, SOL.CelestialBody))\n",
|
|
67
|
+
"kg.add((SOL.Moon, RDFS.subClassOf, SOL.CelestialBody))\n",
|
|
68
|
+
"kg.add((SOL.orbits, RDFS.domain, SOL.CelestialBody))\n",
|
|
69
|
+
"kg.add((SOL.orbits, RDFS.range, SOL.CelestialBody))\n",
|
|
70
|
+
"\n",
|
|
71
|
+
"# --- the data (ABox) ---\n",
|
|
72
|
+
"bodies = {\n",
|
|
73
|
+
" \"Sun\": (None, None, 695_700),\n",
|
|
74
|
+
" \"Mercury\": (\"Planet\", \"Sun\", 2_440),\n",
|
|
75
|
+
" \"Venus\": (\"Planet\", \"Sun\", 6_052),\n",
|
|
76
|
+
" \"Earth\": (\"Planet\", \"Sun\", 6_371),\n",
|
|
77
|
+
" \"Moon\": (\"Moon\", \"Earth\", 1_737),\n",
|
|
78
|
+
" \"Mars\": (\"Planet\", \"Sun\", 3_390),\n",
|
|
79
|
+
" \"Phobos\": (\"Moon\", \"Mars\", 11),\n",
|
|
80
|
+
" \"Deimos\": (\"Moon\", \"Mars\", 6),\n",
|
|
81
|
+
"}\n",
|
|
82
|
+
"for name, (cls, parent, radius) in bodies.items():\n",
|
|
83
|
+
" node = SOL[name]\n",
|
|
84
|
+
" kg.add((node, RDFS.label, Literal(name, lang=\"en\")))\n",
|
|
85
|
+
" kg.add((node, SOL.radius_km, Literal(radius, datatype=XSD.integer)))\n",
|
|
86
|
+
" if cls:\n",
|
|
87
|
+
" kg.add((node, RDF.type, SOL[cls]))\n",
|
|
88
|
+
" if parent:\n",
|
|
89
|
+
" kg.add((node, SOL.orbits, SOL[parent]))\n",
|
|
90
|
+
"\n",
|
|
91
|
+
"print(f\"{len(kg)} triples in the rdflib graph — a taste, as Turtle:\\n\")\n",
|
|
92
|
+
"print(\"\\n\".join(kg.serialize(format=\"turtle\").splitlines()[8:16]))"
|
|
93
|
+
]
|
|
94
|
+
},
|
|
95
|
+
{
|
|
96
|
+
"cell_type": "markdown",
|
|
97
|
+
"id": "build-md",
|
|
98
|
+
"metadata": {},
|
|
99
|
+
"source": [
|
|
100
|
+
"## 2. From rdflib to `.rete` — what the Builder assembles\n",
|
|
101
|
+
"\n",
|
|
102
|
+
"`rete.Builder()` accepts the rdflib graph directly (anything with a\n",
|
|
103
|
+
"`.serialize()` method works). Configuration is **lazy** — each call just\n",
|
|
104
|
+
"records intent — and `run()` assembles the actual file, which is built from\n",
|
|
105
|
+
"four kinds of sections:\n",
|
|
106
|
+
"\n",
|
|
107
|
+
"| Part | What it is | Why it exists |\n",
|
|
108
|
+
"|---|---|---|\n",
|
|
109
|
+
"| **Dictionary** | every distinct term (IRI, literal, bnode) stored **once**, sorted, in role-split sections | queries compare small integer ids, not strings; sorted order → prefix lookups & compression |\n",
|
|
110
|
+
"| **Triple indexes** | the id-triples in **three permutations** (SPO / POS / OSP) with zone maps | any triple pattern — `(s ? ?)`, `(? p ?)`, `(? ? o)` — becomes a *range scan*, which is also what makes lazy HTTP-range querying possible |\n",
|
|
111
|
+
"| **Pyramid** | a leveled *summary* of the graph — communities of nodes, rolled up level by level, plus the label index | \"zoom out\" views, the schema profile, and label autocomplete, all readable without touching the big indexes |\n",
|
|
112
|
+
"| **Dataset Card** | a JSON self-description in the metadata section | the file documents itself: title, license, counts, and runnable starter queries |\n",
|
|
113
|
+
"\n",
|
|
114
|
+
"For the pyramid we choose `algo=\"types\"`: **one community per `rdf:type`\n",
|
|
115
|
+
"class** — for an ontology-shaped graph like ours, the summary *is* the class\n",
|
|
116
|
+
"structure. (The default, `louvain`, clusters by connectivity instead.)"
|
|
117
|
+
]
|
|
118
|
+
},
|
|
119
|
+
{
|
|
120
|
+
"cell_type": "code",
|
|
121
|
+
"execution_count": null,
|
|
122
|
+
"id": "build",
|
|
123
|
+
"metadata": {},
|
|
124
|
+
"outputs": [],
|
|
125
|
+
"source": "builder = (\n rete.Builder()\n .add(kg) # the rdflib graph, directly\n .card(\n title=\"Solar system micro-KG\",\n description=\"Eight bodies + a tiny ontology (Planet/Moon ⊑ CelestialBody), \"\n \"authored in rdflib inside a browser tab.\",\n license=\"CC0-1.0\",\n created=\"2026-07-17\",\n )\n .example(\n \"SELECT ?body ?parent WHERE { ?body <https://example.org/solar/orbits> ?parent }\",\n title=\"Who orbits whom?\",\n question=\"Which body orbits which other body?\",\n )\n .example(\n \"SELECT DISTINCT ?b WHERE { ?b a <https://example.org/solar/CelestialBody> }\",\n title=\"All celestial bodies (needs reasoning!)\",\n question=\"Which things are celestial bodies — including inferred ones?\",\n )\n .pyramid(algo=\"types\") # communities = classes\n .text_index() # opt-in full-text index\n)\n\ndata = builder.run()\nprint(f\"{len(data):,} bytes\")\nbuilder.stats"
|
|
126
|
+
},
|
|
127
|
+
{
|
|
128
|
+
"cell_type": "markdown",
|
|
129
|
+
"id": "dict-md",
|
|
130
|
+
"metadata": {},
|
|
131
|
+
"source": [
|
|
132
|
+
"## 3. The dictionary — terms are stored once\n",
|
|
133
|
+
"\n",
|
|
134
|
+
"Look at `stats` above: the file holds more **statements** than **terms**.\n",
|
|
135
|
+
"Every IRI like `sol:Sun` appears in many triples but is written once; each\n",
|
|
136
|
+
"triple is just three small integers pointing into the dictionary. That's the\n",
|
|
137
|
+
"same trick as HDT and column stores — and because the dictionary is sorted\n",
|
|
138
|
+
"and chunked, a remote client can fetch *only the chunks a query touches* and\n",
|
|
139
|
+
"run **prefix searches** against the label index without a full scan:"
|
|
140
|
+
]
|
|
141
|
+
},
|
|
142
|
+
{
|
|
143
|
+
"cell_type": "code",
|
|
144
|
+
"execution_count": null,
|
|
145
|
+
"id": "dict",
|
|
146
|
+
"metadata": {},
|
|
147
|
+
"outputs": [],
|
|
148
|
+
"source": [
|
|
149
|
+
"g = builder.graph()\n",
|
|
150
|
+
"print(f\"statements: {g.quads} · distinct terms: {g.terms}\")\n",
|
|
151
|
+
"g.prefix_search(\"M\") # label index: everything labelled M… (Moon, Mars, Mercury)"
|
|
152
|
+
]
|
|
153
|
+
},
|
|
154
|
+
{
|
|
155
|
+
"cell_type": "markdown",
|
|
156
|
+
"id": "pyramid-md",
|
|
157
|
+
"metadata": {},
|
|
158
|
+
"source": [
|
|
159
|
+
"## 4. The pyramid — the graph, summarized by class\n",
|
|
160
|
+
"\n",
|
|
161
|
+
"`pyramidLevels` in the build stats came from the `types` algorithm: level 0\n",
|
|
162
|
+
"groups every node under its `rdf:type` class, and the **schema profile** is\n",
|
|
163
|
+
"exactly that quotient graph — which classes exist, and how they relate\n",
|
|
164
|
+
"through predicates. A client reads this in a couple of small range requests\n",
|
|
165
|
+
"*before* deciding what to query — it's the \"map\" you see first in the\n",
|
|
166
|
+
"[playground](https://caviri.github.io/rete/playground.html):"
|
|
167
|
+
]
|
|
168
|
+
},
|
|
169
|
+
{
|
|
170
|
+
"cell_type": "code",
|
|
171
|
+
"execution_count": null,
|
|
172
|
+
"id": "pyramid",
|
|
173
|
+
"metadata": {},
|
|
174
|
+
"outputs": [],
|
|
175
|
+
"source": [
|
|
176
|
+
"s = g.schema()\n",
|
|
177
|
+
"display(pd.DataFrame(s[\"classes\"], columns=[\"class\", \"instances\"]))\n",
|
|
178
|
+
"pd.DataFrame(s[\"relations\"], columns=[\"subject class\", \"predicate\", \"object class\", \"count\"])"
|
|
179
|
+
]
|
|
180
|
+
},
|
|
181
|
+
{
|
|
182
|
+
"cell_type": "markdown",
|
|
183
|
+
"id": "card-md",
|
|
184
|
+
"metadata": {},
|
|
185
|
+
"source": [
|
|
186
|
+
"## 5. The Dataset Card — the file documents itself\n",
|
|
187
|
+
"\n",
|
|
188
|
+
"The card travels **inside** the file's metadata section. We wrote the curated\n",
|
|
189
|
+
"fields (`title`, `description`, `license`, `created`); the counts and\n",
|
|
190
|
+
"`format_version` were stamped automatically at build time. Every client reads\n",
|
|
191
|
+
"the same card — this notebook, the CLI (`rete card solar.rete`), the\n",
|
|
192
|
+
"playground's catalog view:"
|
|
193
|
+
]
|
|
194
|
+
},
|
|
195
|
+
{
|
|
196
|
+
"cell_type": "code",
|
|
197
|
+
"execution_count": null,
|
|
198
|
+
"id": "card",
|
|
199
|
+
"metadata": {},
|
|
200
|
+
"outputs": [],
|
|
201
|
+
"source": [
|
|
202
|
+
"card = g.card()\n",
|
|
203
|
+
"{k: card[k] for k in (\"title\", \"description\", \"license\", \"created\",\n",
|
|
204
|
+
" \"triple_count\", \"term_count\", \"format_version\")}"
|
|
205
|
+
]
|
|
206
|
+
},
|
|
207
|
+
{
|
|
208
|
+
"cell_type": "markdown",
|
|
209
|
+
"id": "examples-md",
|
|
210
|
+
"metadata": {},
|
|
211
|
+
"source": [
|
|
212
|
+
"## 6. Embedded example queries — a dataset that ships its own questions\n",
|
|
213
|
+
"\n",
|
|
214
|
+
"The two `.example()` calls landed in the card's starter-query library. Anyone\n",
|
|
215
|
+
"who opens this file — today or in ten years, in any client — can list them\n",
|
|
216
|
+
"and run them as-is:"
|
|
217
|
+
]
|
|
218
|
+
},
|
|
219
|
+
{
|
|
220
|
+
"cell_type": "code",
|
|
221
|
+
"execution_count": null,
|
|
222
|
+
"id": "examples",
|
|
223
|
+
"metadata": {},
|
|
224
|
+
"outputs": [],
|
|
225
|
+
"source": [
|
|
226
|
+
"examples = g.examples()\n",
|
|
227
|
+
"display(pd.DataFrame(examples)[[\"title\", \"question\"]])\n",
|
|
228
|
+
"g.query_df(examples[0][\"sparql\"]) # \"Who orbits whom?\", straight from the file"
|
|
229
|
+
]
|
|
230
|
+
},
|
|
231
|
+
{
|
|
232
|
+
"cell_type": "markdown",
|
|
233
|
+
"id": "reason-md",
|
|
234
|
+
"metadata": {},
|
|
235
|
+
"source": [
|
|
236
|
+
"## 7. The ontology at work — reasoning by query rewriting\n",
|
|
237
|
+
"\n",
|
|
238
|
+
"Remember: nothing is *asserted* to be a `CelestialBody`. A plain query finds\n",
|
|
239
|
+
"nothing — but with `reason=True` the engine rewrites the query through the\n",
|
|
240
|
+
"ontology (`Planet ⊑ CelestialBody`, `Moon ⊑ CelestialBody`, plus the\n",
|
|
241
|
+
"`orbits` domain/range), computing OWL 2 QL entailment **without\n",
|
|
242
|
+
"materializing a single new triple**. That's why it works over lazy remote\n",
|
|
243
|
+
"files too:"
|
|
244
|
+
]
|
|
245
|
+
},
|
|
246
|
+
{
|
|
247
|
+
"cell_type": "code",
|
|
248
|
+
"execution_count": null,
|
|
249
|
+
"id": "reason",
|
|
250
|
+
"metadata": {},
|
|
251
|
+
"outputs": [],
|
|
252
|
+
"source": [
|
|
253
|
+
"q = examples[1][\"sparql\"] # \"All celestial bodies\" from the card\n",
|
|
254
|
+
"print(\"asserted only:\", len(g.query(q)), \"rows\")\n",
|
|
255
|
+
"print(\"with OWL 2 QL reasoning:\")\n",
|
|
256
|
+
"g.query_df(q, reason=True) # planets, moons — and the Sun, via orbits' range"
|
|
257
|
+
]
|
|
258
|
+
},
|
|
259
|
+
{
|
|
260
|
+
"cell_type": "markdown",
|
|
261
|
+
"id": "export-md",
|
|
262
|
+
"metadata": {},
|
|
263
|
+
"source": [
|
|
264
|
+
"## 8. Export — one immutable file, queryable anywhere\n",
|
|
265
|
+
"\n",
|
|
266
|
+
"`export()` writes the finished file (here into the notebook's in-browser\n",
|
|
267
|
+
"filesystem — check the file browser on the left; you can download it). Host\n",
|
|
268
|
+
"it on anything that serves HTTP `Range` and it is queryable in place from\n",
|
|
269
|
+
"Python, JavaScript, the CLI, and the playground — carrying its card, its\n",
|
|
270
|
+
"examples, and its ontology with it. For datasets beyond a few million\n",
|
|
271
|
+
"triples, graduate to the streaming\n",
|
|
272
|
+
"[`rete build` CLI](https://caviri.github.io/rete/cli.html)."
|
|
273
|
+
]
|
|
274
|
+
},
|
|
275
|
+
{
|
|
276
|
+
"cell_type": "code",
|
|
277
|
+
"execution_count": null,
|
|
278
|
+
"id": "export",
|
|
279
|
+
"metadata": {},
|
|
280
|
+
"outputs": [],
|
|
281
|
+
"source": "path = builder.export(\"solar.rete\")\nreopened = rete.open(path)\nprint(path, \"·\", reopened.quads, \"triples · content hash\", reopened.content_hash())"
|
|
282
|
+
},
|
|
283
|
+
{
|
|
284
|
+
"cell_type": "markdown",
|
|
285
|
+
"id": "outro",
|
|
286
|
+
"metadata": {},
|
|
287
|
+
"source": [
|
|
288
|
+
"---\n",
|
|
289
|
+
"\n",
|
|
290
|
+
"**Where next:** the [guided tour notebook](rete-graph.ipynb) (remote graphs,\n",
|
|
291
|
+
"lazy range reads) · [Python build tutorial](https://caviri.github.io/rete/python-build-tutorial.html) ·\n",
|
|
292
|
+
"[Dataset Cards](https://caviri.github.io/rete/dataset-cards.html) ·\n",
|
|
293
|
+
"[semantic zoom / pyramid](https://caviri.github.io/rete/semantic-zoom.html) ·\n",
|
|
294
|
+
"[reasoning](https://caviri.github.io/rete/reasoning.html)\n",
|
|
295
|
+
"\n",
|
|
296
|
+
"Source & issues: **<https://github.com/caviri/rete>**\n",
|
|
297
|
+
"\n",
|
|
298
|
+
"© 2026 Carlos Vivar Ríos — released under the\n",
|
|
299
|
+
"[Apache License 2.0](https://github.com/caviri/rete/blob/main/LICENSE)."
|
|
300
|
+
]
|
|
301
|
+
}
|
|
302
|
+
],
|
|
303
|
+
"metadata": {
|
|
304
|
+
"kernelspec": {
|
|
305
|
+
"display_name": "Python (Pyodide)",
|
|
306
|
+
"language": "python",
|
|
307
|
+
"name": "python"
|
|
308
|
+
},
|
|
309
|
+
"language_info": {
|
|
310
|
+
"name": "python"
|
|
311
|
+
}
|
|
312
|
+
},
|
|
313
|
+
"nbformat": 4,
|
|
314
|
+
"nbformat_minor": 5
|
|
315
|
+
}
|
|
@@ -30,7 +30,7 @@
|
|
|
30
30
|
"id": "install",
|
|
31
31
|
"metadata": {},
|
|
32
32
|
"outputs": [],
|
|
33
|
-
"source": "
|
|
33
|
+
"source": "%pip install rete-graph pandas\n\nimport sys\nimport rete_graph as rete\nprint(f\"rete-graph {rete.__version__} running on platform: {sys.platform!r}\")"
|
|
34
34
|
},
|
|
35
35
|
{
|
|
36
36
|
"cell_type": "markdown",
|
|
@@ -18,7 +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
|
-
BlockCacheReader, CountingReader, RangeReader, Rete,
|
|
21
|
+
validate_shacl, BlockCacheReader, CountingReader, DataGraph, RangeReader, Rete, ReteGraph,
|
|
22
|
+
ShaclShapes, DEFAULT_BLOCK,
|
|
22
23
|
};
|
|
23
24
|
#[cfg(not(target_os = "emscripten"))]
|
|
24
25
|
use rete_core::{parse_sparql_json_results, Binding, ServiceClient};
|
|
@@ -126,16 +127,59 @@ impl Graph {
|
|
|
126
127
|
}
|
|
127
128
|
Ok(())
|
|
128
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
|
+
}
|
|
129
139
|
}
|
|
130
140
|
|
|
131
141
|
#[pymethods]
|
|
132
142
|
impl Graph {
|
|
143
|
+
/// Validate the graph against SHACL Core shapes written in Turtle.
|
|
144
|
+
/// Lazy-aware: over the default graph only the shapes' targets are
|
|
145
|
+
/// fetched (the index is consulted in place); a named `graph` view
|
|
146
|
+
/// materializes that graph first. `format` is "json" (default) or "ttl".
|
|
147
|
+
#[pyo3(signature = (shapes_turtle, *, graph=None, format="json"))]
|
|
148
|
+
fn shacl(
|
|
149
|
+
&self,
|
|
150
|
+
py: Python<'_>,
|
|
151
|
+
shapes_turtle: &str,
|
|
152
|
+
graph: Option<&str>,
|
|
153
|
+
format: &str,
|
|
154
|
+
) -> PyResult<String> {
|
|
155
|
+
self.fresh_verdict();
|
|
156
|
+
let out = py
|
|
157
|
+
.allow_threads(|| -> Result<String, String> {
|
|
158
|
+
let shapes =
|
|
159
|
+
ShaclShapes::parse_turtle(shapes_turtle).map_err(|e| e.to_string())?;
|
|
160
|
+
let report = match graph {
|
|
161
|
+
None => validate_shacl(&ReteGraph::new(&self.rete), &shapes),
|
|
162
|
+
Some(g) => {
|
|
163
|
+
validate_shacl(&DataGraph::from_rete(&self.rete, Some(g)), &shapes)
|
|
164
|
+
}
|
|
165
|
+
};
|
|
166
|
+
Ok(match format {
|
|
167
|
+
"ttl" => report.to_turtle(),
|
|
168
|
+
_ => report.to_json(),
|
|
169
|
+
})
|
|
170
|
+
})
|
|
171
|
+
.map_err(PyRuntimeError::new_err)?;
|
|
172
|
+
self.incomplete_guard()?;
|
|
173
|
+
Ok(out)
|
|
174
|
+
}
|
|
175
|
+
|
|
133
176
|
/// Run a SPARQL query (SELECT / ASK / CONSTRUCT / DESCRIBE). Returns the
|
|
134
177
|
/// JSON envelope `{"kind": "select", "vars": [...], "rows": [...]}` etc.;
|
|
135
178
|
/// the Python wrapper parses it into values. `reason=True` turns on OWL 2
|
|
136
179
|
/// QL entailment by query rewriting.
|
|
137
180
|
#[pyo3(signature = (query, *, reason=false))]
|
|
138
181
|
fn query(&self, py: Python<'_>, query: &str, reason: bool) -> PyResult<String> {
|
|
182
|
+
self.fresh_verdict();
|
|
139
183
|
let out = py
|
|
140
184
|
.allow_threads(|| {
|
|
141
185
|
if reason {
|
|
@@ -97,6 +97,43 @@ def test_open_custom_reader(rete_bytes):
|
|
|
97
97
|
assert r.calls >= 1
|
|
98
98
|
|
|
99
99
|
|
|
100
|
+
SHAPES_PREFIX = """\
|
|
101
|
+
@prefix sh: <http://www.w3.org/ns/shacl#> .
|
|
102
|
+
@prefix ex: <http://example.org/> .
|
|
103
|
+
@prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#> .
|
|
104
|
+
"""
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def test_shacl_validation(rete_bytes, serve_bytes):
|
|
108
|
+
g = rete.open(rete_bytes)
|
|
109
|
+
|
|
110
|
+
labeled = SHAPES_PREFIX + """
|
|
111
|
+
ex:PersonShape a sh:NodeShape ;
|
|
112
|
+
sh:targetClass ex:Person ;
|
|
113
|
+
sh:property [ sh:path rdfs:label ; sh:minCount 1 ] .
|
|
114
|
+
"""
|
|
115
|
+
report = g.shacl(labeled)
|
|
116
|
+
assert report["conforms"] is True
|
|
117
|
+
assert report["results"] == []
|
|
118
|
+
|
|
119
|
+
emailed = SHAPES_PREFIX + """
|
|
120
|
+
ex:PersonShape a sh:NodeShape ;
|
|
121
|
+
sh:targetClass ex:Person ;
|
|
122
|
+
sh:property [ sh:path ex:email ; sh:minCount 1 ] .
|
|
123
|
+
"""
|
|
124
|
+
report = g.shacl(emailed)
|
|
125
|
+
assert report["conforms"] is False
|
|
126
|
+
assert len(report["results"]) == 2 # alice and bob both lack ex:email
|
|
127
|
+
|
|
128
|
+
ttl = g.shacl(emailed, format="ttl")
|
|
129
|
+
assert "ValidationReport" in ttl
|
|
130
|
+
|
|
131
|
+
# Lazy path: same verdicts over HTTP range reads.
|
|
132
|
+
remote = rete.open(serve_bytes(rete_bytes))
|
|
133
|
+
assert remote.shacl(labeled)["conforms"] is True
|
|
134
|
+
assert remote.shacl(emailed)["conforms"] is False
|
|
135
|
+
|
|
136
|
+
|
|
100
137
|
def test_schema_and_search(rete_bytes):
|
|
101
138
|
g = rete.open(rete_bytes)
|
|
102
139
|
classes = dict(g.schema()["classes"])
|
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
use std::collections::{BTreeMap, HashMap};
|
|
18
18
|
|
|
19
19
|
use crate::file::Rete;
|
|
20
|
-
use crate::index::{GraphIndex, Pattern};
|
|
20
|
+
use crate::index::{GraphIndex, Pattern, Tile};
|
|
21
21
|
use crate::row::{Ctx, Row, Slots, Val};
|
|
22
22
|
|
|
23
23
|
/// A term in a pattern: a named variable or a constant term token.
|
|
@@ -189,7 +189,7 @@ pub(crate) fn eval_bgp_rows(ctx: &Ctx, index: &GraphIndex, patterns: &[TriplePat
|
|
|
189
189
|
&& shares_bound
|
|
190
190
|
&& !rows.is_empty()
|
|
191
191
|
&& if index.is_remote() {
|
|
192
|
-
rows.len() <=
|
|
192
|
+
rows.len() <= remote_probe_max(index)
|
|
193
193
|
&& (rows.len() <= REMOTE_PROBE_MIN || !pattern_is_selective(&t))
|
|
194
194
|
} else {
|
|
195
195
|
rows.len() <= BGP_PROBE_THRESHOLD
|
|
@@ -275,6 +275,22 @@ fn try_merge_join(
|
|
|
275
275
|
if ca != cb {
|
|
276
276
|
return None; // cross-role: the two role id-spaces aren't co-sorted
|
|
277
277
|
}
|
|
278
|
+
// A merge MATERIALIZES both sides in full (`collect` below builds a Vec per
|
|
279
|
+
// side). When one side is a pinpoint (a bound leading key routing to a few
|
|
280
|
+
// KB of tiles) and the other is fat (a broad predicate spanning megabytes),
|
|
281
|
+
// the linear merge costs building — and, on a remote index, FETCHING — the
|
|
282
|
+
// fat extent end-to-end: gigabytes over HTTP, and the 32-bit wasm OOM
|
|
283
|
+
// behind `?x wdt:P279 <C> . ?x rdfs:label ?l` on a 185M-triple remote
|
|
284
|
+
// graph. The hybrid loop handles that shape strictly better (scan the
|
|
285
|
+
// pinpoint side, probe the fat one per surviving row), so leave the merge
|
|
286
|
+
// seed to comparably-sized sides.
|
|
287
|
+
let (lo, hi) = {
|
|
288
|
+
let (a, b) = (pattern_scan_bytes(index, ta), pattern_scan_bytes(index, tb));
|
|
289
|
+
(a.min(b), a.max(b))
|
|
290
|
+
};
|
|
291
|
+
if hi >= FAT_SCAN_BYTES && hi / 4 >= lo {
|
|
292
|
+
return None;
|
|
293
|
+
}
|
|
278
294
|
let lower = |t: &(SlotTerm, SlotTerm, SlotTerm)| -> Option<Pattern> {
|
|
279
295
|
Some((
|
|
280
296
|
const_subject(&t.0, dict)?,
|
|
@@ -366,6 +382,87 @@ const BGP_PROBE_THRESHOLD: usize = 512;
|
|
|
366
382
|
const REMOTE_PROBE_MIN: usize = 8;
|
|
367
383
|
const REMOTE_PROBE_MAX: usize = 1024;
|
|
368
384
|
|
|
385
|
+
/// The remote probe budget, widened by the reader's concurrency: a serial
|
|
386
|
+
/// sync-XHR reader (a phone without the COI pool) pays one full round trip per
|
|
387
|
+
/// probe, so its budget stays at [`REMOTE_PROBE_MAX`]; a reader overlapping
|
|
388
|
+
/// 16 range reads (the CLI's thread pool, the asyncified fetch variant, the
|
|
389
|
+
/// COI fetch-worker pool) amortizes ~16 probes per round trip and gets a
|
|
390
|
+
/// proportionally larger budget before the one-pass scan wins again.
|
|
391
|
+
fn remote_probe_max(index: &GraphIndex) -> usize {
|
|
392
|
+
REMOTE_PROBE_MAX * index.read_concurrency().clamp(1, 16)
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
/// Batch-fault the tiles that a [`ProbePlan`]'s FIRST pattern will route to
|
|
396
|
+
/// across a batch of seed rows — one coalesced read per section instead of a
|
|
397
|
+
/// blocking round trip per row. Only the first pattern is prefetchable (later
|
|
398
|
+
/// ones bind from earlier probe results), and only rows whose seed binds the
|
|
399
|
+
/// pattern's subject or object are included: a predicate-only pattern routes
|
|
400
|
+
/// to the predicate's WHOLE span, which for the fat sides this path serves
|
|
401
|
+
/// would prefetch the very extent the probe strategy exists to avoid.
|
|
402
|
+
pub(crate) fn prefetch_plan_probes(ctx: &Ctx, index: &GraphIndex, plan: &ProbePlan, rows: &[Row]) {
|
|
403
|
+
if !index.is_remote() {
|
|
404
|
+
return;
|
|
405
|
+
}
|
|
406
|
+
let Some(t) = plan.pats.first() else {
|
|
407
|
+
return;
|
|
408
|
+
};
|
|
409
|
+
let mut pats: Vec<Pattern> = Vec::new();
|
|
410
|
+
for base in rows {
|
|
411
|
+
if let (Some(s), Some(p), Some(o)) = (
|
|
412
|
+
probe_subject(ctx, &t.0, base),
|
|
413
|
+
probe_predicate(ctx, &t.1, base),
|
|
414
|
+
probe_object(ctx, &t.2, base),
|
|
415
|
+
) {
|
|
416
|
+
if s.is_some() || o.is_some() {
|
|
417
|
+
pats.push((s, p, o));
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
index.prefetch_probe_tiles(&pats);
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
/// Batch-fault the tiles that probing `patterns` from `subject_ids` (each
|
|
425
|
+
/// bound onto the subject variable `sv`) will route to — one coalesced read
|
|
426
|
+
/// per section instead of one round trip per candidate. Purely a cache
|
|
427
|
+
/// warmer: correctness is untouched, and a local index is a no-op. Used by
|
|
428
|
+
/// the FILTER-CONTAINS pushdown before it probes its candidate subjects.
|
|
429
|
+
pub(crate) fn prefetch_subject_probes(
|
|
430
|
+
ctx: &Ctx,
|
|
431
|
+
index: &GraphIndex,
|
|
432
|
+
patterns: &[TriplePattern],
|
|
433
|
+
sv: &str,
|
|
434
|
+
subject_ids: &[u32],
|
|
435
|
+
) {
|
|
436
|
+
if !index.is_remote() {
|
|
437
|
+
return;
|
|
438
|
+
}
|
|
439
|
+
let Some(lowered) = lower(patterns, ctx) else {
|
|
440
|
+
return;
|
|
441
|
+
};
|
|
442
|
+
let Some(slot) = ctx.slots.slot(sv) else {
|
|
443
|
+
return;
|
|
444
|
+
};
|
|
445
|
+
let dict = ctx.rete.dictionary();
|
|
446
|
+
let mut pats: Vec<Pattern> = Vec::new();
|
|
447
|
+
for &sid in subject_ids {
|
|
448
|
+
let mut base = ctx.slots.empty_row();
|
|
449
|
+
base[slot] = Some(Val::Id(dict.subject_node(sid) as i64));
|
|
450
|
+
for t in &lowered {
|
|
451
|
+
if !matches!(t.0, SlotTerm::Var(i) if i == slot) {
|
|
452
|
+
continue;
|
|
453
|
+
}
|
|
454
|
+
if let (Some(s), Some(p), Some(o)) = (
|
|
455
|
+
probe_subject(ctx, &t.0, &base),
|
|
456
|
+
probe_predicate(ctx, &t.1, &base),
|
|
457
|
+
probe_object(ctx, &t.2, &base),
|
|
458
|
+
) {
|
|
459
|
+
pats.push((s, p, o));
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
index.prefetch_probe_tiles(&pats);
|
|
464
|
+
}
|
|
465
|
+
|
|
369
466
|
/// A pattern whose own constant terms (a bound subject or object) already route
|
|
370
467
|
/// it to a narrow index range — a few coalesced range reads to scan in full.
|
|
371
468
|
fn pattern_is_selective(t: &(SlotTerm, SlotTerm, SlotTerm)) -> bool {
|
|
@@ -531,6 +628,59 @@ fn pattern_estimates(
|
|
|
531
628
|
)
|
|
532
629
|
}
|
|
533
630
|
|
|
631
|
+
/// The minimum, over a BGP's patterns, of [`pattern_scan_bytes`] — the
|
|
632
|
+
/// cheapest single-pattern scan the hash-join path would at least perform to
|
|
633
|
+
/// materialize this BGP. `None` = a constant term is unknown to the
|
|
634
|
+
/// dictionary (the BGP is unsatisfiable; the caller short-circuits anyway).
|
|
635
|
+
/// Unlike [`pattern_estimates`], this needs no pyramid summary, so it works on
|
|
636
|
+
/// remote-lazy files — exactly where the answer matters most.
|
|
637
|
+
pub(crate) fn bgp_min_scan_bytes(
|
|
638
|
+
ctx: &Ctx,
|
|
639
|
+
index: &GraphIndex,
|
|
640
|
+
patterns: &[TriplePattern],
|
|
641
|
+
) -> Option<u64> {
|
|
642
|
+
let lowered = lower(patterns, ctx)?;
|
|
643
|
+
lowered.iter().map(|t| pattern_scan_bytes(index, t)).min()
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
/// One pattern's cheapest scan extent in BYTES: for each permutation, sum the
|
|
647
|
+
/// encoded lengths of the tiles a scan of this pattern would route to (all
|
|
648
|
+
/// tiles covering the bound leading key, or the whole section when the leading
|
|
649
|
+
/// key is unbound), and take the minimum. Tile *counts* are useless here — a
|
|
650
|
+
/// broad predicate can sit inside one giant tile — but the directory's byte
|
|
651
|
+
/// lengths expose it. Reads only resident metadata, never tile data.
|
|
652
|
+
fn pattern_scan_bytes(index: &GraphIndex, t: &(SlotTerm, SlotTerm, SlotTerm)) -> u64 {
|
|
653
|
+
let sections = index.tile_sections();
|
|
654
|
+
let comp = |role: usize| match role {
|
|
655
|
+
0 => &t.0,
|
|
656
|
+
1 => &t.1,
|
|
657
|
+
_ => &t.2,
|
|
658
|
+
};
|
|
659
|
+
let mut best = u64::MAX;
|
|
660
|
+
for perm in crate::index::ALL_PERMS {
|
|
661
|
+
let tiles = sections[perm.section_index()];
|
|
662
|
+
best = best.min(match comp(perm.roles()[0]) {
|
|
663
|
+
SlotTerm::Node(id) | SlotTerm::Pred(id) => tiles
|
|
664
|
+
.iter()
|
|
665
|
+
.filter(|tile| {
|
|
666
|
+
let (lo, hi) = tile.leading_range();
|
|
667
|
+
lo <= *id && *id <= hi
|
|
668
|
+
})
|
|
669
|
+
.map(Tile::encoded_len)
|
|
670
|
+
.sum(),
|
|
671
|
+
SlotTerm::Var(_) => tiles.iter().map(Tile::encoded_len).sum(),
|
|
672
|
+
});
|
|
673
|
+
}
|
|
674
|
+
best
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
/// Scan extent above which a join side counts as "fat": materializing (or, on
|
|
678
|
+
/// a remote index, fetching) ≥2 MiB of encoded tiles to build a hash/merge
|
|
679
|
+
/// side is past the point where probing it per surviving row wins. Shared by
|
|
680
|
+
/// the merge-seed asymmetry gate here and the left-join force-probe gate in
|
|
681
|
+
/// `sparql::eval`.
|
|
682
|
+
pub(crate) const FAT_SCAN_BYTES: u64 = 2 << 20;
|
|
683
|
+
|
|
534
684
|
/// Order pattern indices for a left-deep join: cheapest (smallest estimated
|
|
535
685
|
/// cardinality) first, always preferring a pattern that shares a variable with
|
|
536
686
|
/// the already-joined set so the join stays connected and intermediate relations
|