This commit is contained in:
Hassan Abedi 2026-06-05 11:31:18 +02:00
parent 510662e7c9
commit e68a4b0dee
43 changed files with 3728 additions and 2306 deletions

View File

@ -8,7 +8,7 @@ indent_size = 4
insert_final_newline = true
trim_trailing_whitespace = true
[*.rs]
[*.{rs,hs,py}]
max_line_length = 100
[*.md]

View File

@ -50,7 +50,7 @@ Expected durable areas may include:
- `src/`: Rust source for parser, catalog, planner, execution experiments, and storage prototypes.
- `tests/`: integration tests for rule planning, evaluation, and storage behavior.
- `examples/`: small runnable Datalog-like programs or storage scenarios.
- `tools/exporter/examples/`: hand-authored scenario JSON consumed by the Haskell exporter to produce runner fixtures.
- `fixtures/`: committed input facts and expected outputs.
- `notes/`: local design notes that belong to this project.
- `flowlog/`: project-local notes or sketches derived from the FlowLog line of work.

21
Cargo.lock generated
View File

@ -555,16 +555,6 @@ dependencies = [
"wasip3",
]
[[package]]
name = "glog-runner"
version = "0.1.0"
dependencies = [
"query-ops",
"serde",
"serde_json",
"storage",
]
[[package]]
name = "guardian"
version = "1.3.0"
@ -1156,6 +1146,17 @@ version = "0.3.33"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "19f132c84eca552bf34cab8ec81f1c1dcc229b811638f9d283dceabe58c5569e"
[[package]]
name = "plan-runner"
version = "0.1.0"
dependencies = [
"query-ops",
"serde",
"serde_json",
"storage",
"tempfile",
]
[[package]]
name = "plotters"
version = "0.3.7"

View File

@ -77,22 +77,28 @@ clean: ## Remove build output
fi
EXPORTER_DIR := tools/exporter
EXPORTER_FIXTURES := crates/glog-runner/fixtures
EXPORTER_SCENARIOS := three-atom-chain
EXPORTER_FIXTURES := crates/plan-runner/fixtures
EXAMPLES_DIR := $(EXPORTER_DIR)/examples
.PHONY: export-fixtures
export-fixtures: ## Regenerate JSON plan fixtures from the Haskell exporter (needs Cabal and GHC; use `make shell` first).
export-fixtures: ## Regenerate plan JSON for every tools/exporter/examples/*.scenario.json (needs Cabal and GHC; use `make shell` first).
@if ! command -v cabal >/dev/null 2>&1; then \
echo "cabal not found. Enter the dev shell with 'make shell' (or 'nix develop') first."; \
exit 1; \
fi
@cd $(EXPORTER_DIR) && cabal build glog-export
@for sc in $(EXPORTER_SCENARIOS); do \
out=$(EXPORTER_FIXTURES)/$$(echo $$sc | tr '-' '_').json; \
@cd $(EXPORTER_DIR) && cabal build plan-export
@mkdir -p $(EXPORTER_FIXTURES)
@for sc in $(EXAMPLES_DIR)/*.scenario.json; do \
base=$$(basename $$sc .scenario.json); \
out=$(EXPORTER_FIXTURES)/$$base.json; \
echo "exporting $$sc -> $$out"; \
(cd $(EXPORTER_DIR) && cabal run -v0 glog-export -- $$sc) > $$out; \
(cd $(EXPORTER_DIR) && cabal run -v0 plan-export -- examples/$$base.scenario.json) > $$out; \
done
.PHONY: examples
examples: export-fixtures ## Regenerate fixtures from scenarios and run them through plan-runner against their oracles.
@cargo test -p plan-runner --test examples
.PHONY: shell
shell: ## Enter the Nix dev shell defined in flake.nix
@nix develop

View File

@ -1,6 +1,6 @@
## Storage Engine Playground
This repo is a playground for running small experiments related to things like FlowLog, DBSP, Geomerge, etc.
This repo is a playground for running small experiments related to storage side of things.
### Development

View File

@ -1,12 +1,8 @@
## Crates
## Crate Overview
Each subdirectory should be a normal Cargo package (or crate) with its own `Cargo.toml`.
Something like this:
```text
crates/
app-name/
Cargo.toml
src/
main.rs
```
| Crate | Kind | Responsibility |
|-----------------|--------------------|-----------------------------------------------------------------------------------------------------------------|
| `storage` | library | Defines a unified interface to use different storage backends (like Geomerge, SQLite, LMDB, etc.) |
| `query-ops` | library | Provides a set of operators (like different types of joins) to execute a query plan. |
| `plan-runner` | library and binary | Provides a CLI to run a query plan against a given storage backend using the operatirs provided by `query-ops`. |
| `geomerge-demo` | binary | An example that shows how to write and read to Geomerge (as a storage). |

View File

@ -5,381 +5,239 @@
-->
<!-- Title: GeomergeDemoWorkflow Pages: 1 -->
<svg width="3020pt" height="407pt"
viewBox="0.00 0.00 3020.25 407.00" xmlns="http://www.w3.org/2000/svg">
<g id="graph0" class="graph" transform="scale(1 1) rotate(0) translate(4 403)">
<title>GeomergeDemoWorkflow</title>
<polygon fill="white" stroke="none" points="-4,4 -4,-403 3016.25,-403 3016.25,4 -4,4"/>
<g id="clust1" class="cluster">
<title>cluster_inputs</title>
<polygon fill="white" stroke="#888888" stroke-dasharray="5,2"
points="8,-11 8,-226 202.5,-226 202.5,-11 8,-11"/>
<text text-anchor="middle" x="105.25" y="-208.7" font-family="Helvetica,Arial,sans-serif" font-size="14.00"
fill="#555555">Inputs
</text>
</g>
<g id="clust2" class="cluster">
<title>cluster_demo</title>
<path fill="#fafafa" stroke="#666666"
d="M284.75,-8C284.75,-8 2574.75,-8 2574.75,-8 2580.75,-8 2586.75,-14 2586.75,-20 2586.75,-20 2586.75,-304 2586.75,-304 2586.75,-310 2580.75,-316 2574.75,-316 2574.75,-316 284.75,-316 284.75,-316 278.75,-316 272.75,-310 272.75,-304 272.75,-304 272.75,-20 272.75,-20 272.75,-14 278.75,-8 284.75,-8"/>
<text text-anchor="middle" x="1429.75" y="-298.7" font-family="Helvetica,Arial,sans-serif" font-size="14.00"
fill="#333333">geomerge&#45;demo (run_demo)
</text>
</g>
<g id="clust3" class="cluster">
<title>cluster_loading</title>
<polygon fill="#fafafa" stroke="#9c27b0" stroke-dasharray="5,2"
points="292.75,-116 292.75,-267 609.25,-267 609.25,-116 292.75,-116"/>
<text text-anchor="middle" x="451" y="-249.7" font-family="Helvetica,Arial,sans-serif" font-size="14.00"
fill="#7b1fa2">Theory Loading
</text>
</g>
<g id="clust4" class="cluster">
<title>cluster_store</title>
<polygon fill="#fafafa" stroke="#4caf50" stroke-dasharray="5,2"
points="648.25,-28 648.25,-267 1744,-267 1744,-28 648.25,-28"/>
<text text-anchor="middle" x="1196.12" y="-249.7" font-family="Helvetica,Arial,sans-serif" font-size="14.00"
fill="#388e3c">Storage and Transaction
</text>
</g>
<g id="clust5" class="cluster">
<title>cluster_persist</title>
<polygon fill="#fafafa" stroke="#ff9800" stroke-dasharray="5,2"
points="1809,-77 1809,-267 2566.75,-267 2566.75,-77 1809,-77"/>
<text text-anchor="middle" x="2187.88" y="-249.7" font-family="Helvetica,Arial,sans-serif" font-size="14.00"
fill="#f57c00">Persistence Round Trip
</text>
</g>
<g id="clust6" class="cluster">
<title>cluster_report</title>
<polygon fill="white" stroke="#888888" stroke-dasharray="5,2"
points="2610.75,-174 2610.75,-391 3004.25,-391 3004.25,-174 2610.75,-174"/>
<text text-anchor="middle" x="2807.5" y="-373.7" font-family="Helvetica,Arial,sans-serif" font-size="14.00"
fill="#555555">Report
</text>
</g>
<!-- paths_schema -->
<g id="node1" class="node">
<title>paths_schema</title>
<path fill="#e8f4fd" stroke="#2196f3" stroke-width="1.5"
d="M159,-179C159,-179 51.5,-179 51.5,-179 45.5,-179 39.5,-173 39.5,-167 39.5,-167 39.5,-141 39.5,-141 39.5,-135 45.5,-129 51.5,-129 51.5,-129 159,-129 159,-129 165,-129 171,-135 171,-141 171,-141 171,-167 171,-167 171,-173 165,-179 159,-179"/>
<text text-anchor="middle" x="105.25" y="-161.7" font-family="Helvetica,Arial,sans-serif" font-size="14.00">
paths.json
</text>
<text text-anchor="middle" x="105.25" y="-140.7" font-family="Helvetica,Arial,sans-serif" font-size="14.00">
(compiled schema)
</text>
</g>
<!-- load_theory -->
<g id="node3" class="node">
<title>load_theory</title>
<path fill="#f3e5f5" stroke="#9c27b0" stroke-width="1.5"
d="M422.5,-191C422.5,-191 318.75,-191 318.75,-191 312.75,-191 306.75,-185 306.75,-179 306.75,-179 306.75,-153 306.75,-153 306.75,-147 312.75,-141 318.75,-141 318.75,-141 422.5,-141 422.5,-141 428.5,-141 434.5,-147 434.5,-153 434.5,-153 434.5,-179 434.5,-179 434.5,-185 428.5,-191 422.5,-191"/>
<text text-anchor="middle" x="370.62" y="-173.7" font-family="Helvetica,Arial,sans-serif" font-size="14.00">
load_paths_theory
</text>
<text text-anchor="middle" x="370.62" y="-152.7" font-family="Helvetica,Arial,sans-serif" font-size="14.00">
(serde_json)
</text>
</g>
<!-- paths_schema&#45;&gt;load_theory -->
<g id="edge1" class="edge">
<title>paths_schema&#45;&gt;load_theory</title>
<path fill="none" stroke="#2196f3" stroke-width="1.2"
d="M171.35,-156.97C208.77,-158.67 255.91,-160.82 294.83,-162.59"/>
<polygon fill="#2196f3" stroke="#2196f3" stroke-width="1.2"
points="294.67,-166.09 304.82,-163.05 294.99,-159.1 294.67,-166.09"/>
<text text-anchor="middle" x="241.12" y="-166.02" font-family="Helvetica,Arial,sans-serif" font-size="9.00"
fill="#555555">include_str!
</text>
</g>
<!-- fixture_rows -->
<g id="node2" class="node">
<title>fixture_rows</title>
<path fill="#e8f4fd" stroke="#2196f3" stroke-width="1.5"
d="M172.5,-79C172.5,-79 38,-79 38,-79 32,-79 26,-73 26,-67 26,-67 26,-41 26,-41 26,-35 32,-29 38,-29 38,-29 172.5,-29 172.5,-29 178.5,-29 184.5,-35 184.5,-41 184.5,-41 184.5,-67 184.5,-67 184.5,-73 178.5,-79 172.5,-79"/>
<text text-anchor="middle" x="105.25" y="-61.7" font-family="Helvetica,Arial,sans-serif" font-size="14.00">
Fixture Rows
</text>
<text text-anchor="middle" x="105.25" y="-40.7" font-family="Helvetica,Arial,sans-serif" font-size="14.00">
(graphs, vertices, edge)
</text>
</g>
<!-- transact -->
<g id="node6" class="node">
<title>transact</title>
<path fill="#e8f5e9" stroke="#4caf50" stroke-width="1.5"
d="M1147.25,-223.62C1147.25,-223.62 950.75,-223.62 950.75,-223.62 944.75,-223.62 938.75,-217.62 938.75,-211.62 938.75,-211.62 938.75,-54.38 938.75,-54.38 938.75,-48.38 944.75,-42.38 950.75,-42.38 950.75,-42.38 1147.25,-42.38 1147.25,-42.38 1153.25,-42.38 1159.25,-48.38 1159.25,-54.38 1159.25,-54.38 1159.25,-211.62 1159.25,-211.62 1159.25,-217.62 1153.25,-223.62 1147.25,-223.62"/>
<text text-anchor="start" x="954.88" y="-203.32" font-family="Helvetica,Arial,sans-serif" font-weight="bold"
font-size="14.00">add_paths_data (tx.insert ×7)
</text>
<text text-anchor="start" x="950.75" y="-174.07" font-family="Helvetica,Arial,sans-serif" font-size="14.00">
• insert Graphs rows
</text>
<text text-anchor="start" x="950.75" y="-145.07" font-family="Helvetica,Arial,sans-serif" font-size="14.00">
• insert G0, G1 rows
</text>
<text text-anchor="start" x="950.75" y="-116.08" font-family="Helvetica,Arial,sans-serif" font-size="14.00">
• insert G.V vertices
</text>
<text text-anchor="start" x="950.75" y="-87.08" font-family="Helvetica,Arial,sans-serif" font-size="14.00">
insert G.E edge
</text>
<text text-anchor="start" x="950.75" y="-58.08" font-family="Helvetica,Arial,sans-serif" font-size="14.00">
pending RowIds reused as FKs
</text>
</g>
<!-- fixture_rows&#45;&gt;transact -->
<g id="edge5" class="edge">
<title>fixture_rows&#45;&gt;transact</title>
<path fill="none" stroke="#2196f3" stroke-width="1.2" stroke-dasharray="5,2"
d="M184.91,-54C237.25,-54 307.55,-54 369.62,-54 369.62,-54 369.62,-54 768,-54 821.42,-54 878.49,-67.48 927.08,-83.31"/>
<polygon fill="#2196f3" stroke="#2196f3" stroke-width="1.2"
points="925.95,-86.62 936.55,-86.47 928.17,-79.98 925.95,-86.62"/>
</g>
<!-- flat_theory -->
<g id="node4" class="node">
<title>flat_theory</title>
<path fill="#f3e5f5" stroke="#9c27b0" stroke-width="1.5"
d="M583.25,-224.12C583.25,-224.12 513.5,-224.12 513.5,-224.12 507.5,-224.12 501.5,-218.12 501.5,-212.12 501.5,-212.12 501.5,-141.88 501.5,-141.88 501.5,-135.88 507.5,-129.88 513.5,-129.88 513.5,-129.88 583.25,-129.88 583.25,-129.88 589.25,-129.88 595.25,-135.88 595.25,-141.88 595.25,-141.88 595.25,-212.12 595.25,-212.12 595.25,-218.12 589.25,-224.12 583.25,-224.12"/>
<text text-anchor="start" x="513.5" y="-203.82" font-family="Helvetica,Arial,sans-serif" font-weight="bold"
font-size="14.00">FlatTheory
</text>
<text text-anchor="start" x="513.5" y="-174.57" font-family="Helvetica,Arial,sans-serif" font-size="14.00">
10 tables
</text>
<text text-anchor="start" x="513.5" y="-145.57" font-family="Helvetica,Arial,sans-serif" font-size="14.00">
12 laws
</text>
</g>
<!-- load_theory&#45;&gt;flat_theory -->
<g id="edge2" class="edge">
<title>load_theory&#45;&gt;flat_theory</title>
<path fill="none" stroke="#9c27b0" stroke-width="1.2"
d="M434.58,-169.94C452.51,-171.06 471.95,-172.28 489.56,-173.38"/>
<polygon fill="#9c27b0" stroke="#9c27b0" stroke-width="1.2"
points="489.11,-176.86 499.31,-173.99 489.55,-169.87 489.11,-176.86"/>
</g>
<!-- build_store -->
<g id="node5" class="node">
<title>build_store</title>
<path fill="#e8f5e9" stroke="#4caf50" stroke-width="1.5"
d="M859.75,-186C859.75,-186 674.25,-186 674.25,-186 668.25,-186 662.25,-180 662.25,-174 662.25,-174 662.25,-148 662.25,-148 662.25,-142 668.25,-136 674.25,-136 674.25,-136 859.75,-136 859.75,-136 865.75,-136 871.75,-142 871.75,-148 871.75,-148 871.75,-174 871.75,-174 871.75,-180 865.75,-186 859.75,-186"/>
<text text-anchor="middle" x="767" y="-168.7" font-family="Helvetica,Arial,sans-serif" font-size="14.00">
GeomergeStorage::from_theory
</text>
<text text-anchor="middle" x="767" y="-147.7" font-family="Helvetica,Arial,sans-serif" font-size="14.00">
(Store::try_from_theory)
</text>
</g>
<!-- flat_theory&#45;&gt;build_store -->
<g id="edge3" class="edge">
<title>flat_theory&#45;&gt;build_store</title>
<path fill="none" stroke="#9c27b0" stroke-width="1.2"
d="M595.58,-173.59C611.81,-172.39 630.85,-170.98 650.22,-169.55"/>
<polygon fill="#9c27b0" stroke="#9c27b0" stroke-width="1.2"
points="650.44,-173.05 660.15,-168.82 649.92,-166.06 650.44,-173.05"/>
</g>
<!-- demo_report -->
<g id="node13" class="node">
<title>demo_report</title>
<path fill="#eceff1" stroke="#607d8b" stroke-width="1.5"
d="M2810.25,-344.12C2810.25,-344.12 2640.75,-344.12 2640.75,-344.12 2634.75,-344.12 2628.75,-338.12 2628.75,-332.12 2628.75,-332.12 2628.75,-203.88 2628.75,-203.88 2628.75,-197.88 2634.75,-191.88 2640.75,-191.88 2640.75,-191.88 2810.25,-191.88 2810.25,-191.88 2816.25,-191.88 2822.25,-197.88 2822.25,-203.88 2822.25,-203.88 2822.25,-332.12 2822.25,-332.12 2822.25,-338.12 2816.25,-344.12 2810.25,-344.12"/>
<text text-anchor="start" x="2684.25" y="-323.82" font-family="Helvetica,Arial,sans-serif"
font-weight="bold" font-size="14.00">DemoReport
</text>
<text text-anchor="start" x="2640.75" y="-294.57" font-family="Helvetica,Arial,sans-serif"
font-size="14.00">• table_count, law_count
</text>
<text text-anchor="start" x="2640.75" y="-265.57" font-family="Helvetica,Arial,sans-serif"
font-size="14.00">• graph, vertex, edge counts
</text>
<text text-anchor="start" x="2640.75" y="-236.57" font-family="Helvetica,Arial,sans-serif"
font-size="14.00">• edge endpoints
</text>
<text text-anchor="start" x="2640.75" y="-207.57" font-family="Helvetica,Arial,sans-serif"
font-size="14.00">• persisted_bytes
</text>
</g>
<!-- flat_theory&#45;&gt;demo_report -->
<g id="edge14" class="edge">
<title>flat_theory&#45;&gt;demo_report</title>
<path fill="none" stroke="#607d8b" stroke-width="1.2" stroke-dasharray="5,2"
d="M586.43,-224.45C603.41,-243.31 624.95,-263.65 648.25,-277 695.11,-303.85 711.99,-307 766,-307 766,-307 766,-307 2483.88,-307 2527.78,-307 2575.62,-300.59 2616.99,-292.98"/>
<polygon fill="#607d8b" stroke="#607d8b" stroke-width="1.2"
points="2617.52,-296.44 2626.7,-291.14 2616.22,-289.56 2617.52,-296.44"/>
<text text-anchor="middle" x="1776.5" y="-311.95" font-family="Helvetica,Arial,sans-serif" font-size="9.00"
fill="#555555">counts
</text>
</g>
<!-- build_store&#45;&gt;transact -->
<g id="edge4" class="edge">
<title>build_store&#45;&gt;transact</title>
<path fill="none" stroke="#4caf50" stroke-width="1.2"
d="M871.86,-150.61C889.82,-148.82 908.63,-146.94 927,-145.1"/>
<polygon fill="#4caf50" stroke="#4caf50" stroke-width="1.2"
points="927.27,-148.59 936.87,-144.11 926.57,-141.63 927.27,-148.59"/>
</g>
<!-- commit -->
<g id="node7" class="node">
<title>commit</title>
<path fill="#e8f5e9" stroke="#4caf50" stroke-width="1.5"
d="M1483.5,-180.12C1483.5,-180.12 1238.25,-180.12 1238.25,-180.12 1232.25,-180.12 1226.25,-174.12 1226.25,-168.12 1226.25,-168.12 1226.25,-97.88 1226.25,-97.88 1226.25,-91.88 1232.25,-85.88 1238.25,-85.88 1238.25,-85.88 1483.5,-85.88 1483.5,-85.88 1489.5,-85.88 1495.5,-91.88 1495.5,-97.88 1495.5,-97.88 1495.5,-168.12 1495.5,-168.12 1495.5,-174.12 1489.5,-180.12 1483.5,-180.12"/>
<text text-anchor="start" x="1323.75" y="-159.82" font-family="Helvetica,Arial,sans-serif"
font-weight="bold" font-size="14.00">tx.commit()
</text>
<text text-anchor="start" x="1238.25" y="-130.57" font-family="Helvetica,Arial,sans-serif"
font-size="14.00">• law validation
</text>
<text text-anchor="start" x="1238.25" y="-101.58" font-family="Helvetica,Arial,sans-serif"
font-size="14.00">• CommittedTx resolves pending RowIds
</text>
</g>
<!-- transact&#45;&gt;commit -->
<g id="edge6" class="edge">
<title>transact&#45;&gt;commit</title>
<path fill="none" stroke="#4caf50" stroke-width="1.2"
d="M1159.69,-133C1177.34,-133 1195.85,-133 1214.18,-133"/>
<polygon fill="#4caf50" stroke="#4caf50" stroke-width="1.2"
points="1214.06,-136.5 1224.06,-133 1214.06,-129.5 1214.06,-136.5"/>
</g>
<!-- assert_edge -->
<g id="node8" class="node">
<title>assert_edge</title>
<path fill="#e8f5e9" stroke="#4caf50" stroke-width="1.5"
d="M1718,-158C1718,-158 1574.5,-158 1574.5,-158 1568.5,-158 1562.5,-152 1562.5,-146 1562.5,-146 1562.5,-120 1562.5,-120 1562.5,-114 1568.5,-108 1574.5,-108 1574.5,-108 1718,-108 1718,-108 1724,-108 1730,-114 1730,-120 1730,-120 1730,-146 1730,-146 1730,-152 1724,-158 1718,-158"/>
<text text-anchor="middle" x="1646.25" y="-140.7" font-family="Helvetica,Arial,sans-serif"
font-size="14.00">assert_edge_was_stored
</text>
<text text-anchor="middle" x="1646.25" y="-119.7" font-family="Helvetica,Arial,sans-serif"
font-size="14.00">(storage.scan(G.E))
</text>
</g>
<!-- commit&#45;&gt;assert_edge -->
<g id="edge7" class="edge">
<title>commit&#45;&gt;assert_edge</title>
<path fill="none" stroke="#4caf50" stroke-width="1.2"
d="M1495.68,-133C1514.16,-133 1532.83,-133 1550.41,-133"/>
<polygon fill="#4caf50" stroke="#4caf50" stroke-width="1.2"
points="1550.27,-136.5 1560.27,-133 1550.27,-129.5 1550.27,-136.5"/>
</g>
<!-- dump_before -->
<g id="node9" class="node">
<title>dump_before</title>
<path fill="#fff3e0" stroke="#ff9800" stroke-width="1.5"
d="M1917.75,-159C1917.75,-159 1835,-159 1835,-159 1829,-159 1823,-153 1823,-147 1823,-147 1823,-121 1823,-121 1823,-115 1829,-109 1835,-109 1835,-109 1917.75,-109 1917.75,-109 1923.75,-109 1929.75,-115 1929.75,-121 1929.75,-121 1929.75,-147 1929.75,-147 1929.75,-153 1923.75,-159 1917.75,-159"/>
<text text-anchor="middle" x="1876.38" y="-141.7" font-family="Helvetica,Arial,sans-serif"
font-size="14.00">store.dump()
</text>
<text text-anchor="middle" x="1876.38" y="-120.7" font-family="Helvetica,Arial,sans-serif"
font-size="14.00">(before persist)
</text>
</g>
<!-- assert_edge&#45;&gt;dump_before -->
<g id="edge8" class="edge">
<title>assert_edge&#45;&gt;dump_before</title>
<path fill="none" stroke="#ff9800" stroke-width="1.2"
d="M1730.25,-133.36C1756.87,-133.48 1785.93,-133.61 1811,-133.72"/>
<polygon fill="#ff9800" stroke="#ff9800" stroke-width="1.2"
points="1810.76,-137.22 1820.77,-133.76 1810.79,-130.22 1810.76,-137.22"/>
</g>
<!-- encode -->
<g id="node10" class="node">
<title>encode</title>
<path fill="#fff3e0" stroke="#ff9800" stroke-width="1.5"
d="M2108,-159C2108,-159 2008.75,-159 2008.75,-159 2002.75,-159 1996.75,-153 1996.75,-147 1996.75,-147 1996.75,-121 1996.75,-121 1996.75,-115 2002.75,-109 2008.75,-109 2008.75,-109 2108,-109 2108,-109 2114,-109 2120,-115 2120,-121 2120,-121 2120,-147 2120,-147 2120,-153 2114,-159 2108,-159"/>
<text text-anchor="middle" x="2058.38" y="-141.7" font-family="Helvetica,Arial,sans-serif"
font-size="14.00">pst::encode_store
</text>
<text text-anchor="middle" x="2058.38" y="-120.7" font-family="Helvetica,Arial,sans-serif"
font-size="14.00">&#45;&gt; bytes
</text>
</g>
<!-- dump_before&#45;&gt;encode -->
<g id="edge9" class="edge">
<title>dump_before&#45;&gt;encode</title>
<path fill="none" stroke="#ff9800" stroke-width="1.2"
d="M1929.92,-134C1947.11,-134 1966.52,-134 1984.92,-134"/>
<polygon fill="#ff9800" stroke="#ff9800" stroke-width="1.2"
points="1984.7,-137.5 1994.7,-134 1984.7,-130.5 1984.7,-137.5"/>
</g>
<!-- compare -->
<g id="node12" class="node">
<title>compare</title>
<path fill="#fff3e0" stroke="#ff9800" stroke-width="1.5"
d="M2540.75,-224C2540.75,-224 2425,-224 2425,-224 2419,-224 2413,-218 2413,-212 2413,-212 2413,-200 2413,-200 2413,-194 2419,-188 2425,-188 2425,-188 2540.75,-188 2540.75,-188 2546.75,-188 2552.75,-194 2552.75,-200 2552.75,-200 2552.75,-212 2552.75,-212 2552.75,-218 2546.75,-224 2540.75,-224"/>
<text text-anchor="middle" x="2482.88" y="-203.2" font-family="Helvetica,Arial,sans-serif"
font-size="14.00">dump equality check
</text>
</g>
<!-- dump_before&#45;&gt;compare -->
<g id="edge12" class="edge">
<title>dump_before&#45;&gt;compare</title>
<path fill="none" stroke="#ff9800" stroke-width="1.2" stroke-dasharray="5,2"
d="M1927.2,-159.48C1948.13,-168.95 1973.07,-178.68 1996.75,-184 2136.12,-215.31 2303.52,-214.81 2400.78,-210.9"/>
<polygon fill="#ff9800" stroke="#ff9800" stroke-width="1.2"
points="2400.93,-214.39 2410.77,-210.47 2400.63,-207.4 2400.93,-214.39"/>
<text text-anchor="middle" x="2171.38" y="-214.27" font-family="Helvetica,Arial,sans-serif" font-size="9.00"
fill="#555555">expected
</text>
</g>
<!-- decode -->
<g id="node11" class="node">
<title>decode</title>
<path fill="#fff3e0" stroke="#ff9800" stroke-width="1.5"
d="M2334,-159C2334,-159 2234.75,-159 2234.75,-159 2228.75,-159 2222.75,-153 2222.75,-147 2222.75,-147 2222.75,-121 2222.75,-121 2222.75,-115 2228.75,-109 2234.75,-109 2234.75,-109 2334,-109 2334,-109 2340,-109 2346,-115 2346,-121 2346,-121 2346,-147 2346,-147 2346,-153 2340,-159 2334,-159"/>
<text text-anchor="middle" x="2284.38" y="-141.7" font-family="Helvetica,Arial,sans-serif"
font-size="14.00">pst::decode_store
</text>
<text text-anchor="middle" x="2284.38" y="-120.7" font-family="Helvetica,Arial,sans-serif"
font-size="14.00">&#45;&gt; restored Store
</text>
</g>
<!-- encode&#45;&gt;decode -->
<g id="edge10" class="edge">
<title>encode&#45;&gt;decode</title>
<path fill="none" stroke="#ff9800" stroke-width="1.2"
d="M2120.29,-134C2148.18,-134 2181.46,-134 2210.66,-134"/>
<polygon fill="#ff9800" stroke="#ff9800" stroke-width="1.2"
points="2210.66,-137.5 2220.66,-134 2210.66,-130.5 2210.66,-137.5"/>
<text text-anchor="middle" x="2171.38" y="-138.95" font-family="Helvetica,Arial,sans-serif" font-size="9.00"
fill="#555555">bytes
</text>
</g>
<!-- decode&#45;&gt;compare -->
<g id="edge11" class="edge">
<title>decode&#45;&gt;compare</title>
<path fill="none" stroke="#ff9800" stroke-width="1.2"
d="M2346.47,-156.39C2370.11,-165.05 2397.1,-174.94 2420.74,-183.6"/>
<polygon fill="#ff9800" stroke="#ff9800" stroke-width="1.2"
points="2419.39,-186.83 2429.99,-186.99 2421.8,-180.26 2419.39,-186.83"/>
</g>
<!-- compare&#45;&gt;demo_report -->
<g id="edge13" class="edge">
<title>compare&#45;&gt;demo_report</title>
<path fill="none" stroke="#607d8b" stroke-width="1.2"
d="M2553.15,-223.85C2573.13,-229 2595.42,-234.74 2617.13,-240.34"/>
<polygon fill="#607d8b" stroke="#607d8b" stroke-width="1.2"
points="2616.11,-243.69 2626.67,-242.79 2617.85,-236.91 2616.11,-243.69"/>
</g>
<!-- stdout -->
<g id="node14" class="node">
<title>stdout</title>
<path fill="#eceff1" stroke="#607d8b" stroke-width="1.5"
d="M2974.25,-293C2974.25,-293 2901.25,-293 2901.25,-293 2895.25,-293 2889.25,-287 2889.25,-281 2889.25,-281 2889.25,-255 2889.25,-255 2889.25,-249 2895.25,-243 2901.25,-243 2901.25,-243 2974.25,-243 2974.25,-243 2980.25,-243 2986.25,-249 2986.25,-255 2986.25,-255 2986.25,-281 2986.25,-281 2986.25,-287 2980.25,-293 2974.25,-293"/>
<text text-anchor="middle" x="2937.75" y="-275.7" font-family="Helvetica,Arial,sans-serif"
font-size="14.00">stdout
</text>
<text text-anchor="middle" x="2937.75" y="-254.7" font-family="Helvetica,Arial,sans-serif"
font-size="14.00">(println! lines)
</text>
</g>
<!-- demo_report&#45;&gt;stdout -->
<g id="edge15" class="edge">
<title>demo_report&#45;&gt;stdout</title>
<path fill="none" stroke="#607d8b" stroke-width="1.2"
d="M2822.7,-268C2841.33,-268 2860.33,-268 2877.35,-268"/>
<polygon fill="#607d8b" stroke="#607d8b" stroke-width="1.2"
points="2876.96,-271.5 2886.96,-268 2876.96,-264.5 2876.96,-271.5"/>
</g>
</g>
viewBox="0.00 0.00 3020.25 407.00" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<g id="graph0" class="graph" transform="scale(1 1) rotate(0) translate(4 403)">
<title>GeomergeDemoWorkflow</title>
<polygon fill="white" stroke="none" points="-4,4 -4,-403 3016.25,-403 3016.25,4 -4,4"/>
<g id="clust1" class="cluster">
<title>cluster_inputs</title>
<polygon fill="white" stroke="#888888" stroke-dasharray="5,2" points="8,-11 8,-226 202.5,-226 202.5,-11 8,-11"/>
<text text-anchor="middle" x="105.25" y="-208.7" font-family="Helvetica,Arial,sans-serif" font-size="14.00" fill="#555555">Inputs</text>
</g>
<g id="clust2" class="cluster">
<title>cluster_demo</title>
<path fill="#fafafa" stroke="#666666" d="M284.75,-8C284.75,-8 2574.75,-8 2574.75,-8 2580.75,-8 2586.75,-14 2586.75,-20 2586.75,-20 2586.75,-304 2586.75,-304 2586.75,-310 2580.75,-316 2574.75,-316 2574.75,-316 284.75,-316 284.75,-316 278.75,-316 272.75,-310 272.75,-304 272.75,-304 272.75,-20 272.75,-20 272.75,-14 278.75,-8 284.75,-8"/>
<text text-anchor="middle" x="1429.75" y="-298.7" font-family="Helvetica,Arial,sans-serif" font-size="14.00" fill="#333333">geomerge&#45;demo (run_demo)</text>
</g>
<g id="clust3" class="cluster">
<title>cluster_loading</title>
<polygon fill="#fafafa" stroke="#9c27b0" stroke-dasharray="5,2" points="292.75,-116 292.75,-267 609.25,-267 609.25,-116 292.75,-116"/>
<text text-anchor="middle" x="451" y="-249.7" font-family="Helvetica,Arial,sans-serif" font-size="14.00" fill="#7b1fa2">Theory Loading</text>
</g>
<g id="clust4" class="cluster">
<title>cluster_store</title>
<polygon fill="#fafafa" stroke="#4caf50" stroke-dasharray="5,2" points="648.25,-28 648.25,-267 1744,-267 1744,-28 648.25,-28"/>
<text text-anchor="middle" x="1196.12" y="-249.7" font-family="Helvetica,Arial,sans-serif" font-size="14.00" fill="#388e3c">Storage and Transaction</text>
</g>
<g id="clust5" class="cluster">
<title>cluster_persist</title>
<polygon fill="#fafafa" stroke="#ff9800" stroke-dasharray="5,2" points="1809,-77 1809,-267 2566.75,-267 2566.75,-77 1809,-77"/>
<text text-anchor="middle" x="2187.88" y="-249.7" font-family="Helvetica,Arial,sans-serif" font-size="14.00" fill="#f57c00">Persistence Round Trip</text>
</g>
<g id="clust6" class="cluster">
<title>cluster_report</title>
<polygon fill="white" stroke="#888888" stroke-dasharray="5,2" points="2610.75,-174 2610.75,-391 3004.25,-391 3004.25,-174 2610.75,-174"/>
<text text-anchor="middle" x="2807.5" y="-373.7" font-family="Helvetica,Arial,sans-serif" font-size="14.00" fill="#555555">Report</text>
</g>
<!-- paths_schema -->
<g id="node1" class="node">
<title>paths_schema</title>
<path fill="#e8f4fd" stroke="#2196f3" stroke-width="1.5" d="M159,-179C159,-179 51.5,-179 51.5,-179 45.5,-179 39.5,-173 39.5,-167 39.5,-167 39.5,-141 39.5,-141 39.5,-135 45.5,-129 51.5,-129 51.5,-129 159,-129 159,-129 165,-129 171,-135 171,-141 171,-141 171,-167 171,-167 171,-173 165,-179 159,-179"/>
<text text-anchor="middle" x="105.25" y="-161.7" font-family="Helvetica,Arial,sans-serif" font-size="14.00">paths.json</text>
<text text-anchor="middle" x="105.25" y="-140.7" font-family="Helvetica,Arial,sans-serif" font-size="14.00">(compiled schema)</text>
</g>
<!-- load_theory -->
<g id="node3" class="node">
<title>load_theory</title>
<path fill="#f3e5f5" stroke="#9c27b0" stroke-width="1.5" d="M422.5,-191C422.5,-191 318.75,-191 318.75,-191 312.75,-191 306.75,-185 306.75,-179 306.75,-179 306.75,-153 306.75,-153 306.75,-147 312.75,-141 318.75,-141 318.75,-141 422.5,-141 422.5,-141 428.5,-141 434.5,-147 434.5,-153 434.5,-153 434.5,-179 434.5,-179 434.5,-185 428.5,-191 422.5,-191"/>
<text text-anchor="middle" x="370.62" y="-173.7" font-family="Helvetica,Arial,sans-serif" font-size="14.00">load_paths_theory</text>
<text text-anchor="middle" x="370.62" y="-152.7" font-family="Helvetica,Arial,sans-serif" font-size="14.00">(serde_json)</text>
</g>
<!-- paths_schema&#45;&gt;load_theory -->
<g id="edge1" class="edge">
<title>paths_schema&#45;&gt;load_theory</title>
<path fill="none" stroke="#2196f3" stroke-width="1.2" d="M171.35,-156.97C208.77,-158.67 255.91,-160.82 294.83,-162.59"/>
<polygon fill="#2196f3" stroke="#2196f3" stroke-width="1.2" points="294.67,-166.09 304.82,-163.05 294.99,-159.1 294.67,-166.09"/>
<text text-anchor="middle" x="241.12" y="-166.02" font-family="Helvetica,Arial,sans-serif" font-size="9.00" fill="#555555">include_str!</text>
</g>
<!-- fixture_rows -->
<g id="node2" class="node">
<title>fixture_rows</title>
<path fill="#e8f4fd" stroke="#2196f3" stroke-width="1.5" d="M172.5,-79C172.5,-79 38,-79 38,-79 32,-79 26,-73 26,-67 26,-67 26,-41 26,-41 26,-35 32,-29 38,-29 38,-29 172.5,-29 172.5,-29 178.5,-29 184.5,-35 184.5,-41 184.5,-41 184.5,-67 184.5,-67 184.5,-73 178.5,-79 172.5,-79"/>
<text text-anchor="middle" x="105.25" y="-61.7" font-family="Helvetica,Arial,sans-serif" font-size="14.00">Fixture Rows</text>
<text text-anchor="middle" x="105.25" y="-40.7" font-family="Helvetica,Arial,sans-serif" font-size="14.00">(graphs, vertices, edge)</text>
</g>
<!-- transact -->
<g id="node6" class="node">
<title>transact</title>
<path fill="#e8f5e9" stroke="#4caf50" stroke-width="1.5" d="M1147.25,-223.62C1147.25,-223.62 950.75,-223.62 950.75,-223.62 944.75,-223.62 938.75,-217.62 938.75,-211.62 938.75,-211.62 938.75,-54.38 938.75,-54.38 938.75,-48.38 944.75,-42.38 950.75,-42.38 950.75,-42.38 1147.25,-42.38 1147.25,-42.38 1153.25,-42.38 1159.25,-48.38 1159.25,-54.38 1159.25,-54.38 1159.25,-211.62 1159.25,-211.62 1159.25,-217.62 1153.25,-223.62 1147.25,-223.62"/>
<text text-anchor="start" x="954.88" y="-203.32" font-family="Helvetica,Arial,sans-serif" font-weight="bold" font-size="14.00">add_paths_data (tx.insert ×7)</text>
<text text-anchor="start" x="950.75" y="-174.07" font-family="Helvetica,Arial,sans-serif" font-size="14.00">• insert Graphs rows</text>
<text text-anchor="start" x="950.75" y="-145.07" font-family="Helvetica,Arial,sans-serif" font-size="14.00">• insert G0, G1 rows</text>
<text text-anchor="start" x="950.75" y="-116.08" font-family="Helvetica,Arial,sans-serif" font-size="14.00">• insert G.V vertices</text>
<text text-anchor="start" x="950.75" y="-87.08" font-family="Helvetica,Arial,sans-serif" font-size="14.00">• insert G.E edge</text>
<text text-anchor="start" x="950.75" y="-58.08" font-family="Helvetica,Arial,sans-serif" font-size="14.00">• pending RowIds reused as FKs</text>
</g>
<!-- fixture_rows&#45;&gt;transact -->
<g id="edge5" class="edge">
<title>fixture_rows&#45;&gt;transact</title>
<path fill="none" stroke="#2196f3" stroke-width="1.2" stroke-dasharray="5,2" d="M184.91,-54C237.25,-54 307.55,-54 369.62,-54 369.62,-54 369.62,-54 768,-54 821.42,-54 878.49,-67.48 927.08,-83.31"/>
<polygon fill="#2196f3" stroke="#2196f3" stroke-width="1.2" points="925.95,-86.62 936.55,-86.47 928.17,-79.98 925.95,-86.62"/>
</g>
<!-- flat_theory -->
<g id="node4" class="node">
<title>flat_theory</title>
<path fill="#f3e5f5" stroke="#9c27b0" stroke-width="1.5" d="M583.25,-224.12C583.25,-224.12 513.5,-224.12 513.5,-224.12 507.5,-224.12 501.5,-218.12 501.5,-212.12 501.5,-212.12 501.5,-141.88 501.5,-141.88 501.5,-135.88 507.5,-129.88 513.5,-129.88 513.5,-129.88 583.25,-129.88 583.25,-129.88 589.25,-129.88 595.25,-135.88 595.25,-141.88 595.25,-141.88 595.25,-212.12 595.25,-212.12 595.25,-218.12 589.25,-224.12 583.25,-224.12"/>
<text text-anchor="start" x="513.5" y="-203.82" font-family="Helvetica,Arial,sans-serif" font-weight="bold" font-size="14.00">FlatTheory</text>
<text text-anchor="start" x="513.5" y="-174.57" font-family="Helvetica,Arial,sans-serif" font-size="14.00">• 10 tables</text>
<text text-anchor="start" x="513.5" y="-145.57" font-family="Helvetica,Arial,sans-serif" font-size="14.00">• 12 laws</text>
</g>
<!-- load_theory&#45;&gt;flat_theory -->
<g id="edge2" class="edge">
<title>load_theory&#45;&gt;flat_theory</title>
<path fill="none" stroke="#9c27b0" stroke-width="1.2" d="M434.58,-169.94C452.51,-171.06 471.95,-172.28 489.56,-173.38"/>
<polygon fill="#9c27b0" stroke="#9c27b0" stroke-width="1.2" points="489.11,-176.86 499.31,-173.99 489.55,-169.87 489.11,-176.86"/>
</g>
<!-- build_store -->
<g id="node5" class="node">
<title>build_store</title>
<path fill="#e8f5e9" stroke="#4caf50" stroke-width="1.5" d="M859.75,-186C859.75,-186 674.25,-186 674.25,-186 668.25,-186 662.25,-180 662.25,-174 662.25,-174 662.25,-148 662.25,-148 662.25,-142 668.25,-136 674.25,-136 674.25,-136 859.75,-136 859.75,-136 865.75,-136 871.75,-142 871.75,-148 871.75,-148 871.75,-174 871.75,-174 871.75,-180 865.75,-186 859.75,-186"/>
<text text-anchor="middle" x="767" y="-168.7" font-family="Helvetica,Arial,sans-serif" font-size="14.00">GeomergeStorage::from_theory</text>
<text text-anchor="middle" x="767" y="-147.7" font-family="Helvetica,Arial,sans-serif" font-size="14.00">(Store::try_from_theory)</text>
</g>
<!-- flat_theory&#45;&gt;build_store -->
<g id="edge3" class="edge">
<title>flat_theory&#45;&gt;build_store</title>
<path fill="none" stroke="#9c27b0" stroke-width="1.2" d="M595.58,-173.59C611.81,-172.39 630.85,-170.98 650.22,-169.55"/>
<polygon fill="#9c27b0" stroke="#9c27b0" stroke-width="1.2" points="650.44,-173.05 660.15,-168.82 649.92,-166.06 650.44,-173.05"/>
</g>
<!-- demo_report -->
<g id="node13" class="node">
<title>demo_report</title>
<path fill="#eceff1" stroke="#607d8b" stroke-width="1.5" d="M2810.25,-344.12C2810.25,-344.12 2640.75,-344.12 2640.75,-344.12 2634.75,-344.12 2628.75,-338.12 2628.75,-332.12 2628.75,-332.12 2628.75,-203.88 2628.75,-203.88 2628.75,-197.88 2634.75,-191.88 2640.75,-191.88 2640.75,-191.88 2810.25,-191.88 2810.25,-191.88 2816.25,-191.88 2822.25,-197.88 2822.25,-203.88 2822.25,-203.88 2822.25,-332.12 2822.25,-332.12 2822.25,-338.12 2816.25,-344.12 2810.25,-344.12"/>
<text text-anchor="start" x="2684.25" y="-323.82" font-family="Helvetica,Arial,sans-serif" font-weight="bold" font-size="14.00">DemoReport</text>
<text text-anchor="start" x="2640.75" y="-294.57" font-family="Helvetica,Arial,sans-serif" font-size="14.00">• table_count, law_count</text>
<text text-anchor="start" x="2640.75" y="-265.57" font-family="Helvetica,Arial,sans-serif" font-size="14.00">• graph, vertex, edge counts</text>
<text text-anchor="start" x="2640.75" y="-236.57" font-family="Helvetica,Arial,sans-serif" font-size="14.00">• edge endpoints</text>
<text text-anchor="start" x="2640.75" y="-207.57" font-family="Helvetica,Arial,sans-serif" font-size="14.00">• persisted_bytes</text>
</g>
<!-- flat_theory&#45;&gt;demo_report -->
<g id="edge14" class="edge">
<title>flat_theory&#45;&gt;demo_report</title>
<path fill="none" stroke="#607d8b" stroke-width="1.2" stroke-dasharray="5,2" d="M586.43,-224.45C603.41,-243.31 624.95,-263.65 648.25,-277 695.11,-303.85 711.99,-307 766,-307 766,-307 766,-307 2483.88,-307 2527.78,-307 2575.62,-300.59 2616.99,-292.98"/>
<polygon fill="#607d8b" stroke="#607d8b" stroke-width="1.2" points="2617.52,-296.44 2626.7,-291.14 2616.22,-289.56 2617.52,-296.44"/>
<text text-anchor="middle" x="1776.5" y="-311.95" font-family="Helvetica,Arial,sans-serif" font-size="9.00" fill="#555555">counts</text>
</g>
<!-- build_store&#45;&gt;transact -->
<g id="edge4" class="edge">
<title>build_store&#45;&gt;transact</title>
<path fill="none" stroke="#4caf50" stroke-width="1.2" d="M871.86,-150.61C889.82,-148.82 908.63,-146.94 927,-145.1"/>
<polygon fill="#4caf50" stroke="#4caf50" stroke-width="1.2" points="927.27,-148.59 936.87,-144.11 926.57,-141.63 927.27,-148.59"/>
</g>
<!-- commit -->
<g id="node7" class="node">
<title>commit</title>
<path fill="#e8f5e9" stroke="#4caf50" stroke-width="1.5" d="M1483.5,-180.12C1483.5,-180.12 1238.25,-180.12 1238.25,-180.12 1232.25,-180.12 1226.25,-174.12 1226.25,-168.12 1226.25,-168.12 1226.25,-97.88 1226.25,-97.88 1226.25,-91.88 1232.25,-85.88 1238.25,-85.88 1238.25,-85.88 1483.5,-85.88 1483.5,-85.88 1489.5,-85.88 1495.5,-91.88 1495.5,-97.88 1495.5,-97.88 1495.5,-168.12 1495.5,-168.12 1495.5,-174.12 1489.5,-180.12 1483.5,-180.12"/>
<text text-anchor="start" x="1323.75" y="-159.82" font-family="Helvetica,Arial,sans-serif" font-weight="bold" font-size="14.00">tx.commit()</text>
<text text-anchor="start" x="1238.25" y="-130.57" font-family="Helvetica,Arial,sans-serif" font-size="14.00">• law validation</text>
<text text-anchor="start" x="1238.25" y="-101.58" font-family="Helvetica,Arial,sans-serif" font-size="14.00">• CommittedTx resolves pending RowIds</text>
</g>
<!-- transact&#45;&gt;commit -->
<g id="edge6" class="edge">
<title>transact&#45;&gt;commit</title>
<path fill="none" stroke="#4caf50" stroke-width="1.2" d="M1159.69,-133C1177.34,-133 1195.85,-133 1214.18,-133"/>
<polygon fill="#4caf50" stroke="#4caf50" stroke-width="1.2" points="1214.06,-136.5 1224.06,-133 1214.06,-129.5 1214.06,-136.5"/>
</g>
<!-- assert_edge -->
<g id="node8" class="node">
<title>assert_edge</title>
<path fill="#e8f5e9" stroke="#4caf50" stroke-width="1.5" d="M1718,-158C1718,-158 1574.5,-158 1574.5,-158 1568.5,-158 1562.5,-152 1562.5,-146 1562.5,-146 1562.5,-120 1562.5,-120 1562.5,-114 1568.5,-108 1574.5,-108 1574.5,-108 1718,-108 1718,-108 1724,-108 1730,-114 1730,-120 1730,-120 1730,-146 1730,-146 1730,-152 1724,-158 1718,-158"/>
<text text-anchor="middle" x="1646.25" y="-140.7" font-family="Helvetica,Arial,sans-serif" font-size="14.00">assert_edge_was_stored</text>
<text text-anchor="middle" x="1646.25" y="-119.7" font-family="Helvetica,Arial,sans-serif" font-size="14.00">(storage.scan(G.E))</text>
</g>
<!-- commit&#45;&gt;assert_edge -->
<g id="edge7" class="edge">
<title>commit&#45;&gt;assert_edge</title>
<path fill="none" stroke="#4caf50" stroke-width="1.2" d="M1495.68,-133C1514.16,-133 1532.83,-133 1550.41,-133"/>
<polygon fill="#4caf50" stroke="#4caf50" stroke-width="1.2" points="1550.27,-136.5 1560.27,-133 1550.27,-129.5 1550.27,-136.5"/>
</g>
<!-- dump_before -->
<g id="node9" class="node">
<title>dump_before</title>
<path fill="#fff3e0" stroke="#ff9800" stroke-width="1.5" d="M1917.75,-159C1917.75,-159 1835,-159 1835,-159 1829,-159 1823,-153 1823,-147 1823,-147 1823,-121 1823,-121 1823,-115 1829,-109 1835,-109 1835,-109 1917.75,-109 1917.75,-109 1923.75,-109 1929.75,-115 1929.75,-121 1929.75,-121 1929.75,-147 1929.75,-147 1929.75,-153 1923.75,-159 1917.75,-159"/>
<text text-anchor="middle" x="1876.38" y="-141.7" font-family="Helvetica,Arial,sans-serif" font-size="14.00">store.dump()</text>
<text text-anchor="middle" x="1876.38" y="-120.7" font-family="Helvetica,Arial,sans-serif" font-size="14.00">(before persist)</text>
</g>
<!-- assert_edge&#45;&gt;dump_before -->
<g id="edge8" class="edge">
<title>assert_edge&#45;&gt;dump_before</title>
<path fill="none" stroke="#ff9800" stroke-width="1.2" d="M1730.25,-133.36C1756.87,-133.48 1785.93,-133.61 1811,-133.72"/>
<polygon fill="#ff9800" stroke="#ff9800" stroke-width="1.2" points="1810.76,-137.22 1820.77,-133.76 1810.79,-130.22 1810.76,-137.22"/>
</g>
<!-- encode -->
<g id="node10" class="node">
<title>encode</title>
<path fill="#fff3e0" stroke="#ff9800" stroke-width="1.5" d="M2108,-159C2108,-159 2008.75,-159 2008.75,-159 2002.75,-159 1996.75,-153 1996.75,-147 1996.75,-147 1996.75,-121 1996.75,-121 1996.75,-115 2002.75,-109 2008.75,-109 2008.75,-109 2108,-109 2108,-109 2114,-109 2120,-115 2120,-121 2120,-121 2120,-147 2120,-147 2120,-153 2114,-159 2108,-159"/>
<text text-anchor="middle" x="2058.38" y="-141.7" font-family="Helvetica,Arial,sans-serif" font-size="14.00">pst::encode_store</text>
<text text-anchor="middle" x="2058.38" y="-120.7" font-family="Helvetica,Arial,sans-serif" font-size="14.00">&#45;&gt; bytes</text>
</g>
<!-- dump_before&#45;&gt;encode -->
<g id="edge9" class="edge">
<title>dump_before&#45;&gt;encode</title>
<path fill="none" stroke="#ff9800" stroke-width="1.2" d="M1929.92,-134C1947.11,-134 1966.52,-134 1984.92,-134"/>
<polygon fill="#ff9800" stroke="#ff9800" stroke-width="1.2" points="1984.7,-137.5 1994.7,-134 1984.7,-130.5 1984.7,-137.5"/>
</g>
<!-- compare -->
<g id="node12" class="node">
<title>compare</title>
<path fill="#fff3e0" stroke="#ff9800" stroke-width="1.5" d="M2540.75,-224C2540.75,-224 2425,-224 2425,-224 2419,-224 2413,-218 2413,-212 2413,-212 2413,-200 2413,-200 2413,-194 2419,-188 2425,-188 2425,-188 2540.75,-188 2540.75,-188 2546.75,-188 2552.75,-194 2552.75,-200 2552.75,-200 2552.75,-212 2552.75,-212 2552.75,-218 2546.75,-224 2540.75,-224"/>
<text text-anchor="middle" x="2482.88" y="-203.2" font-family="Helvetica,Arial,sans-serif" font-size="14.00">dump equality check</text>
</g>
<!-- dump_before&#45;&gt;compare -->
<g id="edge12" class="edge">
<title>dump_before&#45;&gt;compare</title>
<path fill="none" stroke="#ff9800" stroke-width="1.2" stroke-dasharray="5,2" d="M1927.2,-159.48C1948.13,-168.95 1973.07,-178.68 1996.75,-184 2136.12,-215.31 2303.52,-214.81 2400.78,-210.9"/>
<polygon fill="#ff9800" stroke="#ff9800" stroke-width="1.2" points="2400.93,-214.39 2410.77,-210.47 2400.63,-207.4 2400.93,-214.39"/>
<text text-anchor="middle" x="2171.38" y="-214.27" font-family="Helvetica,Arial,sans-serif" font-size="9.00" fill="#555555">expected</text>
</g>
<!-- decode -->
<g id="node11" class="node">
<title>decode</title>
<path fill="#fff3e0" stroke="#ff9800" stroke-width="1.5" d="M2334,-159C2334,-159 2234.75,-159 2234.75,-159 2228.75,-159 2222.75,-153 2222.75,-147 2222.75,-147 2222.75,-121 2222.75,-121 2222.75,-115 2228.75,-109 2234.75,-109 2234.75,-109 2334,-109 2334,-109 2340,-109 2346,-115 2346,-121 2346,-121 2346,-147 2346,-147 2346,-153 2340,-159 2334,-159"/>
<text text-anchor="middle" x="2284.38" y="-141.7" font-family="Helvetica,Arial,sans-serif" font-size="14.00">pst::decode_store</text>
<text text-anchor="middle" x="2284.38" y="-120.7" font-family="Helvetica,Arial,sans-serif" font-size="14.00">&#45;&gt; restored Store</text>
</g>
<!-- encode&#45;&gt;decode -->
<g id="edge10" class="edge">
<title>encode&#45;&gt;decode</title>
<path fill="none" stroke="#ff9800" stroke-width="1.2" d="M2120.29,-134C2148.18,-134 2181.46,-134 2210.66,-134"/>
<polygon fill="#ff9800" stroke="#ff9800" stroke-width="1.2" points="2210.66,-137.5 2220.66,-134 2210.66,-130.5 2210.66,-137.5"/>
<text text-anchor="middle" x="2171.38" y="-138.95" font-family="Helvetica,Arial,sans-serif" font-size="9.00" fill="#555555">bytes</text>
</g>
<!-- decode&#45;&gt;compare -->
<g id="edge11" class="edge">
<title>decode&#45;&gt;compare</title>
<path fill="none" stroke="#ff9800" stroke-width="1.2" d="M2346.47,-156.39C2370.11,-165.05 2397.1,-174.94 2420.74,-183.6"/>
<polygon fill="#ff9800" stroke="#ff9800" stroke-width="1.2" points="2419.39,-186.83 2429.99,-186.99 2421.8,-180.26 2419.39,-186.83"/>
</g>
<!-- compare&#45;&gt;demo_report -->
<g id="edge13" class="edge">
<title>compare&#45;&gt;demo_report</title>
<path fill="none" stroke="#607d8b" stroke-width="1.2" d="M2553.15,-223.85C2573.13,-229 2595.42,-234.74 2617.13,-240.34"/>
<polygon fill="#607d8b" stroke="#607d8b" stroke-width="1.2" points="2616.11,-243.69 2626.67,-242.79 2617.85,-236.91 2616.11,-243.69"/>
</g>
<!-- stdout -->
<g id="node14" class="node">
<title>stdout</title>
<path fill="#eceff1" stroke="#607d8b" stroke-width="1.5" d="M2974.25,-293C2974.25,-293 2901.25,-293 2901.25,-293 2895.25,-293 2889.25,-287 2889.25,-281 2889.25,-281 2889.25,-255 2889.25,-255 2889.25,-249 2895.25,-243 2901.25,-243 2901.25,-243 2974.25,-243 2974.25,-243 2980.25,-243 2986.25,-249 2986.25,-255 2986.25,-255 2986.25,-281 2986.25,-281 2986.25,-287 2980.25,-293 2974.25,-293"/>
<text text-anchor="middle" x="2937.75" y="-275.7" font-family="Helvetica,Arial,sans-serif" font-size="14.00">stdout</text>
<text text-anchor="middle" x="2937.75" y="-254.7" font-family="Helvetica,Arial,sans-serif" font-size="14.00">(println! lines)</text>
</g>
<!-- demo_report&#45;&gt;stdout -->
<g id="edge15" class="edge">
<title>demo_report&#45;&gt;stdout</title>
<path fill="none" stroke="#607d8b" stroke-width="1.2" d="M2822.7,-268C2841.33,-268 2860.33,-268 2877.35,-268"/>
<polygon fill="#607d8b" stroke="#607d8b" stroke-width="1.2" points="2876.96,-271.5 2886.96,-268 2876.96,-264.5 2876.96,-271.5"/>
</g>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 24 KiB

After

Width:  |  Height:  |  Size: 20 KiB

View File

@ -1,166 +0,0 @@
{
"_scenario": "three-atom-chain",
"facts": {
"edge": [
[
{
"str": "node:1"
},
{
"str": "node:2"
},
{
"str": "edge:1"
}
],
[
{
"str": "node:2"
},
{
"str": "node:3"
},
{
"str": "edge:2"
}
]
],
"node": [
[
{
"str": "node:1"
}
],
[
{
"str": "node:2"
}
],
[
{
"str": "node:3"
}
]
]
},
"query": {
"nodes": [
{
"action": {
"scan": {
"columns": [
{
"var": "a"
},
{
"var": "b"
},
{
"var": "_w0_2"
}
],
"table": "edge"
}
},
"id": 1
},
{
"action": {
"scan": {
"columns": [
{
"var": "b"
},
{
"var": "c"
},
{
"var": "_w1_2"
}
],
"table": "edge"
}
},
"id": 2
},
{
"action": {
"scan": {
"columns": [
{
"var": "a"
}
],
"table": "node"
}
},
"id": 3
},
{
"action": {
"join": {
"left": 1,
"op": "left",
"right": 3
}
},
"id": 4
},
{
"action": {
"join": {
"left": 2,
"op": "left",
"right": 4
}
},
"id": 5
},
{
"action": {
"join": {
"left": 5,
"op": "right",
"right": 4
}
},
"id": 6
},
{
"action": {
"join": {
"left": 6,
"op": "right",
"right": 3
}
},
"id": 7
},
{
"action": {
"join": {
"left": 6,
"op": "natural",
"right": 7
}
},
"id": 8
},
{
"action": {
"join": {
"left": 5,
"op": "natural",
"right": 8
}
},
"id": 9
}
],
"root": 9
},
"schema": {
"edge": 3,
"node": 1
}
}

View File

@ -1,344 +0,0 @@
//! End-to-end runner that executes a `geolog-lang` conjunctive-query plan
//! against this workspace's storage and `query-ops` operators.
//!
//! The upstream Haskell planner in `external/geolog/geolog-lang`
//! (`Geolog.DB.Plan`) builds a Yannakakis-style join DAG over `QAtom`s. This
//! crate accepts that DAG as JSON, materializes the input relations through
//! the [`Storage`] trait, and walks the DAG using
//! [`query_ops::atom::scan_atom`], [`query_ops::join::semijoin`], and
//! [`query_ops::join::natural_join`]. The result is a binding
//! [`Relation`](query_ops::relation::Relation) over the query's variables.
//!
//! The JSON IR mirrors `Geolog.DB.Plan.JoinPlan` and `Geolog.DB.InMemory.QAtom`
//! without depending on the Haskell side at build time. A Haskell exporter
//! that dumps `(schema, facts, JoinPlan)` to this shape is the planned
//! follow-up that completes the round trip; the IR is the contract.
//!
//! Mapping from the Haskell planner:
//!
//! | `Geolog.DB.Plan` | this crate |
//! |-----------------------------|-----------------------------------------------|
//! | `PlanEvalAtom` | [`Action::Scan`] → `scan_atom` |
//! | `PlanJoin LeftJoin a b` | [`Action::Join`] with [`JoinOp::Left`] → `semijoin(rel[a], rel[b])` |
//! | `PlanJoin RightJoin a b` | [`Action::Join`] with [`JoinOp::Right`] → `semijoin(rel[b], rel[a])` |
//! | `PlanJoin NaturalJoin a b` | [`Action::Join`] with [`JoinOp::Natural`] → `natural_join(rel[a], rel[b])` |
//!
//! The atom side covers `evalAtom` (`Geolog.DB.InMemory`): a [`Term::Var`]
//! repeated across positions enforces equality, [`Term::Lit`] filters by
//! constant, and distinct variables project in first-occurrence order.
use std::collections::HashMap;
use serde::Deserialize;
use query_ops::atom::{AtomPattern, Term, scan_atom};
use query_ops::join::{natural_join, semijoin};
use query_ops::relation::Relation;
use storage::value::Value;
use storage::{MemoryStorage, Storage, StorageError, scan_as_table};
/// A single fixture: schema, ground facts, and a query plan to execute.
#[derive(Debug, Clone, Deserialize)]
pub struct Plan {
/// Relation name → arity (column count).
pub schema: HashMap<String, usize>,
/// Relation name → list of ground tuples to insert before execution.
pub facts: HashMap<String, Vec<Vec<JsonValue>>>,
/// The join DAG itself.
pub query: Query,
}
/// Mirrors `Geolog.DB.Plan.JoinPlan`: a set of nodes plus the id of the
/// rooted result node.
#[derive(Debug, Clone, Deserialize)]
pub struct Query {
pub root: u32,
pub nodes: Vec<Node>,
}
/// One node of the plan DAG. `id`s are dense within a `Query` but don't need
/// to start at any particular value, mirroring the Haskell `PlanNodeId`.
#[derive(Debug, Clone, Deserialize)]
pub struct Node {
pub id: u32,
pub action: Action,
}
/// What to compute at a node. Tagged externally so JSON reads as
/// `{"action": {"scan": {...}}}` or `{"action": {"join": {...}}}`.
#[derive(Debug, Clone, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum Action {
Scan(Atom),
Join(Join),
}
/// A flat atom pattern, one entry per column of the target relation.
/// Matches the `toFlatArgs` view used by `Geolog.DB.InMemory.evalAtom`:
/// `qaValues` positions are filled in directly, and the entity-id column
/// (if any) appears at the last position. Wildcard positions in the
/// Haskell `QAtom` (a `Map Int QVal` with a missing key) translate to a
/// fresh, unique variable name on this side, which the operator binds but
/// never joins against.
#[derive(Debug, Clone, Deserialize)]
pub struct Atom {
pub table: String,
pub columns: Vec<JsonTerm>,
}
#[derive(Debug, Clone, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum JsonTerm {
Var(String),
Lit(JsonValue),
}
/// Wire-level value tag. Restricted to what `storage::value::Value` carries.
/// Entity identities from the Haskell side (`ValEntity path id`) round-trip
/// through `Str` for now using a `"path:id"` convention; that's a fixture
/// concern, not a runner concern.
#[derive(Debug, Clone, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum JsonValue {
Int(i64),
Str(String),
}
#[derive(Debug, Clone, Deserialize)]
pub struct Join {
pub op: JoinOp,
pub left: u32,
pub right: u32,
}
#[derive(Debug, Clone, Copy, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum JoinOp {
/// `Geolog.DB.Plan.LeftJoin`: result is `left` rows whose shared columns
/// appear in `right`. Lowered to `semijoin(left, right)`.
Left,
/// `Geolog.DB.Plan.RightJoin`: result is `right` rows whose shared
/// columns appear in `left`. Lowered to `semijoin(right, left)`.
Right,
/// `Geolog.DB.Plan.NaturalJoin`. Lowered to `natural_join(left, right)`.
Natural,
}
/// Errors a runner can produce in addition to storage failures.
#[derive(Debug)]
pub enum RunError {
/// A fact references a relation that isn't declared in `schema`.
UnknownRelation(String),
/// A node id appears in a `Join` action but no node with that id exists.
MissingNode(u32),
/// `Query.root` doesn't match any node in `nodes`.
MissingRoot(u32),
/// Two nodes share the same id.
DuplicateNode(u32),
/// A join node references its left or right side before that side has
/// been computed: the DAG isn't actually topologically sorted by id, or
/// it has a cycle.
UnresolvedDependency { node: u32, depends_on: u32 },
/// Storage layer rejected an operation.
Storage(StorageError),
}
impl std::fmt::Display for RunError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::UnknownRelation(name) => {
write!(f, "facts reference relation {name:?} not in schema")
}
Self::MissingNode(id) => write!(f, "plan references missing node id {id}"),
Self::MissingRoot(id) => write!(f, "plan root id {id} matches no node"),
Self::DuplicateNode(id) => write!(f, "duplicate node id {id} in plan"),
Self::UnresolvedDependency { node, depends_on } => write!(
f,
"node {node} depends on {depends_on}, which has not been computed yet"
),
Self::Storage(err) => write!(f, "storage error: {err}"),
}
}
}
impl std::error::Error for RunError {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
match self {
Self::Storage(err) => Some(err),
_ => None,
}
}
}
impl From<StorageError> for RunError {
fn from(err: StorageError) -> Self {
Self::Storage(err)
}
}
impl From<JsonValue> for Value {
fn from(jv: JsonValue) -> Self {
match jv {
JsonValue::Int(n) => Self::Int(n),
JsonValue::Str(s) => Self::Str(s),
}
}
}
impl From<JsonTerm> for Term {
fn from(t: JsonTerm) -> Self {
match t {
JsonTerm::Var(name) => Self::Var(name),
JsonTerm::Lit(value) => Self::Lit(value.into()),
}
}
}
/// Parse a [`Plan`] from a JSON string.
///
/// # Errors
/// Returns a [`serde_json::Error`] if the input isn't valid JSON in the
/// expected shape.
pub fn parse_plan(json: &str) -> Result<Plan, serde_json::Error> {
serde_json::from_str(json)
}
/// Load schema and facts from a [`Plan`] into a fresh [`MemoryStorage`].
///
/// All facts are inserted in a single transaction; commit is atomic so a
/// failure on row N leaves the storage empty.
///
/// # Errors
/// Returns [`RunError::UnknownRelation`] if facts mention a relation not
/// declared in `schema`. Wraps storage failures (arity mismatch, transaction
/// errors) in [`RunError::Storage`].
pub fn load_into_memory(plan: &Plan) -> Result<MemoryStorage, RunError> {
let mut storage = MemoryStorage::default();
for (name, arity) in &plan.schema {
storage.create_relation(name, *arity)?;
}
{
let mut tx = storage.transaction()?;
for (name, rows) in &plan.facts {
if !plan.schema.contains_key(name) {
return Err(RunError::UnknownRelation(name.clone()));
}
for row in rows {
let cells: Vec<Value> = row.iter().cloned().map(Value::from).collect();
tx.insert(name, cells)?;
}
}
let _ = tx.commit()?;
}
Ok(storage)
}
/// Execute a plan against a storage backend, returning the bindings
/// [`Relation`] for the rooted plan node.
///
/// Nodes are executed in ascending `id` order. For a Yannakakis plan as
/// emitted by `Geolog.DB.Plan` this is equivalent to a topological sort,
/// since `insertJoin` only references node ids that have already been
/// allocated. A non-monotone id ordering is rejected with
/// [`RunError::UnresolvedDependency`].
///
/// # Errors
/// Returns [`RunError::DuplicateNode`] for repeated ids,
/// [`RunError::MissingNode`] for join references to unknown ids,
/// [`RunError::MissingRoot`] if `query.root` isn't present, and storage
/// errors during the per-scan `scan_as_table` call.
pub fn execute<S: Storage>(storage: &S, query: &Query) -> Result<Relation, RunError> {
let mut seen_ids: std::collections::HashSet<u32> =
std::collections::HashSet::with_capacity(query.nodes.len());
for node in &query.nodes {
if !seen_ids.insert(node.id) {
return Err(RunError::DuplicateNode(node.id));
}
}
if !seen_ids.contains(&query.root) {
return Err(RunError::MissingRoot(query.root));
}
let mut ordered: Vec<&Node> = query.nodes.iter().collect();
ordered.sort_by_key(|n| n.id);
let mut results: HashMap<u32, Relation> = HashMap::with_capacity(ordered.len());
for node in ordered {
let computed = match &node.action {
Action::Scan(atom) => {
let table = scan_as_table(storage, &atom.table)?;
let pattern = AtomPattern {
columns: atom.columns.iter().cloned().map(Term::from).collect(),
};
scan_atom(&table, &pattern)
}
Action::Join(join) => {
let left = require_dep(&results, &seen_ids, node.id, join.left)?;
let right = require_dep(&results, &seen_ids, node.id, join.right)?;
match join.op {
JoinOp::Left => semijoin(left, right),
JoinOp::Right => semijoin(right, left),
JoinOp::Natural => natural_join(left, right),
}
}
};
results.insert(node.id, computed);
}
results
.remove(&query.root)
.ok_or(RunError::MissingRoot(query.root))
}
fn require_dep<'a>(
results: &'a HashMap<u32, Relation>,
seen: &std::collections::HashSet<u32>,
node: u32,
depends_on: u32,
) -> Result<&'a Relation, RunError> {
if let Some(rel) = results.get(&depends_on) {
Ok(rel)
} else if seen.contains(&depends_on) {
Err(RunError::UnresolvedDependency { node, depends_on })
} else {
Err(RunError::MissingNode(depends_on))
}
}
/// Convenience: parse JSON, load it into a fresh in-memory storage, and
/// execute, returning the root binding relation.
///
/// # Errors
/// Returns a JSON parse error if the input is malformed, or a [`RunError`]
/// for any later step.
pub fn run_json(json: &str) -> Result<Relation, RunFromJsonError> {
let plan = parse_plan(json).map_err(RunFromJsonError::Parse)?;
let storage = load_into_memory(&plan).map_err(RunFromJsonError::Run)?;
let bindings = execute(&storage, &plan.query).map_err(RunFromJsonError::Run)?;
Ok(bindings)
}
/// Combined error from [`run_json`].
#[derive(Debug)]
pub enum RunFromJsonError {
Parse(serde_json::Error),
Run(RunError),
}
impl std::fmt::Display for RunFromJsonError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Parse(err) => write!(f, "parse error: {err}"),
Self::Run(err) => write!(f, "run error: {err}"),
}
}
}
impl std::error::Error for RunFromJsonError {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
match self {
Self::Parse(err) => Some(err),
Self::Run(err) => Some(err),
}
}
}

View File

@ -1,59 +0,0 @@
//! `glog-run` CLI: read a JSON plan from a file (or stdin if `-`), execute
//! it against a fresh in-memory store, and print the resulting binding
//! relation as JSON on stdout.
use std::io::{self, Read};
use std::process::ExitCode;
fn main() -> ExitCode {
let mut args = std::env::args().skip(1);
let Some(path) = args.next() else {
eprintln!("usage: glog-run <plan.json | ->");
return ExitCode::from(2);
};
let input = match read_input(&path) {
Ok(s) => s,
Err(err) => {
eprintln!("failed to read {path}: {err}");
return ExitCode::from(1);
}
};
let relation = match glog_runner::run_json(&input) {
Ok(r) => r,
Err(err) => {
eprintln!("{err}");
return ExitCode::from(1);
}
};
let payload = serde_json::json!({
"columns": relation.columns,
"rows": relation
.rows
.iter()
.map(|row| row.iter().map(value_to_json).collect::<Vec<_>>())
.collect::<Vec<_>>(),
});
println!("{payload}");
ExitCode::SUCCESS
}
fn read_input(path: &str) -> io::Result<String> {
if path == "-" {
let mut buf = String::new();
io::stdin().read_to_string(&mut buf)?;
Ok(buf)
} else {
std::fs::read_to_string(path)
}
}
fn value_to_json(value: &storage::value::Value) -> serde_json::Value {
match value {
storage::value::Value::Int(n) => serde_json::Value::Number((*n).into()),
storage::value::Value::Str(s) => serde_json::Value::String(s.clone()),
storage::value::Value::Id(id) => serde_json::Value::String(id.to_string()),
}
}

View File

@ -1,73 +0,0 @@
//! End-to-end check: run the JSON fixture and verify the resulting bindings
//! match the `DB.InMemoryTest` "matches evalConjunction on three-atom chain"
//! case from `external/geolog/geolog-lang/test/DB/InMemoryTest.hs`.
//!
//! For `node = {e1, e2, e3}` and `edge = {(e1,e2,ee1), (e2,e3,ee2)}` the
//! conjunction `node(a), edge(a, b, _), edge(b, c, _)` has exactly one
//! solution: `(a=e1, b=e2, c=e3)`.
use std::collections::BTreeMap;
use glog_runner::run_json;
use storage::value::Value;
fn fixture() -> &'static str {
include_str!("../fixtures/three_atom_chain.json")
}
fn ent(path: &str, id: u32) -> Value {
Value::Str(format!("{path}:{id}"))
}
fn project<'a>(
columns: &'a [String],
row: &'a [Value],
keep: &'a [&'a str],
) -> BTreeMap<&'a str, &'a Value> {
keep.iter()
.map(|name| {
let pos = columns
.iter()
.position(|c| c == name)
.expect("column missing");
(*name, &row[pos])
})
.collect()
}
#[test]
fn three_atom_chain_matches_haskell_oracle() {
let result = run_json(fixture()).expect("fixture should execute");
// The plan's root keeps every variable, including the per-atom wildcards
// `_r1` and `_r2`. The oracle only asserts the (a, b, c) projection.
let keep = ["a", "b", "c"];
let mut projected: Vec<BTreeMap<&str, &Value>> = result
.rows
.iter()
.map(|row| project(&result.columns, row, &keep))
.collect();
projected.sort_by_key(|m| format!("{m:?}"));
let e1 = ent("node", 1);
let e2 = ent("node", 2);
let e3 = ent("node", 3);
let expected = vec![BTreeMap::from([("a", &e1), ("b", &e2), ("c", &e3)])];
assert_eq!(projected, expected);
}
#[test]
fn root_columns_cover_a_b_c_plus_two_wildcards() {
// The exporter emits unique wildcard variable names for the entity-id
// column of each edge atom (e.g. `_w0_2`, `_w1_2`); their exact spelling
// is an implementation detail of the exporter, so this test only checks
// that the named variables are all present and that the total column
// count is the three named ones plus two anonymous wildcards.
let result = run_json(fixture()).expect("fixture should execute");
let cols: std::collections::HashSet<&str> = result.columns.iter().map(String::as_str).collect();
for expected in ["a", "b", "c"] {
assert!(cols.contains(expected), "missing column {expected}");
}
assert_eq!(result.columns.len(), 5, "expected 3 named + 2 wildcards");
}

View File

@ -1,5 +1,5 @@
[package]
name = "glog-runner"
name = "plan-runner"
version = "0.1.0"
edition.workspace = true
license.workspace = true
@ -9,11 +9,18 @@ rust-version.workspace = true
workspace = true
[dependencies]
storage = { path = "../storage" }
query-ops = { path = "../query-ops" }
storage = { path = "../storage", features = [
"lmdb",
"redb",
"fjall",
"sqlite",
"geomerge",
] }
serde = { version = "1", features = ["derive"] }
serde_json = "1"
tempfile = "3"
[[bin]]
name = "glog-run"
name = "plan-run"
path = "src/main.rs"

View File

@ -0,0 +1,101 @@
## Plan Runner
This crate implements an executor for (conjunctive) query plans.
The implementation is a CLI tool.
It reads a JSON plan (which currently is a DAG of scan and join nodes plus the input facts),
walks the DAG using the operators from [`query-ops`](../query-ops),
and prints the resulting relation as JSON to stdout.
### Pipeline
End-to-end, scenarios become runner output through three stages:
```text
tools/exporter/examples/*.scenario.json
└── (Haskell exporter; runs Geolog.DB.Plan.planConjunction
and Geolog.DB.InMemory.evalConjunctionPlanned as a self-check)
└── crates/plan-runner/fixtures/*.json (JSON IR; checked in)
└── (plan-runner; this crate)
└── stdout JSON, with row-for-row oracle check
```
The exporter (`tools/exporter`) is the only producer of runner IR today;
it's where atoms are planned and rejected if they don't fit the supported subset.
Fixtures are regenerated with `make export-fixtures`, and the full loop is `make examples`.
What happens inside the runner once a JSON plan arrives:
<div align="center">
<picture>
<img alt="Workflow" src="docs/diagrams/workflow.svg" height="90%" width="90%">
</picture>
</div>
### Storage Backends
The CLI takes a `--backend` flag.
The `memory` backend is the pure in-memory path;
every other backend routes facts through the [`Storage`](../storage) trait
via `build_tables_via_storage`, then scans tables back out before executing.
| Backend | Storage | Location |
|------------------|-------------------|-----------------------|
| `memory` | none | n/a |
| `memory-storage` | `MemoryStorage` | in-process |
| `lmdb` | `LmdbStorage` | fresh tempdir per run |
| `redb` | `RedbStorage` | fresh tempdir per run |
| `fjall` | `FjallStorage` | fresh tempdir per run |
| `sqlite` | `SqliteStorage` | fresh tempdir per run |
| `geomerge` | `GeomergeStorage` | in-process |
### Execute a Query Plan
```sh
# Run a plan with the default backend (no storage)
cargo run -p plan-runner -- crates/plan-runner/fixtures/two_atom_join.json
# Run the same plan with every supported backend
cargo run -p plan-runner -- --backend memory-storage crates/plan-runner/fixtures/two_atom_join.json
cargo run -p plan-runner -- --backend lmdb crates/plan-runner/fixtures/two_atom_join.json
cargo run -p plan-runner -- --backend redb crates/plan-runner/fixtures/two_atom_join.json
cargo run -p plan-runner -- --backend fjall crates/plan-runner/fixtures/two_atom_join.json
cargo run -p plan-runner -- --backend sqlite crates/plan-runner/fixtures/two_atom_join.json
cargo run -p plan-runner -- --backend geomerge crates/plan-runner/fixtures/two_atom_join.json
```
A sample run:
```sh
$ plan-run crates/plan-runner/fixtures/two_atom_join.json
{"columns":["a","b","_w0_2"],"rows":[["node:1","node:2","edge:1"],["node:2","node:1","edge:2"]]}
```
The `_w<atomIdx>_<pos>` columns are wildcards the exporter named so the runner can bind them.
The scenario's `expected_bindings` block names only the variables the test cares about,
and `verify` projects the runner output to that subset before comparing as a multiset.
### Run the Tests
```sh
cargo test -p plan-runner
```
### Notes
- **IR contract.**
The runner is backend-agnostic and frontend-agnostic.
It consumes JSON in the shape documented in `src/lib.rs` and produces a binding relation.
Anything that emits the same JSON can drive it.
- **No optimizer.**
Plans are executed as written.
Node ordering, join shape, and antijoin scheduling are all the producer's responsibility.
This crate's job ends at faithful execution of the IR.
- **Wildcard columns survive.**
`scan_atom` keeps every distinct variable that appears in the pattern,
including the exporter's synthetic `_w<atomIdx>_<pos>` names.
The runner does not project them out;
oracle verification handles that on the comparison side.
- **Bulk, not streaming.**
Each node materializes its full output as a `Relation`.
This matches `query-ops`' execution model;
it's not designed for incremental or maintained-view workloads.

View File

@ -0,0 +1,14 @@
#!/usr/bin/env bash
# You need to have Graphviz installed to run this script
# On Debian-based OSes, you can install it using: sudo apt-get install graphviz
# Directory containing .dot files. Defaults to the script's own directory so the
# script works regardless of the caller's working directory.
SCRIPT_DIR=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)
ASSET_DIR=${1:-"${SCRIPT_DIR}"}
# Make figures from .dot files
for f in "${ASSET_DIR}"/*.dot; do
dot -Tsvg "$f" -o "${f%.dot}.svg"
done

View File

@ -0,0 +1,136 @@
digraph PlanRunnerWorkflow {
fontname = "Helvetica,Arial,sans-serif"
layout = dot
rankdir = LR
ranksep = 0.9;
nodesep = 0.7;
splines = true;
compound = true;
bgcolor = "white"
node [
fontname = "Helvetica,Arial,sans-serif",
shape = box,
style = "filled,rounded",
color = "#555555",
fillcolor = "white",
penwidth = 1.5
]
edge [
fontname = "Helvetica,Arial,sans-serif",
color = "#333333",
fontsize = 9,
fontcolor = "#555555",
labeldistance = 2.0,
penwidth = 1.2
]
subgraph cluster_input {
label = "Input"
style = "dashed"
color = "#888888"
fontcolor = "#555555"
margin = 18
json_plan [label = <<table border="0" cellborder="0" cellspacing="0" cellpadding="4">
<tr><td align="center"><b>JSON Plan</b></td></tr>
<tr><td align="left" balign="left">• schema: name -&gt; arity</td></tr>
<tr><td align="left" balign="left">• facts: name -&gt; rows</td></tr>
<tr><td align="left" balign="left">• query: { root, nodes }</td></tr>
<tr><td align="left" balign="left">• expected_bindings (optional oracle)</td></tr>
</table>>, fillcolor = "#E8F4FD", color = "#2196F3"]
}
subgraph cluster_parse {
label = "Parse"
style = "dashed"
color = "#9C27B0"
fontcolor = "#7B1FA2"
margin = 14
parse_plan [label = "parse_plan(json)\n-&gt; Plan", fillcolor = "#F3E5F5", color = "#9C27B0"]
}
subgraph cluster_load {
label = "Load Tables (--backend selects the path)"
style = "dashed"
color = "#4CAF50"
fontcolor = "#388E3C"
margin = 14
build_pure [label = <<table border="0" cellborder="0" cellspacing="0" cellpadding="4">
<tr><td align="center"><b>build_tables(plan)</b></td></tr>
<tr><td align="left" balign="left">--backend memory</td></tr>
<tr><td align="left" balign="left">direct from plan.facts</td></tr>
</table>>, fillcolor = "#E8F5E9", color = "#4CAF50"]
build_storage [label = <<table border="0" cellborder="0" cellspacing="0" cellpadding="4">
<tr><td align="center"><b>build_tables_via_storage&lt;S: Storage&gt;</b></td></tr>
<tr><td align="left" balign="left">--backend memory-storage</td></tr>
<tr><td align="left" balign="left">--backend lmdb / redb / fjall</td></tr>
<tr><td align="left" balign="left">--backend sqlite / geomerge</td></tr>
<tr><td align="left" balign="left">create_relation → tx.insert → scan_as_table</td></tr>
</table>>, fillcolor = "#E8F5E9", color = "#4CAF50"]
tables_map [label = <<table border="0" cellborder="0" cellspacing="0" cellpadding="4">
<tr><td align="center"><b>HashMap&lt;String, Table&gt;</b></td></tr>
<tr><td align="left" balign="left">positional rows per relation</td></tr>
</table>>, fillcolor = "#E8F4FD", color = "#2196F3"]
}
subgraph cluster_execute {
label = "Execute (walk node DAG in id order)"
style = "dashed"
color = "#FF9800"
fontcolor = "#F57C00"
margin = 14
execute_node [label = <<table border="0" cellborder="0" cellspacing="0" cellpadding="4">
<tr><td align="center"><b>execute(tables, query)</b></td></tr>
<tr><td align="left" balign="left">Action::Scan → scan_atom</td></tr>
<tr><td align="left" balign="left">Action::Join Left → semijoin(l, r)</td></tr>
<tr><td align="left" balign="left">Action::Join Right → semijoin(r, l)</td></tr>
<tr><td align="left" balign="left">Action::Join Natural → natural_join(l, r)</td></tr>
<tr><td align="left" balign="left">cache per-node Relation; return root</td></tr>
</table>>, fillcolor = "#FFF3E0", color = "#FF9800"]
relation_out [label = <<table border="0" cellborder="0" cellspacing="0" cellpadding="4">
<tr><td align="center"><b>Relation</b></td></tr>
<tr><td align="left" balign="left">columns: variables + wildcards</td></tr>
<tr><td align="left" balign="left">rows: bindings</td></tr>
</table>>, fillcolor = "#FFF3E0", color = "#FF9800"]
}
subgraph cluster_verify {
label = "Verify (when expected_bindings is present)"
style = "dashed"
color = "#9C27B0"
fontcolor = "#7B1FA2"
margin = 14
verify_node [label = <<table border="0" cellborder="0" cellspacing="0" cellpadding="4">
<tr><td align="center"><b>verify(plan, relation)</b></td></tr>
<tr><td align="left" balign="left">project to expected.columns</td></tr>
<tr><td align="left" balign="left">multiset compare against expected.rows</td></tr>
</table>>, fillcolor = "#F3E5F5", color = "#9C27B0"]
}
subgraph cluster_output {
label = "Output"
style = "dashed"
color = "#888888"
fontcolor = "#555555"
margin = 18
stdout_json [label = <<table border="0" cellborder="0" cellspacing="0" cellpadding="4">
<tr><td align="center"><b>stdout JSON</b></td></tr>
<tr><td align="left" balign="left">{ columns, rows }</td></tr>
</table>>, fillcolor = "#ECEFF1", color = "#607D8B"]
oracle_pass [label = "Ok(true) / VerifyError\n(used by tests/examples.rs)", fillcolor = "#ECEFF1", color = "#607D8B"]
}
// Pipeline edges
json_plan -> parse_plan [color = "#2196F3"]
parse_plan -> build_pure [label = "Backend::Memory", color = "#9C27B0"]
parse_plan -> build_storage [label = "Backend::*Storage", color = "#9C27B0"]
build_pure -> tables_map [color = "#4CAF50"]
build_storage -> tables_map [color = "#4CAF50"]
tables_map -> execute_node [color = "#2196F3"]
parse_plan -> execute_node [style = "dashed", label = "plan.query", color = "#9C27B0"]
execute_node -> relation_out [color = "#FF9800"]
relation_out -> stdout_json [color = "#607D8B"]
relation_out -> verify_node [style = "dashed", color = "#FF9800"]
parse_plan -> verify_node [style = "dashed", label = "plan.expected_bindings", color = "#9C27B0"]
verify_node -> oracle_pass [color = "#9C27B0"]
}

View File

@ -0,0 +1,202 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN"
"http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<!-- Generated by graphviz version 12.2.1 (0)
-->
<!-- Title: PlanRunnerWorkflow Pages: 1 -->
<svg width="2495pt" height="593pt"
viewBox="0.00 0.00 2495.25 593.00" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<g id="graph0" class="graph" transform="scale(1 1) rotate(0) translate(4 589)">
<title>PlanRunnerWorkflow</title>
<polygon fill="white" stroke="none" points="-4,4 -4,-589 2491.25,-589 2491.25,4 -4,4"/>
<g id="clust1" class="cluster">
<title>cluster_input</title>
<polygon fill="white" stroke="#888888" stroke-dasharray="5,2" points="8,-126 8,-343 290,-343 290,-126 8,-126"/>
<text text-anchor="middle" x="149" y="-325.7" font-family="Helvetica,Arial,sans-serif" font-size="14.00" fill="#555555">Input</text>
</g>
<g id="clust2" class="cluster">
<title>cluster_parse</title>
<polygon fill="white" stroke="#9c27b0" stroke-dasharray="5,2" points="325,-181 325,-288 469.5,-288 469.5,-181 325,-181"/>
<text text-anchor="middle" x="397.25" y="-270.7" font-family="Helvetica,Arial,sans-serif" font-size="14.00" fill="#7b1fa2">Parse</text>
</g>
<g id="clust3" class="cluster">
<title>cluster_load</title>
<polygon fill="white" stroke="#4caf50" stroke-dasharray="5,2" points="584,-224 584,-577 1197.75,-577 1197.75,-224 584,-224"/>
<text text-anchor="middle" x="890.88" y="-559.7" font-family="Helvetica,Arial,sans-serif" font-size="14.00" fill="#388e3c">Load Tables &#160;(&#45;&#45;backend selects the path)</text>
</g>
<g id="clust4" class="cluster">
<title>cluster_execute</title>
<polygon fill="white" stroke="#ff9800" stroke-dasharray="5,2" points="1333.25,-122 1333.25,-361 1893.25,-361 1893.25,-122 1333.25,-122"/>
<text text-anchor="middle" x="1613.25" y="-343.7" font-family="Helvetica,Arial,sans-serif" font-size="14.00" fill="#f57c00">Execute &#160;(walk node DAG in id order)</text>
</g>
<g id="clust5" class="cluster">
<title>cluster_verify</title>
<polygon fill="white" stroke="#9c27b0" stroke-dasharray="5,2" points="1932.25,-8 1932.25,-159 2226.5,-159 2226.5,-8 1932.25,-8"/>
<text text-anchor="middle" x="2079.38" y="-141.7" font-family="Helvetica,Arial,sans-serif" font-size="14.00" fill="#7b1fa2">Verify &#160;(when expected_bindings is present)</text>
</g>
<g id="clust6" class="cluster">
<title>cluster_output</title>
<polygon fill="white" stroke="#888888" stroke-dasharray="5,2" points="2261.5,-37 2261.5,-268 2479.25,-268 2479.25,-37 2261.5,-37"/>
<text text-anchor="middle" x="2370.38" y="-250.7" font-family="Helvetica,Arial,sans-serif" font-size="14.00" fill="#555555">Output</text>
</g>
<!-- json_plan -->
<g id="node1" class="node">
<title>json_plan</title>
<path fill="#e8f4fd" stroke="#2196f3" stroke-width="1.5" d="M260,-296.12C260,-296.12 38,-296.12 38,-296.12 32,-296.12 26,-290.12 26,-284.12 26,-284.12 26,-155.88 26,-155.88 26,-149.88 32,-143.88 38,-143.88 38,-143.88 260,-143.88 260,-143.88 266,-143.88 272,-149.88 272,-155.88 272,-155.88 272,-284.12 272,-284.12 272,-290.12 266,-296.12 260,-296.12"/>
<text text-anchor="start" x="114.12" y="-275.82" font-family="Helvetica,Arial,sans-serif" font-weight="bold" font-size="14.00">JSON Plan</text>
<text text-anchor="start" x="38" y="-246.57" font-family="Helvetica,Arial,sans-serif" font-size="14.00">• schema: name &#45;&gt; arity</text>
<text text-anchor="start" x="38" y="-217.57" font-family="Helvetica,Arial,sans-serif" font-size="14.00">• facts: name &#45;&gt; rows</text>
<text text-anchor="start" x="38" y="-188.57" font-family="Helvetica,Arial,sans-serif" font-size="14.00">• query: { root, nodes }</text>
<text text-anchor="start" x="38" y="-159.57" font-family="Helvetica,Arial,sans-serif" font-size="14.00">• expected_bindings (optional oracle)</text>
</g>
<!-- parse_plan -->
<g id="node2" class="node">
<title>parse_plan</title>
<path fill="#f3e5f5" stroke="#9c27b0" stroke-width="1.5" d="M443.5,-245C443.5,-245 351,-245 351,-245 345,-245 339,-239 339,-233 339,-233 339,-207 339,-207 339,-201 345,-195 351,-195 351,-195 443.5,-195 443.5,-195 449.5,-195 455.5,-201 455.5,-207 455.5,-207 455.5,-233 455.5,-233 455.5,-239 449.5,-245 443.5,-245"/>
<text text-anchor="middle" x="397.25" y="-227.7" font-family="Helvetica,Arial,sans-serif" font-size="14.00">parse_plan(json)</text>
<text text-anchor="middle" x="397.25" y="-206.7" font-family="Helvetica,Arial,sans-serif" font-size="14.00">&#45;&gt; Plan</text>
</g>
<!-- json_plan&#45;&gt;parse_plan -->
<g id="edge1" class="edge">
<title>json_plan&#45;&gt;parse_plan</title>
<path fill="none" stroke="#2196f3" stroke-width="1.2" d="M272.4,-220C291.23,-220 310.09,-220 327.2,-220"/>
<polygon fill="#2196f3" stroke="#2196f3" stroke-width="1.2" points="326.93,-223.5 336.93,-220 326.93,-216.5 326.93,-223.5"/>
</g>
<!-- build_pure -->
<g id="node3" class="node">
<title>build_pure</title>
<path fill="#e8f5e9" stroke="#4caf50" stroke-width="1.5" d="M806.88,-534.12C806.88,-534.12 680.88,-534.12 680.88,-534.12 674.88,-534.12 668.88,-528.12 668.88,-522.12 668.88,-522.12 668.88,-451.88 668.88,-451.88 668.88,-445.88 674.88,-439.88 680.88,-439.88 680.88,-439.88 806.88,-439.88 806.88,-439.88 812.88,-439.88 818.88,-445.88 818.88,-451.88 818.88,-451.88 818.88,-522.12 818.88,-522.12 818.88,-528.12 812.88,-534.12 806.88,-534.12"/>
<text text-anchor="start" x="686.12" y="-513.83" font-family="Helvetica,Arial,sans-serif" font-weight="bold" font-size="14.00">build_tables(plan)</text>
<text text-anchor="start" x="680.88" y="-484.57" font-family="Helvetica,Arial,sans-serif" font-size="14.00">&#45;&#45;backend memory</text>
<text text-anchor="start" x="680.88" y="-455.57" font-family="Helvetica,Arial,sans-serif" font-size="14.00">direct from plan.facts</text>
</g>
<!-- parse_plan&#45;&gt;build_pure -->
<g id="edge2" class="edge">
<title>parse_plan&#45;&gt;build_pure</title>
<path fill="none" stroke="#9c27b0" stroke-width="1.2" d="M417.17,-245.45C448.23,-285.81 513.67,-365.05 584,-415 606.47,-430.96 633.04,-444.67 657.95,-455.71"/>
<polygon fill="#9c27b0" stroke="#9c27b0" stroke-width="1.2" points="656.28,-458.8 666.85,-459.56 659.06,-452.38 656.28,-458.8"/>
<text text-anchor="middle" x="526.75" y="-402.33" font-family="Helvetica,Arial,sans-serif" font-size="9.00" fill="#555555">Backend::Memory</text>
</g>
<!-- build_storage -->
<g id="node4" class="node">
<title>build_storage</title>
<path fill="#e8f5e9" stroke="#4caf50" stroke-width="1.5" d="M877.75,-390.12C877.75,-390.12 610,-390.12 610,-390.12 604,-390.12 598,-384.12 598,-378.12 598,-378.12 598,-249.88 598,-249.88 598,-243.88 604,-237.88 610,-237.88 610,-237.88 877.75,-237.88 877.75,-237.88 883.75,-237.88 889.75,-243.88 889.75,-249.88 889.75,-249.88 889.75,-378.12 889.75,-378.12 889.75,-384.12 883.75,-390.12 877.75,-390.12"/>
<text text-anchor="start" x="620.5" y="-369.82" font-family="Helvetica,Arial,sans-serif" font-weight="bold" font-size="14.00">build_tables_via_storage&lt;S: Storage&gt;</text>
<text text-anchor="start" x="610" y="-340.57" font-family="Helvetica,Arial,sans-serif" font-size="14.00">&#45;&#45;backend memory&#45;storage</text>
<text text-anchor="start" x="610" y="-311.57" font-family="Helvetica,Arial,sans-serif" font-size="14.00">&#45;&#45;backend lmdb / redb / fjall</text>
<text text-anchor="start" x="610" y="-282.57" font-family="Helvetica,Arial,sans-serif" font-size="14.00">&#45;&#45;backend sqlite / geomerge</text>
<text text-anchor="start" x="610" y="-253.57" font-family="Helvetica,Arial,sans-serif" font-size="14.00">create_relation → tx.insert → scan_as_table</text>
</g>
<!-- parse_plan&#45;&gt;build_storage -->
<g id="edge3" class="edge">
<title>parse_plan&#45;&gt;build_storage</title>
<path fill="none" stroke="#9c27b0" stroke-width="1.2" d="M455.93,-235.73C491.85,-245.53 539.94,-258.65 586.47,-271.34"/>
<polygon fill="#9c27b0" stroke="#9c27b0" stroke-width="1.2" points="585.3,-274.65 595.87,-273.9 587.14,-267.89 585.3,-274.65"/>
<text text-anchor="middle" x="526.75" y="-269.13" font-family="Helvetica,Arial,sans-serif" font-size="9.00" fill="#555555">Backend::*Storage</text>
</g>
<!-- execute_node -->
<g id="node6" class="node">
<title>execute_node</title>
<path fill="#fff3e0" stroke="#ff9800" stroke-width="1.5" d="M1591.75,-317.62C1591.75,-317.62 1359.25,-317.62 1359.25,-317.62 1353.25,-317.62 1347.25,-311.62 1347.25,-305.62 1347.25,-305.62 1347.25,-148.38 1347.25,-148.38 1347.25,-142.38 1353.25,-136.38 1359.25,-136.38 1359.25,-136.38 1591.75,-136.38 1591.75,-136.38 1597.75,-136.38 1603.75,-142.38 1603.75,-148.38 1603.75,-148.38 1603.75,-305.62 1603.75,-305.62 1603.75,-311.62 1597.75,-317.62 1591.75,-317.62"/>
<text text-anchor="start" x="1403.88" y="-297.32" font-family="Helvetica,Arial,sans-serif" font-weight="bold" font-size="14.00">execute(tables, query)</text>
<text text-anchor="start" x="1359.25" y="-268.07" font-family="Helvetica,Arial,sans-serif" font-size="14.00">Action::Scan &#160;→ scan_atom</text>
<text text-anchor="start" x="1359.25" y="-239.07" font-family="Helvetica,Arial,sans-serif" font-size="14.00">Action::Join Left &#160;→ semijoin(l, r)</text>
<text text-anchor="start" x="1359.25" y="-210.07" font-family="Helvetica,Arial,sans-serif" font-size="14.00">Action::Join Right → semijoin(r, l)</text>
<text text-anchor="start" x="1359.25" y="-181.07" font-family="Helvetica,Arial,sans-serif" font-size="14.00">Action::Join Natural → natural_join(l, r)</text>
<text text-anchor="start" x="1359.25" y="-152.07" font-family="Helvetica,Arial,sans-serif" font-size="14.00">cache per&#45;node Relation; return root</text>
</g>
<!-- parse_plan&#45;&gt;execute_node -->
<g id="edge7" class="edge">
<title>parse_plan&#45;&gt;execute_node</title>
<path fill="none" stroke="#9c27b0" stroke-width="1.2" stroke-dasharray="5,2" d="M455.98,-217.47C492.65,-215.87 541.11,-213.78 584,-212 753.85,-204.96 796.32,-193.74 966.25,-198.5 1091.19,-202 1233.2,-210.33 1335.29,-217.09"/>
<polygon fill="#9c27b0" stroke="#9c27b0" stroke-width="1.2" points="1334.89,-220.57 1345.1,-217.74 1335.35,-213.58 1334.89,-220.57"/>
<text text-anchor="middle" x="944.5" y="-203.45" font-family="Helvetica,Arial,sans-serif" font-size="9.00" fill="#555555">plan.query</text>
</g>
<!-- verify_node -->
<g id="node8" class="node">
<title>verify_node</title>
<path fill="#f3e5f5" stroke="#9c27b0" stroke-width="1.5" d="M2200.5,-116.12C2200.5,-116.12 1958.25,-116.12 1958.25,-116.12 1952.25,-116.12 1946.25,-110.12 1946.25,-104.12 1946.25,-104.12 1946.25,-33.88 1946.25,-33.88 1946.25,-27.88 1952.25,-21.88 1958.25,-21.88 1958.25,-21.88 2200.5,-21.88 2200.5,-21.88 2206.5,-21.88 2212.5,-27.88 2212.5,-33.88 2212.5,-33.88 2212.5,-104.12 2212.5,-104.12 2212.5,-110.12 2206.5,-116.12 2200.5,-116.12"/>
<text text-anchor="start" x="2014.88" y="-95.83" font-family="Helvetica,Arial,sans-serif" font-weight="bold" font-size="14.00">verify(plan, relation)</text>
<text text-anchor="start" x="1958.25" y="-66.58" font-family="Helvetica,Arial,sans-serif" font-size="14.00">project to expected.columns</text>
<text text-anchor="start" x="1958.25" y="-37.58" font-family="Helvetica,Arial,sans-serif" font-size="14.00">multiset compare against expected.rows</text>
</g>
<!-- parse_plan&#45;&gt;verify_node -->
<g id="edge11" class="edge">
<title>parse_plan&#45;&gt;verify_node</title>
<path fill="none" stroke="#9c27b0" stroke-width="1.2" stroke-dasharray="5,2" d="M428.8,-194.65C486.38,-149.53 616.55,-60 742.88,-60 742.88,-60 742.88,-60 1776,-60 1827.65,-60 1884.23,-61.33 1934.34,-62.96"/>
<polygon fill="#9c27b0" stroke="#9c27b0" stroke-width="1.2" points="1934.01,-66.45 1944.12,-63.29 1934.24,-59.46 1934.01,-66.45"/>
<text text-anchor="middle" x="1265.5" y="-64.95" font-family="Helvetica,Arial,sans-serif" font-size="9.00" fill="#555555">plan.expected_bindings</text>
</g>
<!-- tables_map -->
<g id="node5" class="node">
<title>tables_map</title>
<path fill="#e8f4fd" stroke="#2196f3" stroke-width="1.5" d="M1171.75,-346.62C1171.75,-346.62 1011.25,-346.62 1011.25,-346.62 1005.25,-346.62 999.25,-340.62 999.25,-334.62 999.25,-334.62 999.25,-293.38 999.25,-293.38 999.25,-287.38 1005.25,-281.38 1011.25,-281.38 1011.25,-281.38 1171.75,-281.38 1171.75,-281.38 1177.75,-281.38 1183.75,-287.38 1183.75,-293.38 1183.75,-293.38 1183.75,-334.62 1183.75,-334.62 1183.75,-340.62 1177.75,-346.62 1171.75,-346.62"/>
<text text-anchor="start" x="1012.38" y="-326.32" font-family="Helvetica,Arial,sans-serif" font-weight="bold" font-size="14.00">HashMap&lt;String, Table&gt;</text>
<text text-anchor="start" x="1011.25" y="-297.07" font-family="Helvetica,Arial,sans-serif" font-size="14.00">positional rows per relation</text>
</g>
<!-- build_pure&#45;&gt;tables_map -->
<g id="edge4" class="edge">
<title>build_pure&#45;&gt;tables_map</title>
<path fill="none" stroke="#4caf50" stroke-width="1.2" d="M819.36,-450.07C841.98,-438.83 866.89,-426.43 889.75,-415 931.06,-394.35 977.08,-371.2 1014.64,-352.28"/>
<polygon fill="#4caf50" stroke="#4caf50" stroke-width="1.2" points="1016.01,-355.5 1023.37,-347.88 1012.86,-349.25 1016.01,-355.5"/>
</g>
<!-- build_storage&#45;&gt;tables_map -->
<g id="edge5" class="edge">
<title>build_storage&#45;&gt;tables_map</title>
<path fill="none" stroke="#4caf50" stroke-width="1.2" d="M890.2,-314C922.86,-314 956.86,-314 987.38,-314"/>
<polygon fill="#4caf50" stroke="#4caf50" stroke-width="1.2" points="987.08,-317.5 997.08,-314 987.08,-310.5 987.08,-317.5"/>
</g>
<!-- tables_map&#45;&gt;execute_node -->
<g id="edge6" class="edge">
<title>tables_map&#45;&gt;execute_node</title>
<path fill="none" stroke="#2196f3" stroke-width="1.2" d="M1184,-293.16C1229.35,-282.83 1285.08,-270.14 1335.56,-258.64"/>
<polygon fill="#2196f3" stroke="#2196f3" stroke-width="1.2" points="1336.18,-262.09 1345.15,-256.46 1334.62,-255.27 1336.18,-262.09"/>
</g>
<!-- relation_out -->
<g id="node7" class="node">
<title>relation_out</title>
<path fill="#fff3e0" stroke="#ff9800" stroke-width="1.5" d="M1867.25,-246.12C1867.25,-246.12 1682.75,-246.12 1682.75,-246.12 1676.75,-246.12 1670.75,-240.12 1670.75,-234.12 1670.75,-234.12 1670.75,-163.88 1670.75,-163.88 1670.75,-157.88 1676.75,-151.88 1682.75,-151.88 1682.75,-151.88 1867.25,-151.88 1867.25,-151.88 1873.25,-151.88 1879.25,-157.88 1879.25,-163.88 1879.25,-163.88 1879.25,-234.12 1879.25,-234.12 1879.25,-240.12 1873.25,-246.12 1867.25,-246.12"/>
<text text-anchor="start" x="1748.38" y="-225.82" font-family="Helvetica,Arial,sans-serif" font-weight="bold" font-size="14.00">Relation</text>
<text text-anchor="start" x="1682.75" y="-196.57" font-family="Helvetica,Arial,sans-serif" font-size="14.00">columns: variables + wildcards</text>
<text text-anchor="start" x="1682.75" y="-167.57" font-family="Helvetica,Arial,sans-serif" font-size="14.00">rows: bindings</text>
</g>
<!-- execute_node&#45;&gt;relation_out -->
<g id="edge8" class="edge">
<title>execute_node&#45;&gt;relation_out</title>
<path fill="none" stroke="#ff9800" stroke-width="1.2" d="M1603.96,-215C1622.2,-213.29 1640.88,-211.53 1658.87,-209.84"/>
<polygon fill="#ff9800" stroke="#ff9800" stroke-width="1.2" points="1659.14,-213.33 1668.76,-208.9 1658.48,-206.36 1659.14,-213.33"/>
</g>
<!-- relation_out&#45;&gt;verify_node -->
<g id="edge10" class="edge">
<title>relation_out&#45;&gt;verify_node</title>
<path fill="none" stroke="#ff9800" stroke-width="1.2" stroke-dasharray="5,2" d="M1879.64,-154.44C1904.58,-143.72 1931.45,-132.17 1957.11,-121.13"/>
<polygon fill="#ff9800" stroke="#ff9800" stroke-width="1.2" points="1958.17,-124.49 1965.98,-117.32 1955.41,-118.06 1958.17,-124.49"/>
</g>
<!-- stdout_json -->
<g id="node9" class="node">
<title>stdout_json</title>
<path fill="#eceff1" stroke="#607d8b" stroke-width="1.5" d="M2422.12,-220.62C2422.12,-220.62 2318.62,-220.62 2318.62,-220.62 2312.62,-220.62 2306.62,-214.62 2306.62,-208.62 2306.62,-208.62 2306.62,-167.38 2306.62,-167.38 2306.62,-161.38 2312.62,-155.38 2318.62,-155.38 2318.62,-155.38 2422.12,-155.38 2422.12,-155.38 2428.12,-155.38 2434.12,-161.38 2434.12,-167.38 2434.12,-167.38 2434.12,-208.62 2434.12,-208.62 2434.12,-214.62 2428.12,-220.62 2422.12,-220.62"/>
<text text-anchor="start" x="2329.12" y="-200.32" font-family="Helvetica,Arial,sans-serif" font-weight="bold" font-size="14.00">stdout JSON</text>
<text text-anchor="start" x="2318.62" y="-171.07" font-family="Helvetica,Arial,sans-serif" font-size="14.00">{ columns, rows }</text>
</g>
<!-- relation_out&#45;&gt;stdout_json -->
<g id="edge9" class="edge">
<title>relation_out&#45;&gt;stdout_json</title>
<path fill="none" stroke="#607d8b" stroke-width="1.2" d="M1879.7,-197.08C1998.37,-194.88 2189.83,-191.33 2294.73,-189.38"/>
<polygon fill="#607d8b" stroke="#607d8b" stroke-width="1.2" points="2294.71,-192.88 2304.64,-189.2 2294.58,-185.89 2294.71,-192.88"/>
</g>
<!-- oracle_pass -->
<g id="node10" class="node">
<title>oracle_pass</title>
<path fill="#eceff1" stroke="#607d8b" stroke-width="1.5" d="M2449.25,-105C2449.25,-105 2291.5,-105 2291.5,-105 2285.5,-105 2279.5,-99 2279.5,-93 2279.5,-93 2279.5,-67 2279.5,-67 2279.5,-61 2285.5,-55 2291.5,-55 2291.5,-55 2449.25,-55 2449.25,-55 2455.25,-55 2461.25,-61 2461.25,-67 2461.25,-67 2461.25,-93 2461.25,-93 2461.25,-99 2455.25,-105 2449.25,-105"/>
<text text-anchor="middle" x="2370.38" y="-87.7" font-family="Helvetica,Arial,sans-serif" font-size="14.00">Ok(true) &#160;/ &#160;VerifyError</text>
<text text-anchor="middle" x="2370.38" y="-66.7" font-family="Helvetica,Arial,sans-serif" font-size="14.00">(used by tests/examples.rs)</text>
</g>
<!-- verify_node&#45;&gt;oracle_pass -->
<g id="edge12" class="edge">
<title>verify_node&#45;&gt;oracle_pass</title>
<path fill="none" stroke="#9c27b0" stroke-width="1.2" d="M2212.61,-74.03C2231.14,-74.74 2249.95,-75.45 2267.79,-76.13"/>
<polygon fill="#9c27b0" stroke="#9c27b0" stroke-width="1.2" points="2267.42,-79.62 2277.55,-76.5 2267.69,-72.63 2267.42,-79.62"/>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 18 KiB

View File

@ -0,0 +1,114 @@
{
"_scenario": "cartesian",
"expected_bindings": {
"columns": [
"a",
"b"
],
"rows": [
[
{
"str": "left:1"
},
{
"str": "right:10"
}
],
[
{
"str": "left:1"
},
{
"str": "right:20"
}
],
[
{
"str": "left:2"
},
{
"str": "right:10"
}
],
[
{
"str": "left:2"
},
{
"str": "right:20"
}
]
]
},
"facts": {
"left": [
[
{
"str": "left:1"
}
],
[
{
"str": "left:2"
}
]
],
"right": [
[
{
"str": "right:10"
}
],
[
{
"str": "right:20"
}
]
]
},
"query": {
"nodes": [
{
"action": {
"scan": {
"columns": [
{
"var": "a"
}
],
"table": "left"
}
},
"id": 1
},
{
"action": {
"scan": {
"columns": [
{
"var": "b"
}
],
"table": "right"
}
},
"id": 2
},
{
"action": {
"join": {
"left": 1,
"op": "natural",
"right": 2
}
},
"id": 3
}
],
"root": 3
},
"schema": {
"left": 1,
"right": 1
}
}

View File

@ -0,0 +1,84 @@
{
"_scenario": "self-loop",
"expected_bindings": {
"columns": [
"x"
],
"rows": [
[
{
"str": "node:2"
}
],
[
{
"str": "node:3"
}
]
]
},
"facts": {
"edge": [
[
{
"str": "node:1"
},
{
"str": "node:2"
},
{
"str": "edge:1"
}
],
[
{
"str": "node:2"
},
{
"str": "node:2"
},
{
"str": "edge:2"
}
],
[
{
"str": "node:3"
},
{
"str": "node:3"
},
{
"str": "edge:3"
}
]
]
},
"query": {
"nodes": [
{
"action": {
"scan": {
"columns": [
{
"var": "x"
},
{
"var": "x"
},
{
"var": "_w0_2"
}
],
"table": "edge"
}
},
"id": 1
}
],
"root": 1
},
"schema": {
"edge": 3
}
}

View File

@ -0,0 +1,186 @@
{
"_scenario": "three-atom-chain",
"expected_bindings": {
"columns": [
"a",
"b",
"c"
],
"rows": [
[
{
"str": "node:1"
},
{
"str": "node:2"
},
{
"str": "node:3"
}
]
]
},
"facts": {
"edge": [
[
{
"str": "node:1"
},
{
"str": "node:2"
},
{
"str": "edge:1"
}
],
[
{
"str": "node:2"
},
{
"str": "node:3"
},
{
"str": "edge:2"
}
]
],
"node": [
[
{
"str": "node:1"
}
],
[
{
"str": "node:2"
}
],
[
{
"str": "node:3"
}
]
]
},
"query": {
"nodes": [
{
"action": {
"scan": {
"columns": [
{
"var": "a"
},
{
"var": "b"
},
{
"var": "_w0_2"
}
],
"table": "edge"
}
},
"id": 1
},
{
"action": {
"scan": {
"columns": [
{
"var": "b"
},
{
"var": "c"
},
{
"var": "_w1_2"
}
],
"table": "edge"
}
},
"id": 2
},
{
"action": {
"scan": {
"columns": [
{
"var": "a"
}
],
"table": "node"
}
},
"id": 3
},
{
"action": {
"join": {
"left": 1,
"op": "left",
"right": 3
}
},
"id": 4
},
{
"action": {
"join": {
"left": 2,
"op": "left",
"right": 4
}
},
"id": 5
},
{
"action": {
"join": {
"left": 5,
"op": "right",
"right": 4
}
},
"id": 6
},
{
"action": {
"join": {
"left": 6,
"op": "right",
"right": 3
}
},
"id": 7
},
{
"action": {
"join": {
"left": 6,
"op": "natural",
"right": 7
}
},
"id": 8
},
{
"action": {
"join": {
"left": 5,
"op": "natural",
"right": 8
}
},
"id": 9
}
],
"root": 9
},
"schema": {
"edge": 3,
"node": 1
}
}

View File

@ -0,0 +1,136 @@
{
"_scenario": "two-atom-join",
"expected_bindings": {
"columns": [
"a",
"b"
],
"rows": [
[
{
"str": "node:1"
},
{
"str": "node:2"
}
],
[
{
"str": "node:2"
},
{
"str": "node:1"
}
]
]
},
"facts": {
"edge": [
[
{
"str": "node:1"
},
{
"str": "node:2"
},
{
"str": "edge:1"
}
],
[
{
"str": "node:2"
},
{
"str": "node:1"
},
{
"str": "edge:2"
}
]
],
"node": [
[
{
"str": "node:1"
}
],
[
{
"str": "node:2"
}
]
]
},
"query": {
"nodes": [
{
"action": {
"scan": {
"columns": [
{
"var": "a"
},
{
"var": "b"
},
{
"var": "_w0_2"
}
],
"table": "edge"
}
},
"id": 1
},
{
"action": {
"scan": {
"columns": [
{
"var": "a"
}
],
"table": "node"
}
},
"id": 2
},
{
"action": {
"join": {
"left": 1,
"op": "left",
"right": 2
}
},
"id": 3
},
{
"action": {
"join": {
"left": 3,
"op": "right",
"right": 2
}
},
"id": 4
},
{
"action": {
"join": {
"left": 3,
"op": "natural",
"right": 4
}
},
"id": 5
}
],
"root": 5
},
"schema": {
"edge": 3,
"node": 1
}
}

View File

@ -0,0 +1,540 @@
//! Snapshot executor for conjunctive-query plans.
//!
//! Takes a structural plan (a DAG of `Scan` and `Join` nodes), the input
//! tables it scans, and walks the DAG via [`query_ops::atom::scan_atom`],
//! [`query_ops::join::semijoin`], and [`query_ops::join::natural_join`].
//! The result is a binding [`Relation`](query_ops::relation::Relation) over
//! the query's variables.
//!
//! The runner is intentionally backend-agnostic: it depends only on
//! `query-ops`, and the planner that emits the JSON IR is decoupled from
//! the storage backend that produced the facts. To execute a plan against
//! a [`Storage`](storage::Storage) backend, materialize each input table
//! with [`storage::scan_as_table`] and call [`execute`] with the resulting
//! map. The in-tree `tests/storage_roundtrip.rs` is the canonical example.
//!
//! The JSON IR mirrors `Geolog.DB.Plan.PlanGraph` and
//! `Geolog.DB.InMemory.QAtom` from the `external/geolog` submodule, but the
//! shape is the contract: any frontend that emits this JSON can use the
//! runner.
//!
//! Operator mapping from the Haskell planner:
//!
//! | `Geolog.DB.Plan` | this crate |
//! |-----------------------------|-----------------------------------------------|
//! | `PlanEvalAtom` | [`Action::Scan`] → `scan_atom` |
//! | `PlanJoin LeftJoin a b` | [`Action::Join`] with [`JoinOp::Left`] → `semijoin(rel[a], rel[b])` |
//! | `PlanJoin RightJoin a b` | [`Action::Join`] with [`JoinOp::Right`] → `semijoin(rel[b], rel[a])` |
//! | `PlanJoin NaturalJoin a b` | [`Action::Join`] with [`JoinOp::Natural`] → `natural_join(rel[a], rel[b])` |
//!
//! The atom side covers `evalAtom` (`Geolog.DB.InMemory`): a [`Term::Var`]
//! repeated across positions enforces equality, [`Term::Lit`] filters by
//! constant, and distinct variables project in first-occurrence order.
use std::collections::HashMap;
use serde::Deserialize;
use query_ops::atom::{AtomPattern, Term, scan_atom};
use query_ops::join::{natural_join, semijoin};
use query_ops::relation::Relation;
use storage::table::Table;
use storage::value::Value;
use storage::{Storage, StorageError, scan_as_table};
/// A single fixture: schema, ground facts, and a query plan to execute.
#[derive(Debug, Clone, Deserialize)]
pub struct Plan {
/// Relation name → arity (column count).
pub schema: HashMap<String, usize>,
/// Relation name → list of ground tuples to insert before execution.
pub facts: HashMap<String, Vec<Vec<JsonValue>>>,
/// The join DAG itself.
pub query: Query,
/// Optional oracle: if present, [`verify`] cross-checks an executed
/// [`Relation`] against this projection. The exporter lifts the
/// scenario's `expected_bindings` block into this field.
#[serde(default)]
pub expected_bindings: Option<ExpectedBindings>,
}
/// Expected query result, projected to a named subset of variables. The
/// columns named here must all appear in the runner's output; any extra
/// columns (typically per-atom wildcards) are ignored.
#[derive(Debug, Clone, Deserialize)]
pub struct ExpectedBindings {
pub columns: Vec<String>,
pub rows: Vec<Vec<JsonValue>>,
}
/// Mirrors `Geolog.DB.Plan.PlanGraph`: a set of nodes plus the id of the
/// rooted result node (the last node in topological order).
#[derive(Debug, Clone, Deserialize)]
pub struct Query {
pub root: u32,
pub nodes: Vec<Node>,
}
/// One node of the plan DAG. `id`s don't need to start at any particular
/// value, mirroring the Haskell `PlanNodeId`.
#[derive(Debug, Clone, Deserialize)]
pub struct Node {
pub id: u32,
pub action: Action,
}
/// What to compute at a node. Tagged externally so JSON reads as
/// `{"action": {"scan": {...}}}` or `{"action": {"join": {...}}}`.
#[derive(Debug, Clone, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum Action {
Scan(Atom),
Join(Join),
}
/// A flat atom pattern, one entry per column of the target relation.
/// Matches the `toFlatArgs` view used by `Geolog.DB.InMemory.evalAtom`:
/// `qaValues` positions are filled in directly, and the entity-id column
/// (if any) appears at the last position. Wildcard positions in the
/// Haskell `QAtom` (a `Map Int QVal` with a missing key) translate to a
/// fresh, unique variable name on this side, which the operator binds but
/// never joins against.
#[derive(Debug, Clone, Deserialize)]
pub struct Atom {
pub table: String,
pub columns: Vec<JsonTerm>,
}
#[derive(Debug, Clone, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum JsonTerm {
Var(String),
Lit(JsonValue),
}
/// Wire-level value tag. Restricted to what
/// [`storage::value::Value`](storage::value::Value) carries. Entity identities from
/// the Haskell side (`ValEntity path id`) round-trip through `Str` using a
/// `"path:id"` convention; that's a fixture concern, not a runner concern.
#[derive(Debug, Clone, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum JsonValue {
Int(i64),
Str(String),
}
#[derive(Debug, Clone, Deserialize)]
pub struct Join {
pub op: JoinOp,
pub left: u32,
pub right: u32,
}
#[derive(Debug, Clone, Copy, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum JoinOp {
/// `Geolog.DB.Plan.LeftJoin`: result is `left` rows whose shared columns
/// appear in `right`. Lowered to `semijoin(left, right)`.
Left,
/// `Geolog.DB.Plan.RightJoin`: result is `right` rows whose shared
/// columns appear in `left`. Lowered to `semijoin(right, left)`.
Right,
/// `Geolog.DB.Plan.NaturalJoin`. Lowered to `natural_join(left, right)`.
Natural,
}
/// Errors produced by [`verify`] when actual bindings don't match the
/// scenario's `expected_bindings` projection.
#[derive(Debug)]
pub enum VerifyError {
/// An expected column wasn't produced by the plan.
MissingColumn(String),
/// An expected row's width didn't match the column count.
ExpectedRowArity { expected: usize, got: usize },
/// The expected and actual rows (after projection) differ as multisets.
BindingsMismatch {
expected: Vec<Vec<Value>>,
actual: Vec<Vec<Value>>,
},
}
impl std::fmt::Display for VerifyError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::MissingColumn(name) => {
write!(f, "expected column {name:?} not in plan output")
}
Self::ExpectedRowArity { expected, got } => write!(
f,
"expected row has {got} cells but columns has {expected} entries"
),
Self::BindingsMismatch { expected, actual } => write!(
f,
"bindings mismatch:\n expected: {expected:?}\n actual: {actual:?}"
),
}
}
}
impl std::error::Error for VerifyError {}
/// Cross-check an executed [`Relation`] against a [`Plan`]'s
/// `expected_bindings`. Projects `actual` to the expected columns (so the
/// runner is free to surface wildcard columns the oracle doesn't name) and
/// compares as a multiset.
///
/// Returns `Ok(true)` if the plan carried an oracle and it matched,
/// `Ok(false)` if there was no oracle (caller decides whether that's an
/// error). Returns [`VerifyError`] on mismatch.
///
/// # Errors
/// See [`VerifyError`].
pub fn verify(plan: &Plan, actual: &Relation) -> Result<bool, VerifyError> {
let Some(expected) = &plan.expected_bindings else {
return Ok(false);
};
let mut projection: Vec<usize> = Vec::with_capacity(expected.columns.len());
for col in &expected.columns {
let idx = actual
.columns
.iter()
.position(|c| c == col)
.ok_or_else(|| VerifyError::MissingColumn(col.clone()))?;
projection.push(idx);
}
let mut actual_proj: Vec<Vec<Value>> = actual
.rows
.iter()
.map(|row| projection.iter().map(|&i| row[i].clone()).collect())
.collect();
let mut expected_proj: Vec<Vec<Value>> = Vec::with_capacity(expected.rows.len());
for row in &expected.rows {
if row.len() != expected.columns.len() {
return Err(VerifyError::ExpectedRowArity {
expected: expected.columns.len(),
got: row.len(),
});
}
expected_proj.push(row.iter().cloned().map(Value::from).collect());
}
// Value is not Ord; use Debug-derived sort keys to compare as a multiset.
let key = |row: &[Value]| -> String { format!("{row:?}") };
actual_proj.sort_by_key(|r| key(r));
expected_proj.sort_by_key(|r| key(r));
if actual_proj == expected_proj {
Ok(true)
} else {
Err(VerifyError::BindingsMismatch {
expected: expected_proj,
actual: actual_proj,
})
}
}
/// Errors a runner can produce during plan validation and execution.
#[derive(Debug)]
pub enum RunError {
/// A fact or scan references a relation that isn't declared in `schema`.
UnknownRelation(String),
/// A scan refers to a table that wasn't supplied in the input map.
MissingTable(String),
/// A fact row's length doesn't match the schema's declared arity.
ArityMismatch {
relation: String,
expected: usize,
got: usize,
},
/// A scan's atom pattern doesn't match the table's arity.
PatternArityMismatch {
table: String,
table_arity: usize,
pattern_arity: usize,
},
/// A join node references a node id that doesn't exist.
MissingNode(u32),
/// `Query.root` doesn't match any node in `nodes`.
MissingRoot(u32),
/// Two nodes share the same id.
DuplicateNode(u32),
/// A join node references its left or right side before that side has
/// been computed: the DAG isn't actually topologically sorted by id, or
/// it has a cycle.
UnresolvedDependency { node: u32, depends_on: u32 },
/// A [`Storage`] backend used to materialize tables returned an error.
Storage(StorageError),
}
impl From<StorageError> for RunError {
fn from(err: StorageError) -> Self {
Self::Storage(err)
}
}
impl std::fmt::Display for RunError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::UnknownRelation(name) => {
write!(f, "facts reference relation {name:?} not in schema")
}
Self::MissingTable(name) => write!(f, "scan references missing table {name:?}"),
Self::ArityMismatch {
relation,
expected,
got,
} => write!(
f,
"relation {relation:?}: row arity {got} differs from schema arity {expected}"
),
Self::PatternArityMismatch {
table,
table_arity,
pattern_arity,
} => write!(
f,
"scan of {table:?}: pattern has {pattern_arity} columns, table has {table_arity}"
),
Self::MissingNode(id) => write!(f, "plan references missing node id {id}"),
Self::MissingRoot(id) => write!(f, "plan root id {id} matches no node"),
Self::DuplicateNode(id) => write!(f, "duplicate node id {id} in plan"),
Self::UnresolvedDependency { node, depends_on } => write!(
f,
"node {node} depends on {depends_on}, which has not been computed yet"
),
Self::Storage(err) => write!(f, "storage backend error: {err}"),
}
}
}
impl std::error::Error for RunError {}
impl From<JsonValue> for Value {
fn from(jv: JsonValue) -> Self {
match jv {
JsonValue::Int(n) => Self::Int(n),
JsonValue::Str(s) => Self::Str(s),
}
}
}
impl From<JsonTerm> for Term {
fn from(t: JsonTerm) -> Self {
match t {
JsonTerm::Var(name) => Self::Var(name),
JsonTerm::Lit(value) => Self::Lit(value.into()),
}
}
}
/// Parse a [`Plan`] from a JSON string.
///
/// # Errors
/// Returns a [`serde_json::Error`] if the input isn't valid JSON in the
/// expected shape.
pub fn parse_plan(json: &str) -> Result<Plan, serde_json::Error> {
serde_json::from_str(json)
}
/// Build the input [`Table`] for each relation declared in a [`Plan`]'s
/// schema, populating rows from the plan's `facts` map. Relations with no
/// facts get an empty table at the declared arity.
///
/// # Errors
/// Returns [`RunError::UnknownRelation`] if `facts` mentions a relation
/// not in `schema`, or [`RunError::ArityMismatch`] if a row's width doesn't
/// match the declared arity.
pub fn build_tables(plan: &Plan) -> Result<HashMap<String, Table>, RunError> {
let mut tables: HashMap<String, Table> = plan
.schema
.iter()
.map(|(name, arity)| (name.clone(), Table::new(*arity)))
.collect();
for (name, rows) in &plan.facts {
let Some(table) = tables.get_mut(name) else {
return Err(RunError::UnknownRelation(name.clone()));
};
for row in rows {
if row.len() != table.arity {
return Err(RunError::ArityMismatch {
relation: name.clone(),
expected: table.arity,
got: row.len(),
});
}
let cells: Vec<Value> = row.iter().cloned().map(Value::from).collect();
table.push(cells);
}
}
Ok(tables)
}
/// Populate a [`Storage`] backend from a [`Plan`]'s schema and facts, then
/// materialize each declared relation back into an in-memory [`Table`] via
/// [`scan_as_table`]. The returned map is the same shape [`execute`]
/// consumes, so this is the storage-backed analogue of [`build_tables`].
///
/// Adding a new backend means constructing a different `S` at the call
/// site; the body here doesn't need to change.
///
/// # Errors
/// Returns [`RunError::UnknownRelation`] or [`RunError::ArityMismatch`] on
/// the same conditions as [`build_tables`], or [`RunError::Storage`] when
/// the backend itself rejects an operation.
pub fn build_tables_via_storage<S: Storage>(
plan: &Plan,
storage: &mut S,
) -> Result<HashMap<String, Table>, RunError> {
for (name, arity) in &plan.schema {
storage.create_relation(name, *arity)?;
}
{
let mut tx = storage.transaction()?;
for (name, rows) in &plan.facts {
let Some(&arity) = plan.schema.get(name) else {
return Err(RunError::UnknownRelation(name.clone()));
};
for row in rows {
if row.len() != arity {
return Err(RunError::ArityMismatch {
relation: name.clone(),
expected: arity,
got: row.len(),
});
}
let cells: Vec<Value> = row.iter().cloned().map(Value::from).collect();
tx.insert(name, cells)?;
}
}
tx.commit()?;
}
let mut tables: HashMap<String, Table> = HashMap::with_capacity(plan.schema.len());
for name in plan.schema.keys() {
let table = scan_as_table(storage as &dyn Storage, name)?;
tables.insert(name.clone(), table);
}
Ok(tables)
}
/// Execute a query DAG against the supplied input tables, returning the
/// bindings [`Relation`] for the rooted plan node.
///
/// Nodes are executed in ascending `id` order. For a Yannakakis plan as
/// emitted by `Geolog.DB.Plan` this is equivalent to a topological sort,
/// since `insertJoin` only references node ids that have already been
/// allocated. A non-monotone id ordering is rejected with
/// [`RunError::UnresolvedDependency`].
///
/// # Errors
/// Returns [`RunError::DuplicateNode`] for repeated ids,
/// [`RunError::MissingNode`] for join references to unknown ids,
/// [`RunError::MissingRoot`] if `query.root` isn't present,
/// [`RunError::MissingTable`] if a scan references a table not in the map,
/// or [`RunError::PatternArityMismatch`] if a scan's pattern doesn't match
/// the table's arity.
pub fn execute<S: std::hash::BuildHasher>(
tables: &HashMap<String, Table, S>,
query: &Query,
) -> Result<Relation, RunError> {
let mut seen_ids: std::collections::HashSet<u32> =
std::collections::HashSet::with_capacity(query.nodes.len());
for node in &query.nodes {
if !seen_ids.insert(node.id) {
return Err(RunError::DuplicateNode(node.id));
}
}
if !seen_ids.contains(&query.root) {
return Err(RunError::MissingRoot(query.root));
}
let mut ordered: Vec<&Node> = query.nodes.iter().collect();
ordered.sort_by_key(|n| n.id);
let mut results: HashMap<u32, Relation> = HashMap::with_capacity(ordered.len());
for node in ordered {
let computed = match &node.action {
Action::Scan(atom) => {
let table = tables
.get(&atom.table)
.ok_or_else(|| RunError::MissingTable(atom.table.clone()))?;
if atom.columns.len() != table.arity {
return Err(RunError::PatternArityMismatch {
table: atom.table.clone(),
table_arity: table.arity,
pattern_arity: atom.columns.len(),
});
}
let pattern = AtomPattern {
columns: atom.columns.iter().cloned().map(Term::from).collect(),
};
scan_atom(table, &pattern)
}
Action::Join(join) => {
let left = require_dep(&results, &seen_ids, node.id, join.left)?;
let right = require_dep(&results, &seen_ids, node.id, join.right)?;
match join.op {
JoinOp::Left => semijoin(left, right),
JoinOp::Right => semijoin(right, left),
JoinOp::Natural => natural_join(left, right),
}
}
};
results.insert(node.id, computed);
}
results
.remove(&query.root)
.ok_or(RunError::MissingRoot(query.root))
}
fn require_dep<'a>(
results: &'a HashMap<u32, Relation>,
seen: &std::collections::HashSet<u32>,
node: u32,
depends_on: u32,
) -> Result<&'a Relation, RunError> {
if let Some(rel) = results.get(&depends_on) {
Ok(rel)
} else if seen.contains(&depends_on) {
Err(RunError::UnresolvedDependency { node, depends_on })
} else {
Err(RunError::MissingNode(depends_on))
}
}
/// Convenience: parse JSON, build tables from the embedded facts, and
/// execute, returning the root binding relation. Equivalent to
/// `parse_plan` + [`build_tables`] + [`execute`].
///
/// # Errors
/// Returns a JSON parse error if the input is malformed, or a [`RunError`]
/// for any later step.
pub fn run_json(json: &str) -> Result<Relation, RunFromJsonError> {
let plan = parse_plan(json).map_err(RunFromJsonError::Parse)?;
let tables = build_tables(&plan).map_err(RunFromJsonError::Run)?;
let bindings = execute(&tables, &plan.query).map_err(RunFromJsonError::Run)?;
Ok(bindings)
}
/// Combined error from [`run_json`].
#[derive(Debug)]
pub enum RunFromJsonError {
Parse(serde_json::Error),
Run(RunError),
}
impl std::fmt::Display for RunFromJsonError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Parse(err) => write!(f, "parse error: {err}"),
Self::Run(err) => write!(f, "run error: {err}"),
}
}
}
impl std::error::Error for RunFromJsonError {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
match self {
Self::Parse(err) => Some(err),
Self::Run(err) => Some(err),
}
}
}

View File

@ -0,0 +1,225 @@
//! `plan-run` CLI: read a JSON plan from a file (or stdin if `-`), execute
//! it against the chosen backend, and print the resulting binding relation
//! as JSON on stdout.
//!
//! Backends:
//!
//! - `memory` (default): build tables straight from the plan's `facts`
//! block, no `Storage` trait involved. Pure in-memory path.
//! - `memory-storage`: load the same facts through `storage::MemoryStorage`
//! via the `Storage` trait, then materialize tables back out with
//! `scan_as_table` before executing.
//! - `lmdb`, `redb`, `fjall`, `sqlite`: file-backed `Storage` adapters.
//! Each invocation creates a fresh tempdir for the store and drops it on
//! exit; the runner is one-shot, so persistent paths aren't needed.
//! - `geomerge`: CRDT-backed adapter. Constructed in-memory; alpha-status
//! upstream.
use std::collections::HashMap;
use std::io::{self, Read};
use std::process::ExitCode;
use plan_runner::{JsonValue, Plan, build_tables, build_tables_via_storage, execute, parse_plan};
use storage::MemoryStorage;
use storage::adapters::fjall::FjallStorage;
use storage::adapters::geomerge::{ColumnKind, GeomergeStorage};
use storage::adapters::lmdb::LmdbStorage;
use storage::adapters::redb::RedbStorage;
use storage::adapters::sqlite::SqliteStorage;
use storage::table::Table;
use storage::value::Value;
use tempfile::TempDir;
#[derive(Debug, Clone, Copy)]
enum Backend {
Memory,
MemoryStorage,
Lmdb,
Redb,
Fjall,
Sqlite,
Geomerge,
}
impl Backend {
fn parse(s: &str) -> Option<Self> {
match s {
"memory" => Some(Self::Memory),
"memory-storage" => Some(Self::MemoryStorage),
"lmdb" => Some(Self::Lmdb),
"redb" => Some(Self::Redb),
"fjall" => Some(Self::Fjall),
"sqlite" => Some(Self::Sqlite),
"geomerge" => Some(Self::Geomerge),
_ => None,
}
}
}
const BACKENDS_HELP: &str = "memory|memory-storage|lmdb|redb|fjall|sqlite|geomerge";
fn main() -> ExitCode {
let mut backend = Backend::Memory;
let mut input_path: Option<String> = None;
let mut args = std::env::args().skip(1);
while let Some(arg) = args.next() {
match arg.as_str() {
"--backend" => {
let Some(value) = args.next() else {
eprintln!("--backend requires a value ({BACKENDS_HELP})");
return ExitCode::from(2);
};
let Some(parsed) = Backend::parse(&value) else {
eprintln!("unknown backend {value:?} (try {BACKENDS_HELP})");
return ExitCode::from(2);
};
backend = parsed;
}
other if input_path.is_none() => input_path = Some(other.to_string()),
other => {
eprintln!("unexpected argument: {other}");
return ExitCode::from(2);
}
}
}
let Some(path) = input_path else {
eprintln!("usage: plan-run [--backend {BACKENDS_HELP}] <plan.json | ->");
return ExitCode::from(2);
};
let input = match read_input(&path) {
Ok(s) => s,
Err(err) => {
eprintln!("failed to read {path}: {err}");
return ExitCode::from(1);
}
};
let plan = match parse_plan(&input) {
Ok(p) => p,
Err(err) => {
eprintln!("parse error: {err}");
return ExitCode::from(1);
}
};
let tables = match build_tables_for(&plan, backend) {
Ok(t) => t,
Err(err) => {
eprintln!("{err}");
return ExitCode::from(1);
}
};
let relation = match execute(&tables, &plan.query) {
Ok(r) => r,
Err(err) => {
eprintln!("execute error: {err}");
return ExitCode::from(1);
}
};
let payload = serde_json::json!({
"columns": relation.columns,
"rows": relation
.rows
.iter()
.map(|row| row.iter().map(value_to_json).collect::<Vec<_>>())
.collect::<Vec<_>>(),
});
println!("{payload}");
ExitCode::SUCCESS
}
/// Build the input tables for `plan` using `backend`. Path-based adapters
/// allocate a fresh tempdir; it drops at the end of this function, which is
/// safe because `build_tables_via_storage` fully materializes the tables
/// into owned `Vec<Value>` before returning.
fn build_tables_for(plan: &Plan, backend: Backend) -> Result<HashMap<String, Table>, String> {
match backend {
Backend::Memory => build_tables(plan).map_err(|e| format!("build error: {e}")),
Backend::MemoryStorage => {
let mut storage = MemoryStorage::default();
build_tables_via_storage(plan, &mut storage)
.map_err(|e| format!("build error (memory-storage): {e}"))
}
Backend::Lmdb => {
let dir = TempDir::new().map_err(|e| format!("tempdir: {e}"))?;
let mut storage = LmdbStorage::open(dir.path())
.map_err(|e| format!("failed to open lmdb backend: {e}"))?;
build_tables_via_storage(plan, &mut storage)
.map_err(|e| format!("build error (lmdb): {e}"))
}
Backend::Redb => {
let dir = TempDir::new().map_err(|e| format!("tempdir: {e}"))?;
let mut storage = RedbStorage::open(dir.path().join("data.redb"))
.map_err(|e| format!("failed to open redb backend: {e}"))?;
build_tables_via_storage(plan, &mut storage)
.map_err(|e| format!("build error (redb): {e}"))
}
Backend::Fjall => {
let dir = TempDir::new().map_err(|e| format!("tempdir: {e}"))?;
let mut storage = FjallStorage::open(dir.path())
.map_err(|e| format!("failed to open fjall backend: {e}"))?;
build_tables_via_storage(plan, &mut storage)
.map_err(|e| format!("build error (fjall): {e}"))
}
Backend::Sqlite => {
let dir = TempDir::new().map_err(|e| format!("tempdir: {e}"))?;
let mut storage = SqliteStorage::open(dir.path().join("data.sqlite"))
.map_err(|e| format!("failed to open sqlite backend: {e}"))?;
build_tables_via_storage(plan, &mut storage)
.map_err(|e| format!("build error (sqlite): {e}"))
}
Backend::Geomerge => {
let relations = plan
.schema
.iter()
.map(|(name, &arity)| (name.clone(), infer_column_kinds(plan, name, arity)));
let mut storage = GeomergeStorage::with_relations(relations)
.map_err(|e| format!("failed to open geomerge backend: {e}"))?;
build_tables_via_storage(plan, &mut storage)
.map_err(|e| format!("build error (geomerge): {e}"))
}
}
}
/// Best-effort column type inference for `geomerge`'s synthesized theory.
/// The runner IR carries only arity, so we peek at the first fact row of
/// the relation. Columns without a sample default to `String`, which
/// matches every checked-in fixture (entity identities are encoded as
/// strings by the exporter).
fn infer_column_kinds(plan: &Plan, name: &str, arity: usize) -> Vec<ColumnKind> {
let mut kinds = vec![ColumnKind::String; arity];
let Some(rows) = plan.facts.get(name) else {
return kinds;
};
let Some(first) = rows.first() else {
return kinds;
};
for (i, cell) in first.iter().take(arity).enumerate() {
kinds[i] = match cell {
JsonValue::Int(_) => ColumnKind::Int,
JsonValue::Str(_) => ColumnKind::String,
};
}
kinds
}
fn read_input(path: &str) -> io::Result<String> {
if path == "-" {
let mut buf = String::new();
io::stdin().read_to_string(&mut buf)?;
Ok(buf)
} else {
std::fs::read_to_string(path)
}
}
fn value_to_json(value: &Value) -> serde_json::Value {
match value {
Value::Int(n) => serde_json::Value::Number((*n).into()),
Value::Str(s) => serde_json::Value::String(s.clone()),
Value::Id(id) => serde_json::Value::String(id.to_string()),
}
}

View File

@ -0,0 +1,77 @@
//! Walks every JSON fixture under `crates/plan-runner/fixtures/` and
//! verifies it against the `expected_bindings` the exporter lifted from
//! the matching `tools/exporter/examples/*.scenario.json`. A fixture without an oracle
//! is reported as a failure (every checked-in fixture is expected to
//! carry one).
use std::collections::BTreeMap;
use std::fs;
use std::path::PathBuf;
use plan_runner::{parse_plan, run_json, verify};
fn fixtures_dir() -> PathBuf {
PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("fixtures")
}
fn collect_fixtures() -> BTreeMap<String, String> {
let mut out = BTreeMap::new();
for entry in fs::read_dir(fixtures_dir()).expect("read fixtures/") {
let path = entry.expect("dir entry").path();
if path.extension().and_then(|e| e.to_str()) != Some("json") {
continue;
}
let name = path
.file_stem()
.and_then(|s| s.to_str())
.expect("ascii fixture name")
.to_string();
let contents = fs::read_to_string(&path).expect("read fixture");
out.insert(name, contents);
}
out
}
#[test]
fn every_fixture_runs_and_matches_its_oracle() {
let fixtures = collect_fixtures();
assert!(
!fixtures.is_empty(),
"no fixtures found in {}",
fixtures_dir().display()
);
let mut failures: Vec<String> = Vec::new();
for (name, json) in &fixtures {
let plan = match parse_plan(json) {
Ok(p) => p,
Err(err) => {
failures.push(format!("{name}: parse error: {err}"));
continue;
}
};
if plan.expected_bindings.is_none() {
failures.push(format!("{name}: fixture has no expected_bindings"));
continue;
}
let relation = match run_json(json) {
Ok(r) => r,
Err(err) => {
failures.push(format!("{name}: run error: {err}"));
continue;
}
};
match verify(&plan, &relation) {
Ok(true) => {}
Ok(false) => failures.push(format!("{name}: verify returned no-oracle unexpectedly")),
Err(err) => failures.push(format!("{name}: {err}")),
}
}
assert!(
failures.is_empty(),
"{} fixture(s) failed:\n {}",
failures.len(),
failures.join("\n ")
);
}

View File

@ -0,0 +1,52 @@
//! Cross-checks the two paths [`plan-runner`] exposes for materializing
//! input tables: the pure [`build_tables`] path and the [`Storage`]-routed
//! [`build_tables_via_storage`] path. Same fixture, same plan, must agree
//! row-for-row.
//!
//! This is the visible proof of the layer boundary: any new `Storage`
//! backend (LMDB, fjall, geomerge) keeps this test honest by re-running it
//! with a different `S`.
use plan_runner::{build_tables, build_tables_via_storage, execute, parse_plan, run_json};
use storage::MemoryStorage;
use storage::value::Value;
const FIXTURE: &str = include_str!("../fixtures/three_atom_chain.json");
#[test]
fn storage_backed_execution_matches_pure_path() {
let plan = parse_plan(FIXTURE).expect("parse plan");
let pure_tables = build_tables(&plan).expect("build_tables");
let pure = execute(&pure_tables, &plan.query).expect("pure execute");
let mut storage = MemoryStorage::default();
let storage_tables =
build_tables_via_storage(&plan, &mut storage).expect("build_tables_via_storage");
let via_storage = execute(&storage_tables, &plan.query).expect("storage execute");
assert_eq!(pure.columns, via_storage.columns);
// Scan order between MemoryStorage and the direct-from-JSON path isn't
// required to match; compare rows as a multiset. `Value` is not `Ord`
// (it carries `RowId` and `String`), so use a Debug-derived sort key.
assert_eq!(sorted_rows(&pure.rows), sorted_rows(&via_storage.rows));
}
#[test]
fn storage_backed_execution_matches_run_json_oracle() {
let plan = parse_plan(FIXTURE).expect("parse plan");
let oracle = run_json(FIXTURE).expect("run_json");
let mut storage = MemoryStorage::default();
let tables = build_tables_via_storage(&plan, &mut storage).expect("build_tables_via_storage");
let via_storage = execute(&tables, &plan.query).expect("storage execute");
assert_eq!(oracle.columns, via_storage.columns);
assert_eq!(sorted_rows(&oracle.rows), sorted_rows(&via_storage.rows));
}
fn sorted_rows(rows: &[Vec<Value>]) -> Vec<String> {
let mut keys: Vec<String> = rows.iter().map(|r| format!("{r:?}")).collect();
keys.sort();
keys
}

View File

@ -121,7 +121,7 @@ How it works (logically):
<div align="center">
<picture>
<img alt="Types" src="docs/diagrams/workflow.svg" height="90%" width="90%%">
<img alt="Workflow" src="docs/diagrams/workflow.svg" height="90%" width="90%">
</picture>
</div>

View File

@ -24,7 +24,7 @@ penwidth = 1.2
]
table_node [label = <<table border="0" cellborder="0" cellspacing="0" cellpadding="4">
<tr><td align="center"><b>Table</b> (struct)</td></tr>
<tr><td align="center"><b>Table</b> (struct, from storage)</td></tr>
<tr><td align="left" balign="left">arity: usize</td></tr>
<tr><td align="left" balign="left">rows: Vec&lt;Vec&lt;Value&gt;&gt;</td></tr>
</table>>, fillcolor = "#E8F4FD", color = "#2196F3"]
@ -47,7 +47,7 @@ term_node [label = <<table border="0" cellborder="0" cellspacing="0" cellpadding
</table>>, fillcolor = "#F3E5F5", color = "#9C27B0"]
value_node [label = <<table border="0" cellborder="0" cellspacing="0" cellpadding="4">
<tr><td align="center"><b>Value</b> (enum)</td></tr>
<tr><td align="center"><b>Value</b> (enum, from storage)</td></tr>
<tr><td align="left" balign="left">Int(i64)</td></tr>
<tr><td align="left" balign="left">Str(String)</td></tr>
<tr><td align="left" balign="left">Id(RowId)</td></tr>

View File

@ -4,144 +4,83 @@
<!-- Generated by graphviz version 12.2.1 (0)
-->
<!-- Title: QueryOpsTypes Pages: 1 -->
<svg width="584pt" height="420pt"
viewBox="0.00 0.00 583.50 420.00" xmlns="http://www.w3.org/2000/svg">
<g id="graph0" class="graph" transform="scale(1 1) rotate(0) translate(4 416)">
<title>QueryOpsTypes</title>
<polygon fill="white" stroke="none" points="-4,4 -4,-416 579.5,-416 579.5,4 -4,4"/>
<!-- table_node -->
<g id="node1" class="node">
<title>table_node</title>
<path fill="#e8f4fd" stroke="#2196f3" stroke-width="1.5"
d="M159.75,-282.5C159.75,-282.5 12,-282.5 12,-282.5 6,-282.5 0,-276.5 0,-270.5 0,-270.5 0,-199.5 0,-199.5 0,-193.5 6,-187.5 12,-187.5 12,-187.5 159.75,-187.5 159.75,-187.5 165.75,-187.5 171.75,-193.5 171.75,-199.5 171.75,-199.5 171.75,-270.5 171.75,-270.5 171.75,-276.5 165.75,-282.5 159.75,-282.5"/>
<text text-anchor="start" x="43.88" y="-262.2" font-family="Helvetica,Arial,sans-serif" font-weight="bold"
font-size="14.00">Table
</text>
<text text-anchor="start" x="78.38" y="-262.2" font-family="Helvetica,Arial,sans-serif" font-size="14.00">
&#160;(struct)
</text>
<text text-anchor="start" x="12" y="-232.2" font-family="Helvetica,Arial,sans-serif" font-size="14.00">
arity: usize
</text>
<text text-anchor="start" x="12" y="-203.2" font-family="Helvetica,Arial,sans-serif" font-size="14.00">rows:
Vec&lt;Vec&lt;Value&gt;&gt;
</text>
</g>
<!-- value_node -->
<g id="node5" class="node">
<title>value_node</title>
<path fill="#fff3e0" stroke="#ff9800" stroke-width="1.5"
d="M351.38,-124C351.38,-124 264.38,-124 264.38,-124 258.38,-124 252.38,-118 252.38,-112 252.38,-112 252.38,-12 252.38,-12 252.38,-6 258.38,0 264.38,0 264.38,0 351.38,0 351.38,0 357.38,0 363.38,-6 363.38,-12 363.38,-12 363.38,-112 363.38,-112 363.38,-118 357.38,-124 351.38,-124"/>
<text text-anchor="start" x="264.38" y="-103.7" font-family="Helvetica,Arial,sans-serif" font-weight="bold"
font-size="14.00">Value
</text>
<text text-anchor="start" x="300.38" y="-103.7" font-family="Helvetica,Arial,sans-serif" font-size="14.00">
&#160;(enum)
</text>
<text text-anchor="start" x="264.38" y="-73.7" font-family="Helvetica,Arial,sans-serif" font-size="14.00">
Int(i64)
</text>
<text text-anchor="start" x="264.38" y="-44.7" font-family="Helvetica,Arial,sans-serif" font-size="14.00">
Str(String)
</text>
<text text-anchor="start" x="264.38" y="-15.7" font-family="Helvetica,Arial,sans-serif" font-size="14.00">
Id(RowId)
</text>
</g>
<!-- table_node&#45;&gt;value_node -->
<g id="edge3" class="edge">
<title>table_node&#45;&gt;value_node</title>
<path fill="none" stroke="#333333" stroke-width="1.2"
d="M146.83,-187.05C176.61,-164.11 212.47,-136.49 242.78,-113.14"/>
<polygon fill="#333333" stroke="#333333" stroke-width="1.2"
points="244.77,-116.02 250.56,-107.15 240.5,-110.48 244.77,-116.02"/>
<text text-anchor="middle" x="227.34" y="-153.95" font-family="Helvetica,Arial,sans-serif" font-size="9.00"
fill="#555555">Vec&lt;Vec&lt;Value&gt;&gt;
</text>
</g>
<!-- relation_node -->
<g id="node2" class="node">
<title>relation_node</title>
<path fill="#eceff1" stroke="#607d8b" stroke-width="1.5"
d="M381.75,-282.5C381.75,-282.5 234,-282.5 234,-282.5 228,-282.5 222,-276.5 222,-270.5 222,-270.5 222,-199.5 222,-199.5 222,-193.5 228,-187.5 234,-187.5 234,-187.5 381.75,-187.5 381.75,-187.5 387.75,-187.5 393.75,-193.5 393.75,-199.5 393.75,-199.5 393.75,-270.5 393.75,-270.5 393.75,-276.5 387.75,-282.5 381.75,-282.5"/>
<text text-anchor="start" x="256.5" y="-262.2" font-family="Helvetica,Arial,sans-serif" font-weight="bold"
font-size="14.00">Relation
</text>
<text text-anchor="start" x="309.75" y="-262.2" font-family="Helvetica,Arial,sans-serif" font-size="14.00">
&#160;(struct)
</text>
<text text-anchor="start" x="234" y="-232.2" font-family="Helvetica,Arial,sans-serif" font-size="14.00">
columns: Vec&lt;String&gt;
</text>
<text text-anchor="start" x="234" y="-203.2" font-family="Helvetica,Arial,sans-serif" font-size="14.00">
rows: Vec&lt;Vec&lt;Value&gt;&gt;
</text>
</g>
<!-- relation_node&#45;&gt;value_node -->
<g id="edge4" class="edge">
<title>relation_node&#45;&gt;value_node</title>
<path fill="none" stroke="#333333" stroke-width="1.2"
d="M307.88,-187.27C307.88,-171.37 307.88,-153.21 307.88,-135.76"/>
<polygon fill="#333333" stroke="#333333" stroke-width="1.2"
points="311.38,-136.25 307.88,-126.25 304.38,-136.25 311.38,-136.25"/>
<text text-anchor="middle" x="345" y="-153.95" font-family="Helvetica,Arial,sans-serif" font-size="9.00"
fill="#555555">Vec&lt;Vec&lt;Value&gt;&gt;
</text>
</g>
<!-- atom_pattern_node -->
<g id="node3" class="node">
<title>atom_pattern_node</title>
<path fill="#f3e5f5" stroke="#9c27b0" stroke-width="1.5"
d="M563.5,-412C563.5,-412 432.25,-412 432.25,-412 426.25,-412 420.25,-406 420.25,-400 420.25,-400 420.25,-358 420.25,-358 420.25,-352 426.25,-346 432.25,-346 432.25,-346 563.5,-346 563.5,-346 569.5,-346 575.5,-352 575.5,-358 575.5,-358 575.5,-400 575.5,-400 575.5,-406 569.5,-412 563.5,-412"/>
<text text-anchor="start" x="432.25" y="-391.7" font-family="Helvetica,Arial,sans-serif" font-weight="bold"
font-size="14.00">AtomPattern
</text>
<text text-anchor="start" x="514" y="-391.7" font-family="Helvetica,Arial,sans-serif" font-size="14.00">
&#160;(struct)
</text>
<text text-anchor="start" x="432.25" y="-361.7" font-family="Helvetica,Arial,sans-serif" font-size="14.00">
columns: Vec&lt;Term&gt;
</text>
</g>
<!-- term_node -->
<g id="node4" class="node">
<title>term_node</title>
<path fill="#f3e5f5" stroke="#9c27b0" stroke-width="1.5"
d="M539.88,-282.5C539.88,-282.5 455.88,-282.5 455.88,-282.5 449.88,-282.5 443.88,-276.5 443.88,-270.5 443.88,-270.5 443.88,-199.5 443.88,-199.5 443.88,-193.5 449.88,-187.5 455.88,-187.5 455.88,-187.5 539.88,-187.5 539.88,-187.5 545.88,-187.5 551.88,-193.5 551.88,-199.5 551.88,-199.5 551.88,-270.5 551.88,-270.5 551.88,-276.5 545.88,-282.5 539.88,-282.5"/>
<text text-anchor="start" x="455.88" y="-262.2" font-family="Helvetica,Arial,sans-serif" font-weight="bold"
font-size="14.00">Term
</text>
<text text-anchor="start" x="488.88" y="-262.2" font-family="Helvetica,Arial,sans-serif" font-size="14.00">
&#160;(enum)
</text>
<text text-anchor="start" x="455.88" y="-232.2" font-family="Helvetica,Arial,sans-serif" font-size="14.00">
Var(String)
</text>
<text text-anchor="start" x="455.88" y="-203.2" font-family="Helvetica,Arial,sans-serif" font-size="14.00">
Lit(Value)
</text>
</g>
<!-- atom_pattern_node&#45;&gt;term_node -->
<g id="edge1" class="edge">
<title>atom_pattern_node&#45;&gt;term_node</title>
<path fill="none" stroke="#333333" stroke-width="1.2"
d="M497.88,-345.78C497.88,-330.61 497.88,-312.04 497.88,-294.52"/>
<polygon fill="#333333" stroke="#333333" stroke-width="1.2"
points="501.38,-294.73 497.88,-284.73 494.38,-294.73 501.38,-294.73"/>
<text text-anchor="middle" x="520.75" y="-312.45" font-family="Helvetica,Arial,sans-serif" font-size="9.00"
fill="#555555">Vec&lt;Term&gt;
</text>
</g>
<!-- term_node&#45;&gt;value_node -->
<g id="edge2" class="edge">
<title>term_node&#45;&gt;value_node</title>
<path fill="none" stroke="#333333" stroke-width="1.2"
d="M445.71,-187.05C423.01,-166.62 396.18,-142.47 372.26,-120.94"/>
<polygon fill="#333333" stroke="#333333" stroke-width="1.2"
points="374.71,-118.44 364.94,-114.35 370.03,-123.65 374.71,-118.44"/>
<text text-anchor="middle" x="433.75" y="-153.95" font-family="Helvetica,Arial,sans-serif" font-size="9.00"
fill="#555555">Lit(Value)
</text>
</g>
</g>
<svg width="604pt" height="420pt"
viewBox="0.00 0.00 603.62 420.00" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<g id="graph0" class="graph" transform="scale(1 1) rotate(0) translate(4 416)">
<title>QueryOpsTypes</title>
<polygon fill="white" stroke="none" points="-4,4 -4,-416 599.62,-416 599.62,4 -4,4"/>
<!-- table_node -->
<g id="node1" class="node">
<title>table_node</title>
<path fill="#e8f4fd" stroke="#2196f3" stroke-width="1.5" d="M180,-282.5C180,-282.5 12,-282.5 12,-282.5 6,-282.5 0,-276.5 0,-270.5 0,-270.5 0,-199.5 0,-199.5 0,-193.5 6,-187.5 12,-187.5 12,-187.5 180,-187.5 180,-187.5 186,-187.5 192,-193.5 192,-199.5 192,-199.5 192,-270.5 192,-270.5 192,-276.5 186,-282.5 180,-282.5"/>
<text text-anchor="start" x="12" y="-262.2" font-family="Helvetica,Arial,sans-serif" font-weight="bold" font-size="14.00">Table</text>
<text text-anchor="start" x="46.5" y="-262.2" font-family="Helvetica,Arial,sans-serif" font-size="14.00"> &#160;(struct, from storage)</text>
<text text-anchor="start" x="12" y="-232.2" font-family="Helvetica,Arial,sans-serif" font-size="14.00">arity: usize</text>
<text text-anchor="start" x="12" y="-203.2" font-family="Helvetica,Arial,sans-serif" font-size="14.00">rows: Vec&lt;Vec&lt;Value&gt;&gt;</text>
</g>
<!-- value_node -->
<g id="node5" class="node">
<title>value_node</title>
<path fill="#fff3e0" stroke="#ff9800" stroke-width="1.5" d="M413.5,-124C413.5,-124 242.5,-124 242.5,-124 236.5,-124 230.5,-118 230.5,-112 230.5,-112 230.5,-12 230.5,-12 230.5,-6 236.5,0 242.5,0 242.5,0 413.5,0 413.5,0 419.5,0 425.5,-6 425.5,-12 425.5,-12 425.5,-112 425.5,-112 425.5,-118 419.5,-124 413.5,-124"/>
<text text-anchor="start" x="242.5" y="-103.7" font-family="Helvetica,Arial,sans-serif" font-weight="bold" font-size="14.00">Value</text>
<text text-anchor="start" x="278.5" y="-103.7" font-family="Helvetica,Arial,sans-serif" font-size="14.00"> &#160;(enum, from storage)</text>
<text text-anchor="start" x="242.5" y="-73.7" font-family="Helvetica,Arial,sans-serif" font-size="14.00">Int(i64)</text>
<text text-anchor="start" x="242.5" y="-44.7" font-family="Helvetica,Arial,sans-serif" font-size="14.00">Str(String)</text>
<text text-anchor="start" x="242.5" y="-15.7" font-family="Helvetica,Arial,sans-serif" font-size="14.00">Id(RowId)</text>
</g>
<!-- table_node&#45;&gt;value_node -->
<g id="edge3" class="edge">
<title>table_node&#45;&gt;value_node</title>
<path fill="none" stroke="#333333" stroke-width="1.2" d="M159.7,-187.05C183.12,-169.79 210.13,-149.88 235.56,-131.13"/>
<polygon fill="#333333" stroke="#333333" stroke-width="1.2" points="237.42,-134.11 243.39,-125.36 233.27,-128.48 237.42,-134.11"/>
<text text-anchor="middle" x="242.17" y="-153.95" font-family="Helvetica,Arial,sans-serif" font-size="9.00" fill="#555555">Vec&lt;Vec&lt;Value&gt;&gt;</text>
</g>
<!-- relation_node -->
<g id="node2" class="node">
<title>relation_node</title>
<path fill="#eceff1" stroke="#607d8b" stroke-width="1.5" d="M401.88,-282.5C401.88,-282.5 254.12,-282.5 254.12,-282.5 248.12,-282.5 242.12,-276.5 242.12,-270.5 242.12,-270.5 242.12,-199.5 242.12,-199.5 242.12,-193.5 248.12,-187.5 254.12,-187.5 254.12,-187.5 401.88,-187.5 401.88,-187.5 407.88,-187.5 413.88,-193.5 413.88,-199.5 413.88,-199.5 413.88,-270.5 413.88,-270.5 413.88,-276.5 407.88,-282.5 401.88,-282.5"/>
<text text-anchor="start" x="276.62" y="-262.2" font-family="Helvetica,Arial,sans-serif" font-weight="bold" font-size="14.00">Relation</text>
<text text-anchor="start" x="329.88" y="-262.2" font-family="Helvetica,Arial,sans-serif" font-size="14.00"> &#160;(struct)</text>
<text text-anchor="start" x="254.12" y="-232.2" font-family="Helvetica,Arial,sans-serif" font-size="14.00">columns: Vec&lt;String&gt;</text>
<text text-anchor="start" x="254.12" y="-203.2" font-family="Helvetica,Arial,sans-serif" font-size="14.00">rows: Vec&lt;Vec&lt;Value&gt;&gt;</text>
</g>
<!-- relation_node&#45;&gt;value_node -->
<g id="edge4" class="edge">
<title>relation_node&#45;&gt;value_node</title>
<path fill="none" stroke="#333333" stroke-width="1.2" d="M328,-187.27C328,-171.37 328,-153.21 328,-135.76"/>
<polygon fill="#333333" stroke="#333333" stroke-width="1.2" points="331.5,-136.25 328,-126.25 324.5,-136.25 331.5,-136.25"/>
<text text-anchor="middle" x="365.12" y="-153.95" font-family="Helvetica,Arial,sans-serif" font-size="9.00" fill="#555555">Vec&lt;Vec&lt;Value&gt;&gt;</text>
</g>
<!-- atom_pattern_node -->
<g id="node3" class="node">
<title>atom_pattern_node</title>
<path fill="#f3e5f5" stroke="#9c27b0" stroke-width="1.5" d="M583.62,-412C583.62,-412 452.38,-412 452.38,-412 446.38,-412 440.38,-406 440.38,-400 440.38,-400 440.38,-358 440.38,-358 440.38,-352 446.38,-346 452.38,-346 452.38,-346 583.62,-346 583.62,-346 589.62,-346 595.62,-352 595.62,-358 595.62,-358 595.62,-400 595.62,-400 595.62,-406 589.62,-412 583.62,-412"/>
<text text-anchor="start" x="452.38" y="-391.7" font-family="Helvetica,Arial,sans-serif" font-weight="bold" font-size="14.00">AtomPattern</text>
<text text-anchor="start" x="534.12" y="-391.7" font-family="Helvetica,Arial,sans-serif" font-size="14.00"> &#160;(struct)</text>
<text text-anchor="start" x="452.38" y="-361.7" font-family="Helvetica,Arial,sans-serif" font-size="14.00">columns: Vec&lt;Term&gt;</text>
</g>
<!-- term_node -->
<g id="node4" class="node">
<title>term_node</title>
<path fill="#f3e5f5" stroke="#9c27b0" stroke-width="1.5" d="M560,-282.5C560,-282.5 476,-282.5 476,-282.5 470,-282.5 464,-276.5 464,-270.5 464,-270.5 464,-199.5 464,-199.5 464,-193.5 470,-187.5 476,-187.5 476,-187.5 560,-187.5 560,-187.5 566,-187.5 572,-193.5 572,-199.5 572,-199.5 572,-270.5 572,-270.5 572,-276.5 566,-282.5 560,-282.5"/>
<text text-anchor="start" x="476" y="-262.2" font-family="Helvetica,Arial,sans-serif" font-weight="bold" font-size="14.00">Term</text>
<text text-anchor="start" x="509" y="-262.2" font-family="Helvetica,Arial,sans-serif" font-size="14.00"> &#160;(enum)</text>
<text text-anchor="start" x="476" y="-232.2" font-family="Helvetica,Arial,sans-serif" font-size="14.00">Var(String)</text>
<text text-anchor="start" x="476" y="-203.2" font-family="Helvetica,Arial,sans-serif" font-size="14.00">Lit(Value)</text>
</g>
<!-- atom_pattern_node&#45;&gt;term_node -->
<g id="edge1" class="edge">
<title>atom_pattern_node&#45;&gt;term_node</title>
<path fill="none" stroke="#333333" stroke-width="1.2" d="M518,-345.78C518,-330.61 518,-312.04 518,-294.52"/>
<polygon fill="#333333" stroke="#333333" stroke-width="1.2" points="521.5,-294.73 518,-284.73 514.5,-294.73 521.5,-294.73"/>
<text text-anchor="middle" x="540.88" y="-312.45" font-family="Helvetica,Arial,sans-serif" font-size="9.00" fill="#555555">Vec&lt;Term&gt;</text>
</g>
<!-- term_node&#45;&gt;value_node -->
<g id="edge2" class="edge">
<title>term_node&#45;&gt;value_node</title>
<path fill="none" stroke="#333333" stroke-width="1.2" d="M465.83,-187.05C446.99,-170.09 425.31,-150.58 404.8,-132.12"/>
<polygon fill="#333333" stroke="#333333" stroke-width="1.2" points="407.22,-129.59 397.44,-125.5 402.53,-134.79 407.22,-129.59"/>
<text text-anchor="middle" x="453.88" y="-153.95" font-family="Helvetica,Arial,sans-serif" font-size="9.00" fill="#555555">Lit(Value)</text>
</g>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 9.1 KiB

After

Width:  |  Height:  |  Size: 7.3 KiB

View File

@ -5,253 +5,155 @@
-->
<!-- Title: QueryOpsHandPlan Pages: 1 -->
<svg width="1482pt" height="471pt"
viewBox="0.00 0.00 1481.75 471.00" xmlns="http://www.w3.org/2000/svg">
<g id="graph0" class="graph" transform="scale(1 1) rotate(0) translate(4 467)">
<title>QueryOpsHandPlan</title>
<polygon fill="white" stroke="none" points="-4,4 -4,-467 1477.75,-467 1477.75,4 -4,4"/>
<g id="clust1" class="cluster">
<title>cluster_inputs</title>
<polygon fill="white" stroke="#888888" stroke-dasharray="5,2"
points="8,-8 8,-455 198.5,-455 198.5,-8 8,-8"/>
<text text-anchor="middle" x="103.25" y="-437.7" font-family="Helvetica,Arial,sans-serif" font-size="14.00"
fill="#555555">Inputs (positional tables)
</text>
</g>
<g id="clust2" class="cluster">
<title>cluster_atoms</title>
<polygon fill="white" stroke="#9c27b0" stroke-dasharray="5,2"
points="233.5,-12 233.5,-451 609.5,-451 609.5,-12 233.5,-12"/>
<text text-anchor="middle" x="421.5" y="-433.7" font-family="Helvetica,Arial,sans-serif" font-size="14.00"
fill="#7b1fa2">Atom Scans &#160;(scan_atom: Table × AtomPattern → Relation)
</text>
</g>
<g id="clust3" class="cluster">
<title>cluster_joins</title>
<polygon fill="white" stroke="#4caf50" stroke-dasharray="5,2"
points="665.5,-141 665.5,-322 1106,-322 1106,-141 665.5,-141"/>
<text text-anchor="middle" x="885.75" y="-304.7" font-family="Helvetica,Arial,sans-serif" font-size="14.00"
fill="#388e3c">Joins &#160;(shared cols = matching column names)
</text>
</g>
<g id="clust4" class="cluster">
<title>cluster_output</title>
<polygon fill="white" stroke="#888888" stroke-dasharray="5,2"
points="1141,-152 1141,-311 1465.75,-311 1465.75,-152 1141,-152"/>
<text text-anchor="middle" x="1303.38" y="-293.7" font-family="Helvetica,Arial,sans-serif" font-size="14.00"
fill="#555555">Output (binding relation)
</text>
</g>
<!-- author_table -->
<g id="node1" class="node">
<title>author_table</title>
<path fill="#e8f4fd" stroke="#2196f3" stroke-width="1.5"
d="M165.88,-408.12C165.88,-408.12 40.62,-408.12 40.62,-408.12 34.62,-408.12 28.62,-402.12 28.62,-396.12 28.62,-396.12 28.62,-325.88 28.62,-325.88 28.62,-319.88 34.62,-313.88 40.62,-313.88 40.62,-313.88 165.88,-313.88 165.88,-313.88 171.88,-313.88 177.88,-319.88 177.88,-325.88 177.88,-325.88 177.88,-396.12 177.88,-396.12 177.88,-402.12 171.88,-408.12 165.88,-408.12"/>
<text text-anchor="start" x="60.88" y="-387.82" font-family="Helvetica,Arial,sans-serif" font-weight="bold"
font-size="14.00">Table: author
</text>
<text text-anchor="start" x="40.62" y="-358.57" font-family="Helvetica,Arial,sans-serif" font-size="14.00">
arity 2
</text>
<text text-anchor="start" x="40.62" y="-329.57" font-family="Helvetica,Arial,sans-serif" font-size="14.00">
rows: (name, book)
</text>
</g>
<!-- author_rel -->
<g id="node4" class="node">
<title>author_rel</title>
<path fill="#f3e5f5" stroke="#9c27b0" stroke-width="1.5"
d="M509.12,-408.12C509.12,-408.12 332.88,-408.12 332.88,-408.12 326.88,-408.12 320.88,-402.12 320.88,-396.12 320.88,-396.12 320.88,-325.88 320.88,-325.88 320.88,-319.88 326.88,-313.88 332.88,-313.88 332.88,-313.88 509.12,-313.88 509.12,-313.88 515.12,-313.88 521.12,-319.88 521.12,-325.88 521.12,-325.88 521.12,-396.12 521.12,-396.12 521.12,-402.12 515.12,-408.12 509.12,-408.12"/>
<text text-anchor="start" x="388" y="-387.82" font-family="Helvetica,Arial,sans-serif" font-weight="bold"
font-size="14.00">author_rel
</text>
<text text-anchor="start" x="332.88" y="-358.57" font-family="Helvetica,Arial,sans-serif" font-size="14.00">
pattern: [Var name, Var book]
</text>
<text text-anchor="start" x="332.88" y="-329.57" font-family="Helvetica,Arial,sans-serif" font-size="14.00">
cols: [name, book]
</text>
</g>
<!-- author_table&#45;&gt;author_rel -->
<g id="edge1" class="edge">
<title>author_table&#45;&gt;author_rel</title>
<path fill="none" stroke="#2196f3" stroke-width="1.2" d="M178.28,-361C217.1,-361 265.45,-361 308.68,-361"/>
<polygon fill="#2196f3" stroke="#2196f3" stroke-width="1.2"
points="308.62,-364.5 318.62,-361 308.62,-357.5 308.62,-364.5"/>
</g>
<!-- bestseller_table -->
<g id="node2" class="node">
<title>bestseller_table</title>
<path fill="#e8f4fd" stroke="#2196f3" stroke-width="1.5"
d="M156.12,-264.12C156.12,-264.12 50.38,-264.12 50.38,-264.12 44.38,-264.12 38.38,-258.12 38.38,-252.12 38.38,-252.12 38.38,-181.88 38.38,-181.88 38.38,-175.88 44.38,-169.88 50.38,-169.88 50.38,-169.88 156.12,-169.88 156.12,-169.88 162.12,-169.88 168.12,-175.88 168.12,-181.88 168.12,-181.88 168.12,-252.12 168.12,-252.12 168.12,-258.12 162.12,-264.12 156.12,-264.12"/>
<text text-anchor="start" x="50.38" y="-243.82" font-family="Helvetica,Arial,sans-serif" font-weight="bold"
font-size="14.00">Table: bestseller
</text>
<text text-anchor="start" x="50.38" y="-214.57" font-family="Helvetica,Arial,sans-serif" font-size="14.00">
arity 1
</text>
<text text-anchor="start" x="50.38" y="-185.57" font-family="Helvetica,Arial,sans-serif" font-size="14.00">
rows: (book)
</text>
</g>
<!-- bestseller_rel -->
<g id="node5" class="node">
<title>bestseller_rel</title>
<path fill="#f3e5f5" stroke="#9c27b0" stroke-width="1.5"
d="M476.12,-264.12C476.12,-264.12 365.88,-264.12 365.88,-264.12 359.88,-264.12 353.88,-258.12 353.88,-252.12 353.88,-252.12 353.88,-181.88 353.88,-181.88 353.88,-175.88 359.88,-169.88 365.88,-169.88 365.88,-169.88 476.12,-169.88 476.12,-169.88 482.12,-169.88 488.12,-175.88 488.12,-181.88 488.12,-181.88 488.12,-252.12 488.12,-252.12 488.12,-258.12 482.12,-264.12 476.12,-264.12"/>
<text text-anchor="start" x="377.5" y="-243.82" font-family="Helvetica,Arial,sans-serif" font-weight="bold"
font-size="14.00">bestseller_rel
</text>
<text text-anchor="start" x="365.88" y="-214.57" font-family="Helvetica,Arial,sans-serif" font-size="14.00">
pattern: [Var book]
</text>
<text text-anchor="start" x="365.88" y="-185.57" font-family="Helvetica,Arial,sans-serif" font-size="14.00">
cols: [book]
</text>
</g>
<!-- bestseller_table&#45;&gt;bestseller_rel -->
<g id="edge2" class="edge">
<title>bestseller_table&#45;&gt;bestseller_rel</title>
<path fill="none" stroke="#2196f3" stroke-width="1.2" d="M168.53,-217C218.65,-217 288.47,-217 341.83,-217"/>
<polygon fill="#2196f3" stroke="#2196f3" stroke-width="1.2"
points="341.82,-220.5 351.82,-217 341.82,-213.5 341.82,-220.5"/>
</g>
<!-- price_table -->
<g id="node3" class="node">
<title>price_table</title>
<path fill="#e8f4fd" stroke="#2196f3" stroke-width="1.5"
d="M168.5,-120.12C168.5,-120.12 38,-120.12 38,-120.12 32,-120.12 26,-114.12 26,-108.12 26,-108.12 26,-37.88 26,-37.88 26,-31.88 32,-25.88 38,-25.88 38,-25.88 168.5,-25.88 168.5,-25.88 174.5,-25.88 180.5,-31.88 180.5,-37.88 180.5,-37.88 180.5,-108.12 180.5,-108.12 180.5,-114.12 174.5,-120.12 168.5,-120.12"/>
<text text-anchor="start" x="65.75" y="-99.83" font-family="Helvetica,Arial,sans-serif" font-weight="bold"
font-size="14.00">Table: price
</text>
<text text-anchor="start" x="38" y="-70.58" font-family="Helvetica,Arial,sans-serif" font-size="14.00">
arity 2
</text>
<text text-anchor="start" x="38" y="-41.58" font-family="Helvetica,Arial,sans-serif" font-size="14.00">
rows: (book, dollars)
</text>
</g>
<!-- price_rel -->
<g id="node6" class="node">
<title>price_rel</title>
<path fill="#f3e5f5" stroke="#9c27b0" stroke-width="1.5"
d="M511.75,-120.12C511.75,-120.12 330.25,-120.12 330.25,-120.12 324.25,-120.12 318.25,-114.12 318.25,-108.12 318.25,-108.12 318.25,-37.88 318.25,-37.88 318.25,-31.88 324.25,-25.88 330.25,-25.88 330.25,-25.88 511.75,-25.88 511.75,-25.88 517.75,-25.88 523.75,-31.88 523.75,-37.88 523.75,-37.88 523.75,-108.12 523.75,-108.12 523.75,-114.12 517.75,-120.12 511.75,-120.12"/>
<text text-anchor="start" x="392.88" y="-99.83" font-family="Helvetica,Arial,sans-serif" font-weight="bold"
font-size="14.00">price_rel
</text>
<text text-anchor="start" x="330.25" y="-70.58" font-family="Helvetica,Arial,sans-serif" font-size="14.00">
pattern: [Var book, Var dollars]
</text>
<text text-anchor="start" x="330.25" y="-41.58" font-family="Helvetica,Arial,sans-serif" font-size="14.00">
cols: [book, dollars]
</text>
</g>
<!-- price_table&#45;&gt;price_rel -->
<g id="edge3" class="edge">
<title>price_table&#45;&gt;price_rel</title>
<path fill="none" stroke="#2196f3" stroke-width="1.2" d="M180.68,-73C218.39,-73 264.62,-73 306.37,-73"/>
<polygon fill="#2196f3" stroke="#2196f3" stroke-width="1.2"
points="306.2,-76.5 316.2,-73 306.2,-69.5 306.2,-76.5"/>
</g>
<!-- semijoin_step -->
<g id="node7" class="node">
<title>semijoin_step</title>
<path fill="#e8f5e9" stroke="#4caf50" stroke-width="1.5"
d="M819.75,-278.62C819.75,-278.62 691.5,-278.62 691.5,-278.62 685.5,-278.62 679.5,-272.62 679.5,-266.62 679.5,-266.62 679.5,-167.38 679.5,-167.38 679.5,-161.38 685.5,-155.38 691.5,-155.38 691.5,-155.38 819.75,-155.38 819.75,-155.38 825.75,-155.38 831.75,-161.38 831.75,-167.38 831.75,-167.38 831.75,-266.62 831.75,-266.62 831.75,-272.62 825.75,-278.62 819.75,-278.62"/>
<text text-anchor="start" x="727.88" y="-258.32" font-family="Helvetica,Arial,sans-serif" font-weight="bold"
font-size="14.00">semijoin
</text>
<text text-anchor="start" x="691.5" y="-229.07" font-family="Helvetica,Arial,sans-serif" font-size="14.00">
authors of bestsellers
</text>
<text text-anchor="start" x="691.5" y="-200.07" font-family="Helvetica,Arial,sans-serif" font-size="14.00">
shared: book
</text>
<text text-anchor="start" x="691.5" y="-171.07" font-family="Helvetica,Arial,sans-serif" font-size="14.00">
cols: [name, book]
</text>
</g>
<!-- author_rel&#45;&gt;semijoin_step -->
<g id="edge4" class="edge">
<title>author_rel&#45;&gt;semijoin_step</title>
<path fill="none" stroke="#9c27b0" stroke-width="1.2"
d="M521.48,-324.79C550.11,-313.83 581.24,-301.4 609.5,-289 628.84,-280.51 649.32,-270.81 668.61,-261.33"/>
<polygon fill="#9c27b0" stroke="#9c27b0" stroke-width="1.2"
points="670.15,-264.48 677.56,-256.91 667.04,-258.2 670.15,-264.48"/>
<text text-anchor="middle" x="637.5" y="-284.9" font-family="Helvetica,Arial,sans-serif" font-size="9.00"
fill="#555555">left
</text>
</g>
<!-- bestseller_rel&#45;&gt;semijoin_step -->
<g id="edge5" class="edge">
<title>bestseller_rel&#45;&gt;semijoin_step</title>
<path fill="none" stroke="#9c27b0" stroke-width="1.2" d="M488.51,-217C539.93,-217 611.54,-217 667.53,-217"/>
<polygon fill="#9c27b0" stroke="#9c27b0" stroke-width="1.2"
points="667.41,-220.5 677.41,-217 667.41,-213.5 667.41,-220.5"/>
<text text-anchor="middle" x="637.5" y="-221.95" font-family="Helvetica,Arial,sans-serif" font-size="9.00"
fill="#555555">right
</text>
</g>
<!-- natural_join_step -->
<g id="node8" class="node">
<title>natural_join_step</title>
<path fill="#e8f5e9" stroke="#4caf50" stroke-width="1.5"
d="M1080,-278.62C1080,-278.62 922.5,-278.62 922.5,-278.62 916.5,-278.62 910.5,-272.62 910.5,-266.62 910.5,-266.62 910.5,-167.38 910.5,-167.38 910.5,-161.38 916.5,-155.38 922.5,-155.38 922.5,-155.38 1080,-155.38 1080,-155.38 1086,-155.38 1092,-161.38 1092,-167.38 1092,-167.38 1092,-266.62 1092,-266.62 1092,-272.62 1086,-278.62 1080,-278.62"/>
<text text-anchor="start" x="963" y="-258.32" font-family="Helvetica,Arial,sans-serif" font-weight="bold"
font-size="14.00">natural_join
</text>
<text text-anchor="start" x="922.5" y="-229.07" font-family="Helvetica,Arial,sans-serif" font-size="14.00">
attach each book&#39;s price
</text>
<text text-anchor="start" x="922.5" y="-200.07" font-family="Helvetica,Arial,sans-serif" font-size="14.00">
shared: book
</text>
<text text-anchor="start" x="922.5" y="-171.07" font-family="Helvetica,Arial,sans-serif" font-size="14.00">
cols: [name, book, dollars]
</text>
</g>
<!-- price_rel&#45;&gt;natural_join_step -->
<g id="edge7" class="edge">
<title>price_rel&#45;&gt;natural_join_step</title>
<path fill="none" stroke="#9c27b0" stroke-width="1.2"
d="M523.91,-71.78C608.41,-73.58 730.63,-82.79 831.75,-116.5 855.71,-124.49 879.92,-136.28 902.24,-149"/>
<polygon fill="#9c27b0" stroke="#9c27b0" stroke-width="1.2"
points="900.38,-151.97 910.78,-153.98 903.91,-145.92 900.38,-151.97"/>
<text text-anchor="middle" x="755.62" y="-121.45" font-family="Helvetica,Arial,sans-serif" font-size="9.00"
fill="#555555">right
</text>
</g>
<!-- semijoin_step&#45;&gt;natural_join_step -->
<g id="edge6" class="edge">
<title>semijoin_step&#45;&gt;natural_join_step</title>
<path fill="none" stroke="#4caf50" stroke-width="1.2" d="M832.04,-217C853.1,-217 876.34,-217 898.65,-217"/>
<polygon fill="#4caf50" stroke="#4caf50" stroke-width="1.2"
points="898.4,-220.5 908.4,-217 898.4,-213.5 898.4,-220.5"/>
<text text-anchor="middle" x="871.12" y="-221.95" font-family="Helvetica,Arial,sans-serif" font-size="9.00"
fill="#555555">left
</text>
</g>
<!-- result -->
<g id="node9" class="node">
<title>result</title>
<path fill="#eceff1" stroke="#607d8b" stroke-width="1.5"
d="M1435.75,-264.12C1435.75,-264.12 1171,-264.12 1171,-264.12 1165,-264.12 1159,-258.12 1159,-252.12 1159,-252.12 1159,-181.88 1159,-181.88 1159,-175.88 1165,-169.88 1171,-169.88 1171,-169.88 1435.75,-169.88 1435.75,-169.88 1441.75,-169.88 1447.75,-175.88 1447.75,-181.88 1447.75,-181.88 1447.75,-252.12 1447.75,-252.12 1447.75,-258.12 1441.75,-264.12 1435.75,-264.12"/>
<text text-anchor="start" x="1277.5" y="-243.82" font-family="Helvetica,Arial,sans-serif" font-weight="bold"
font-size="14.00">Q result
</text>
<text text-anchor="start" x="1171" y="-214.57" font-family="Helvetica,Arial,sans-serif" font-size="14.00">
authors of bestsellers with each book&#39;s price
</text>
<text text-anchor="start" x="1171" y="-185.57" font-family="Helvetica,Arial,sans-serif" font-size="14.00">
cols: [name, book, dollars]
</text>
</g>
<!-- natural_join_step&#45;&gt;result -->
<g id="edge8" class="edge">
<title>natural_join_step&#45;&gt;result</title>
<path fill="none" stroke="#4caf50" stroke-width="1.2"
d="M1092.3,-217C1109.6,-217 1128.17,-217 1146.86,-217"/>
<polygon fill="#4caf50" stroke="#4caf50" stroke-width="1.2"
points="1146.69,-220.5 1156.69,-217 1146.69,-213.5 1146.69,-220.5"/>
</g>
</g>
viewBox="0.00 0.00 1481.75 471.00" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<g id="graph0" class="graph" transform="scale(1 1) rotate(0) translate(4 467)">
<title>QueryOpsHandPlan</title>
<polygon fill="white" stroke="none" points="-4,4 -4,-467 1477.75,-467 1477.75,4 -4,4"/>
<g id="clust1" class="cluster">
<title>cluster_inputs</title>
<polygon fill="white" stroke="#888888" stroke-dasharray="5,2" points="8,-8 8,-455 198.5,-455 198.5,-8 8,-8"/>
<text text-anchor="middle" x="103.25" y="-437.7" font-family="Helvetica,Arial,sans-serif" font-size="14.00" fill="#555555">Inputs (positional tables)</text>
</g>
<g id="clust2" class="cluster">
<title>cluster_atoms</title>
<polygon fill="white" stroke="#9c27b0" stroke-dasharray="5,2" points="233.5,-12 233.5,-451 609.5,-451 609.5,-12 233.5,-12"/>
<text text-anchor="middle" x="421.5" y="-433.7" font-family="Helvetica,Arial,sans-serif" font-size="14.00" fill="#7b1fa2">Atom Scans &#160;(scan_atom: Table × AtomPattern → Relation)</text>
</g>
<g id="clust3" class="cluster">
<title>cluster_joins</title>
<polygon fill="white" stroke="#4caf50" stroke-dasharray="5,2" points="665.5,-141 665.5,-322 1106,-322 1106,-141 665.5,-141"/>
<text text-anchor="middle" x="885.75" y="-304.7" font-family="Helvetica,Arial,sans-serif" font-size="14.00" fill="#388e3c">Joins &#160;(shared cols = matching column names)</text>
</g>
<g id="clust4" class="cluster">
<title>cluster_output</title>
<polygon fill="white" stroke="#888888" stroke-dasharray="5,2" points="1141,-152 1141,-311 1465.75,-311 1465.75,-152 1141,-152"/>
<text text-anchor="middle" x="1303.38" y="-293.7" font-family="Helvetica,Arial,sans-serif" font-size="14.00" fill="#555555">Output (binding relation)</text>
</g>
<!-- author_table -->
<g id="node1" class="node">
<title>author_table</title>
<path fill="#e8f4fd" stroke="#2196f3" stroke-width="1.5" d="M165.88,-408.12C165.88,-408.12 40.62,-408.12 40.62,-408.12 34.62,-408.12 28.62,-402.12 28.62,-396.12 28.62,-396.12 28.62,-325.88 28.62,-325.88 28.62,-319.88 34.62,-313.88 40.62,-313.88 40.62,-313.88 165.88,-313.88 165.88,-313.88 171.88,-313.88 177.88,-319.88 177.88,-325.88 177.88,-325.88 177.88,-396.12 177.88,-396.12 177.88,-402.12 171.88,-408.12 165.88,-408.12"/>
<text text-anchor="start" x="60.88" y="-387.82" font-family="Helvetica,Arial,sans-serif" font-weight="bold" font-size="14.00">Table: author</text>
<text text-anchor="start" x="40.62" y="-358.57" font-family="Helvetica,Arial,sans-serif" font-size="14.00">• arity 2</text>
<text text-anchor="start" x="40.62" y="-329.57" font-family="Helvetica,Arial,sans-serif" font-size="14.00">• rows: (name, book)</text>
</g>
<!-- author_rel -->
<g id="node4" class="node">
<title>author_rel</title>
<path fill="#f3e5f5" stroke="#9c27b0" stroke-width="1.5" d="M509.12,-408.12C509.12,-408.12 332.88,-408.12 332.88,-408.12 326.88,-408.12 320.88,-402.12 320.88,-396.12 320.88,-396.12 320.88,-325.88 320.88,-325.88 320.88,-319.88 326.88,-313.88 332.88,-313.88 332.88,-313.88 509.12,-313.88 509.12,-313.88 515.12,-313.88 521.12,-319.88 521.12,-325.88 521.12,-325.88 521.12,-396.12 521.12,-396.12 521.12,-402.12 515.12,-408.12 509.12,-408.12"/>
<text text-anchor="start" x="388" y="-387.82" font-family="Helvetica,Arial,sans-serif" font-weight="bold" font-size="14.00">author_rel</text>
<text text-anchor="start" x="332.88" y="-358.57" font-family="Helvetica,Arial,sans-serif" font-size="14.00">pattern: [Var name, Var book]</text>
<text text-anchor="start" x="332.88" y="-329.57" font-family="Helvetica,Arial,sans-serif" font-size="14.00">cols: [name, book]</text>
</g>
<!-- author_table&#45;&gt;author_rel -->
<g id="edge1" class="edge">
<title>author_table&#45;&gt;author_rel</title>
<path fill="none" stroke="#2196f3" stroke-width="1.2" d="M178.28,-361C217.1,-361 265.45,-361 308.68,-361"/>
<polygon fill="#2196f3" stroke="#2196f3" stroke-width="1.2" points="308.62,-364.5 318.62,-361 308.62,-357.5 308.62,-364.5"/>
</g>
<!-- bestseller_table -->
<g id="node2" class="node">
<title>bestseller_table</title>
<path fill="#e8f4fd" stroke="#2196f3" stroke-width="1.5" d="M156.12,-264.12C156.12,-264.12 50.38,-264.12 50.38,-264.12 44.38,-264.12 38.38,-258.12 38.38,-252.12 38.38,-252.12 38.38,-181.88 38.38,-181.88 38.38,-175.88 44.38,-169.88 50.38,-169.88 50.38,-169.88 156.12,-169.88 156.12,-169.88 162.12,-169.88 168.12,-175.88 168.12,-181.88 168.12,-181.88 168.12,-252.12 168.12,-252.12 168.12,-258.12 162.12,-264.12 156.12,-264.12"/>
<text text-anchor="start" x="50.38" y="-243.82" font-family="Helvetica,Arial,sans-serif" font-weight="bold" font-size="14.00">Table: bestseller</text>
<text text-anchor="start" x="50.38" y="-214.57" font-family="Helvetica,Arial,sans-serif" font-size="14.00">• arity 1</text>
<text text-anchor="start" x="50.38" y="-185.57" font-family="Helvetica,Arial,sans-serif" font-size="14.00">• rows: (book)</text>
</g>
<!-- bestseller_rel -->
<g id="node5" class="node">
<title>bestseller_rel</title>
<path fill="#f3e5f5" stroke="#9c27b0" stroke-width="1.5" d="M476.12,-264.12C476.12,-264.12 365.88,-264.12 365.88,-264.12 359.88,-264.12 353.88,-258.12 353.88,-252.12 353.88,-252.12 353.88,-181.88 353.88,-181.88 353.88,-175.88 359.88,-169.88 365.88,-169.88 365.88,-169.88 476.12,-169.88 476.12,-169.88 482.12,-169.88 488.12,-175.88 488.12,-181.88 488.12,-181.88 488.12,-252.12 488.12,-252.12 488.12,-258.12 482.12,-264.12 476.12,-264.12"/>
<text text-anchor="start" x="377.5" y="-243.82" font-family="Helvetica,Arial,sans-serif" font-weight="bold" font-size="14.00">bestseller_rel</text>
<text text-anchor="start" x="365.88" y="-214.57" font-family="Helvetica,Arial,sans-serif" font-size="14.00">pattern: [Var book]</text>
<text text-anchor="start" x="365.88" y="-185.57" font-family="Helvetica,Arial,sans-serif" font-size="14.00">cols: [book]</text>
</g>
<!-- bestseller_table&#45;&gt;bestseller_rel -->
<g id="edge2" class="edge">
<title>bestseller_table&#45;&gt;bestseller_rel</title>
<path fill="none" stroke="#2196f3" stroke-width="1.2" d="M168.53,-217C218.65,-217 288.47,-217 341.83,-217"/>
<polygon fill="#2196f3" stroke="#2196f3" stroke-width="1.2" points="341.82,-220.5 351.82,-217 341.82,-213.5 341.82,-220.5"/>
</g>
<!-- price_table -->
<g id="node3" class="node">
<title>price_table</title>
<path fill="#e8f4fd" stroke="#2196f3" stroke-width="1.5" d="M168.5,-120.12C168.5,-120.12 38,-120.12 38,-120.12 32,-120.12 26,-114.12 26,-108.12 26,-108.12 26,-37.88 26,-37.88 26,-31.88 32,-25.88 38,-25.88 38,-25.88 168.5,-25.88 168.5,-25.88 174.5,-25.88 180.5,-31.88 180.5,-37.88 180.5,-37.88 180.5,-108.12 180.5,-108.12 180.5,-114.12 174.5,-120.12 168.5,-120.12"/>
<text text-anchor="start" x="65.75" y="-99.83" font-family="Helvetica,Arial,sans-serif" font-weight="bold" font-size="14.00">Table: price</text>
<text text-anchor="start" x="38" y="-70.58" font-family="Helvetica,Arial,sans-serif" font-size="14.00">• arity 2</text>
<text text-anchor="start" x="38" y="-41.58" font-family="Helvetica,Arial,sans-serif" font-size="14.00">• rows: (book, dollars)</text>
</g>
<!-- price_rel -->
<g id="node6" class="node">
<title>price_rel</title>
<path fill="#f3e5f5" stroke="#9c27b0" stroke-width="1.5" d="M511.75,-120.12C511.75,-120.12 330.25,-120.12 330.25,-120.12 324.25,-120.12 318.25,-114.12 318.25,-108.12 318.25,-108.12 318.25,-37.88 318.25,-37.88 318.25,-31.88 324.25,-25.88 330.25,-25.88 330.25,-25.88 511.75,-25.88 511.75,-25.88 517.75,-25.88 523.75,-31.88 523.75,-37.88 523.75,-37.88 523.75,-108.12 523.75,-108.12 523.75,-114.12 517.75,-120.12 511.75,-120.12"/>
<text text-anchor="start" x="392.88" y="-99.83" font-family="Helvetica,Arial,sans-serif" font-weight="bold" font-size="14.00">price_rel</text>
<text text-anchor="start" x="330.25" y="-70.58" font-family="Helvetica,Arial,sans-serif" font-size="14.00">pattern: [Var book, Var dollars]</text>
<text text-anchor="start" x="330.25" y="-41.58" font-family="Helvetica,Arial,sans-serif" font-size="14.00">cols: [book, dollars]</text>
</g>
<!-- price_table&#45;&gt;price_rel -->
<g id="edge3" class="edge">
<title>price_table&#45;&gt;price_rel</title>
<path fill="none" stroke="#2196f3" stroke-width="1.2" d="M180.68,-73C218.39,-73 264.62,-73 306.37,-73"/>
<polygon fill="#2196f3" stroke="#2196f3" stroke-width="1.2" points="306.2,-76.5 316.2,-73 306.2,-69.5 306.2,-76.5"/>
</g>
<!-- semijoin_step -->
<g id="node7" class="node">
<title>semijoin_step</title>
<path fill="#e8f5e9" stroke="#4caf50" stroke-width="1.5" d="M819.75,-278.62C819.75,-278.62 691.5,-278.62 691.5,-278.62 685.5,-278.62 679.5,-272.62 679.5,-266.62 679.5,-266.62 679.5,-167.38 679.5,-167.38 679.5,-161.38 685.5,-155.38 691.5,-155.38 691.5,-155.38 819.75,-155.38 819.75,-155.38 825.75,-155.38 831.75,-161.38 831.75,-167.38 831.75,-167.38 831.75,-266.62 831.75,-266.62 831.75,-272.62 825.75,-278.62 819.75,-278.62"/>
<text text-anchor="start" x="727.88" y="-258.32" font-family="Helvetica,Arial,sans-serif" font-weight="bold" font-size="14.00">semijoin</text>
<text text-anchor="start" x="691.5" y="-229.07" font-family="Helvetica,Arial,sans-serif" font-size="14.00">authors of bestsellers</text>
<text text-anchor="start" x="691.5" y="-200.07" font-family="Helvetica,Arial,sans-serif" font-size="14.00">shared: book</text>
<text text-anchor="start" x="691.5" y="-171.07" font-family="Helvetica,Arial,sans-serif" font-size="14.00">cols: [name, book]</text>
</g>
<!-- author_rel&#45;&gt;semijoin_step -->
<g id="edge4" class="edge">
<title>author_rel&#45;&gt;semijoin_step</title>
<path fill="none" stroke="#9c27b0" stroke-width="1.2" d="M521.48,-324.79C550.11,-313.83 581.24,-301.4 609.5,-289 628.84,-280.51 649.32,-270.81 668.61,-261.33"/>
<polygon fill="#9c27b0" stroke="#9c27b0" stroke-width="1.2" points="670.15,-264.48 677.56,-256.91 667.04,-258.2 670.15,-264.48"/>
<text text-anchor="middle" x="637.5" y="-284.9" font-family="Helvetica,Arial,sans-serif" font-size="9.00" fill="#555555">left</text>
</g>
<!-- bestseller_rel&#45;&gt;semijoin_step -->
<g id="edge5" class="edge">
<title>bestseller_rel&#45;&gt;semijoin_step</title>
<path fill="none" stroke="#9c27b0" stroke-width="1.2" d="M488.51,-217C539.93,-217 611.54,-217 667.53,-217"/>
<polygon fill="#9c27b0" stroke="#9c27b0" stroke-width="1.2" points="667.41,-220.5 677.41,-217 667.41,-213.5 667.41,-220.5"/>
<text text-anchor="middle" x="637.5" y="-221.95" font-family="Helvetica,Arial,sans-serif" font-size="9.00" fill="#555555">right</text>
</g>
<!-- natural_join_step -->
<g id="node8" class="node">
<title>natural_join_step</title>
<path fill="#e8f5e9" stroke="#4caf50" stroke-width="1.5" d="M1080,-278.62C1080,-278.62 922.5,-278.62 922.5,-278.62 916.5,-278.62 910.5,-272.62 910.5,-266.62 910.5,-266.62 910.5,-167.38 910.5,-167.38 910.5,-161.38 916.5,-155.38 922.5,-155.38 922.5,-155.38 1080,-155.38 1080,-155.38 1086,-155.38 1092,-161.38 1092,-167.38 1092,-167.38 1092,-266.62 1092,-266.62 1092,-272.62 1086,-278.62 1080,-278.62"/>
<text text-anchor="start" x="963" y="-258.32" font-family="Helvetica,Arial,sans-serif" font-weight="bold" font-size="14.00">natural_join</text>
<text text-anchor="start" x="922.5" y="-229.07" font-family="Helvetica,Arial,sans-serif" font-size="14.00">attach each book&#39;s price</text>
<text text-anchor="start" x="922.5" y="-200.07" font-family="Helvetica,Arial,sans-serif" font-size="14.00">shared: book</text>
<text text-anchor="start" x="922.5" y="-171.07" font-family="Helvetica,Arial,sans-serif" font-size="14.00">cols: [name, book, dollars]</text>
</g>
<!-- price_rel&#45;&gt;natural_join_step -->
<g id="edge7" class="edge">
<title>price_rel&#45;&gt;natural_join_step</title>
<path fill="none" stroke="#9c27b0" stroke-width="1.2" d="M523.91,-71.78C608.41,-73.58 730.63,-82.79 831.75,-116.5 855.71,-124.49 879.92,-136.28 902.24,-149"/>
<polygon fill="#9c27b0" stroke="#9c27b0" stroke-width="1.2" points="900.38,-151.97 910.78,-153.98 903.91,-145.92 900.38,-151.97"/>
<text text-anchor="middle" x="755.62" y="-121.45" font-family="Helvetica,Arial,sans-serif" font-size="9.00" fill="#555555">right</text>
</g>
<!-- semijoin_step&#45;&gt;natural_join_step -->
<g id="edge6" class="edge">
<title>semijoin_step&#45;&gt;natural_join_step</title>
<path fill="none" stroke="#4caf50" stroke-width="1.2" d="M832.04,-217C853.1,-217 876.34,-217 898.65,-217"/>
<polygon fill="#4caf50" stroke="#4caf50" stroke-width="1.2" points="898.4,-220.5 908.4,-217 898.4,-213.5 898.4,-220.5"/>
<text text-anchor="middle" x="871.12" y="-221.95" font-family="Helvetica,Arial,sans-serif" font-size="9.00" fill="#555555">left</text>
</g>
<!-- result -->
<g id="node9" class="node">
<title>result</title>
<path fill="#eceff1" stroke="#607d8b" stroke-width="1.5" d="M1435.75,-264.12C1435.75,-264.12 1171,-264.12 1171,-264.12 1165,-264.12 1159,-258.12 1159,-252.12 1159,-252.12 1159,-181.88 1159,-181.88 1159,-175.88 1165,-169.88 1171,-169.88 1171,-169.88 1435.75,-169.88 1435.75,-169.88 1441.75,-169.88 1447.75,-175.88 1447.75,-181.88 1447.75,-181.88 1447.75,-252.12 1447.75,-252.12 1447.75,-258.12 1441.75,-264.12 1435.75,-264.12"/>
<text text-anchor="start" x="1277.5" y="-243.82" font-family="Helvetica,Arial,sans-serif" font-weight="bold" font-size="14.00">Q result</text>
<text text-anchor="start" x="1171" y="-214.57" font-family="Helvetica,Arial,sans-serif" font-size="14.00">authors of bestsellers with each book&#39;s price</text>
<text text-anchor="start" x="1171" y="-185.57" font-family="Helvetica,Arial,sans-serif" font-size="14.00">cols: [name, book, dollars]</text>
</g>
<!-- natural_join_step&#45;&gt;result -->
<g id="edge8" class="edge">
<title>natural_join_step&#45;&gt;result</title>
<path fill="none" stroke="#4caf50" stroke-width="1.2" d="M1092.3,-217C1109.6,-217 1128.17,-217 1146.86,-217"/>
<polygon fill="#4caf50" stroke="#4caf50" stroke-width="1.2" points="1146.69,-220.5 1156.69,-217 1146.69,-213.5 1146.69,-220.5"/>
</g>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 17 KiB

After

Width:  |  Height:  |  Size: 14 KiB

View File

@ -2,9 +2,9 @@
//!
//! Three operators are in scope:
//!
//! - [`atom::scan_atom`] scans a [`Table`](storage::table::Table) under
//! an [`atom::AtomPattern`], filtering for repeated-variable equality and
//! literal equality, and outputs a binding [`relation::Relation`].
//! - [`atom::scan_atom`] scans a [`Table`] under an [`atom::AtomPattern`],
//! filtering for repeated-variable equality and literal equality, and
//! outputs a binding [`relation::Relation`].
//! - [`join::semijoin`] keeps rows of one relation whose shared-column values
//! appear in another.
//! - [`join::natural_join`] combines rows that agree on shared columns,
@ -14,10 +14,8 @@
//! is just an expression like
//! `natural_join(&semijoin(&a, &b), &scan_atom(&t, &p))`.
//!
//! Foundational types [`Value`](storage::value::Value) and
//! [`Table`](storage::table::Table) live in `storage`, the
//! storage-layer crate this crate is built on; storage backends produce
//! `Table`s that operators here consume.
//! `Value` and `Table` live in the `storage` crate; consumers that build
//! inputs depend on `storage` directly.
pub mod atom;
pub mod join;

View File

@ -8,7 +8,7 @@ This crates helps with decoupling the query execution logic from the underlying
### Public API
| Item | Kind | Description |
|--------------------------------------------------------------------|---------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|--------------------------------------------------------------------|---------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| `Storage` | trait | Backend-agnostic interface for storing and retrieving rows. Required methods: `create_relation`, `arity`, `scan_iter`, and `transaction`. The rest (`scan`, `scan_where`, `insert`, `delete`) have default implementations. |
| `Transaction` | trait | Atomic batch of inserts and deletes against a `Storage`. `insert` returns a pending `RowId`; `commit` consumes the boxed transaction and returns a `CommittedTx`; dropping without committing rolls back. |
| `CommittedTx` | struct | Result of a successful `Transaction::commit`. Resolves pending `RowId`s returned during the transaction to their post-commit form via `resolve`. Empty for KV adapters where pending equals real; populated for `geomerge`. |
@ -24,7 +24,8 @@ This crates helps with decoupling the query execution logic from the underlying
| `adapters::redb::RedbStorage` | struct (feat) | Single-file B-tree backed `Storage`, behind the `redb` feature. Wraps `redb::WriteTransaction` for native atomic commits. |
| `adapters::fjall::FjallStorage` | struct (feat) | LSM-tree backed `Storage`, behind the `fjall` feature. Each relation gets a partition; transactions buffer inserts and apply them on commit. |
| `adapters::lmdb::LmdbStorage` | struct (feat) | mmap'd B-tree backed `Storage`, behind the `lmdb` feature. Wraps `heed`'s `RwTxn` for native atomic commits. |
| `adapters::geomerge::GeomergeStorage` | struct (feat) | CRDT-backed `Storage` over the workspace's `geomerge` crate, behind the `geomerge` feature. Wraps `geomerge::Transaction` and resolves pending row IDs via `CommittedTx`. Deletion is not supported (append-only log). |
| `adapters::geomerge::GeomergeStorage` | struct (feat) | CRDT-backed `Storage` over the workspace's `geomerge` crate, behind the `geomerge` feature. Wraps `geomerge::Transaction` and resolves pending row IDs via `CommittedTx`. Deletion is not supported (append-only log). Construct with `from_theory`, `from_store`, or `with_relations` (synthesizes a theory from `(name, Vec<ColumnKind>)` for callers that lack a typed schema). |
| `adapters::geomerge::ColumnKind` | enum (feat) | Primitive column type fed to `GeomergeStorage::with_relations`: `Int` maps to geomerge `PrimInt`, `String` maps to `PrimString`. Exists so callers can synthesize a theory without depending on `geolog-lang::ir` directly. |
Data types and their relationships:
@ -99,7 +100,8 @@ cargo test -p storage --all-features
iterators.
- **Atomic transactions.**
For storage backends with write transactions support (LMDB, Redb, SQLite, and geomerge) we use their transaction API directly.
Adapters without native transaction support (MemoryStorage and Fjall) implement `Transaction` with an internal buffer of pending operations that are applied on `commit`.
Adapters without native transaction support (MemoryStorage and Fjall) implement `Transaction` with an internal buffer of pending operations that are
applied on `commit`.
Note that dropping a transaction without calling `commit` rolls back any pending operations.
- **Deletion support.**
Most adapters implement `delete`.

View File

@ -5,370 +5,196 @@
-->
<!-- Title: StorageTypes Pages: 1 -->
<svg width="836pt" height="1114pt"
viewBox="0.00 0.00 835.88 1114.00" xmlns="http://www.w3.org/2000/svg">
<g id="graph0" class="graph" transform="scale(1 1) rotate(0) translate(4 1110)">
<title>StorageTypes</title>
<polygon fill="white" stroke="none" points="-4,4 -4,-1110 831.88,-1110 831.88,4 -4,4"/>
<!-- storage_node -->
<g id="node1" class="node">
<title>storage_node</title>
<path fill="#e8f4fd" stroke="#2196f3" stroke-width="1.5"
d="M439.38,-802.5C439.38,-802.5 180.63,-802.5 180.63,-802.5 174.63,-802.5 168.63,-796.5 168.63,-790.5 168.63,-790.5 168.63,-545.5 168.63,-545.5 168.63,-539.5 174.63,-533.5 180.63,-533.5 180.63,-533.5 439.38,-533.5 439.38,-533.5 445.38,-533.5 451.38,-539.5 451.38,-545.5 451.38,-545.5 451.38,-790.5 451.38,-790.5 451.38,-796.5 445.38,-802.5 439.38,-802.5"/>
<text text-anchor="start" x="265.01" y="-782.2" font-family="Helvetica,Arial,sans-serif" font-weight="bold"
font-size="14.00">Storage
</text>
<text text-anchor="start" x="316.01" y="-782.2" font-family="Helvetica,Arial,sans-serif" font-size="14.00">
&#160;(trait)
</text>
<text text-anchor="start" x="180.63" y="-752.2" font-family="Helvetica,Arial,sans-serif" font-size="14.00">
create_relation(name, arity)
</text>
<text text-anchor="start" x="180.63" y="-723.2" font-family="Helvetica,Arial,sans-serif" font-size="14.00">
arity(name)
</text>
<text text-anchor="start" x="180.63" y="-694.2" font-family="Helvetica,Arial,sans-serif" font-size="14.00">
scan_iter(name) &#45;&gt; RowStream
</text>
<text text-anchor="start" x="180.63" y="-665.2" font-family="Helvetica,Arial,sans-serif" font-size="14.00">
scan(name) &#45;&gt; Vec&lt;(RowId, Vec&lt;Value&gt;)&gt;
</text>
<text text-anchor="start" x="180.63" y="-636.2" font-family="Helvetica,Arial,sans-serif" font-size="14.00">
scan_where(name, col, value)
</text>
<text text-anchor="start" x="180.63" y="-607.2" font-family="Helvetica,Arial,sans-serif" font-size="14.00">
transaction() &#45;&gt; Box&lt;dyn Transaction&gt;
</text>
<text text-anchor="start" x="180.63" y="-578.2" font-family="Helvetica,Arial,sans-serif" font-size="14.00">
insert(name, row) &#45;&gt; RowId
</text>
<text text-anchor="start" x="180.63" y="-549.2" font-family="Helvetica,Arial,sans-serif" font-size="14.00">
delete(name, id)
</text>
</g>
<!-- transaction_node -->
<g id="node2" class="node">
<title>transaction_node</title>
<path fill="#e8f5e9" stroke="#4caf50" stroke-width="1.5"
d="M245.51,-470C245.51,-470 80.51,-470 80.51,-470 74.51,-470 68.51,-464 68.51,-458 68.51,-458 68.51,-358 68.51,-358 68.51,-352 74.51,-346 80.51,-346 80.51,-346 245.51,-346 245.51,-346 251.51,-346 257.51,-352 257.51,-358 257.51,-358 257.51,-458 257.51,-458 257.51,-464 251.51,-470 245.51,-470"/>
<text text-anchor="start" x="105.63" y="-449.7" font-family="Helvetica,Arial,sans-serif" font-weight="bold"
font-size="14.00">Transaction
</text>
<text text-anchor="start" x="181.38" y="-449.7" font-family="Helvetica,Arial,sans-serif" font-size="14.00">
&#160;(trait)
</text>
<text text-anchor="start" x="80.51" y="-419.7" font-family="Helvetica,Arial,sans-serif" font-size="14.00">
insert(name, row) &#45;&gt; RowId
</text>
<text text-anchor="start" x="80.51" y="-390.7" font-family="Helvetica,Arial,sans-serif" font-size="14.00">
delete(name, id)
</text>
<text text-anchor="start" x="80.51" y="-361.7" font-family="Helvetica,Arial,sans-serif" font-size="14.00">
commit() &#45;&gt; CommittedTx
</text>
</g>
<!-- storage_node&#45;&gt;transaction_node -->
<g id="edge2" class="edge">
<title>storage_node&#45;&gt;transaction_node</title>
<path fill="none" stroke="#333333" stroke-width="1.2"
d="M233.71,-533.09C223.34,-514.9 213.06,-496.84 203.67,-480.36"/>
<polygon fill="#333333" stroke="#333333" stroke-width="1.2"
points="206.83,-478.85 198.84,-471.9 200.75,-482.32 206.83,-478.85"/>
<text text-anchor="middle" x="257.64" y="-499.95" font-family="Helvetica,Arial,sans-serif" font-size="9.00"
fill="#555555">transaction() yields
</text>
</g>
<!-- row_stream_node -->
<g id="node4" class="node">
<title>row_stream_node</title>
<path fill="#eceff1" stroke="#607d8b" stroke-width="1.5"
d="M594.26,-470C594.26,-470 319.76,-470 319.76,-470 313.76,-470 307.76,-464 307.76,-458 307.76,-458 307.76,-358 307.76,-358 307.76,-352 313.76,-346 319.76,-346 319.76,-346 594.26,-346 594.26,-346 600.26,-346 606.26,-352 606.26,-358 606.26,-358 606.26,-458 606.26,-458 606.26,-464 600.26,-470 594.26,-470"/>
<text text-anchor="start" x="368.88" y="-449.7" font-family="Helvetica,Arial,sans-serif" font-weight="bold"
font-size="14.00">RowStream&lt;&#39;a&gt;
</text>
<text text-anchor="start" x="471.63" y="-449.7" font-family="Helvetica,Arial,sans-serif" font-size="14.00">
&#160;(type alias)
</text>
<text text-anchor="start" x="319.76" y="-419.7" font-family="Helvetica,Arial,sans-serif" font-size="14.00">
Box&lt;dyn Iterator&lt;Item =
</text>
<text text-anchor="start" x="319.76" y="-390.7" font-family="Helvetica,Arial,sans-serif" font-size="14.00">
&#160;Result&lt;(RowId, Vec&lt;Value&gt;), StorageError&gt;
</text>
<text text-anchor="start" x="319.76" y="-361.7" font-family="Helvetica,Arial,sans-serif" font-size="14.00">
&gt; + &#39;a&gt;
</text>
</g>
<!-- storage_node&#45;&gt;row_stream_node -->
<g id="edge5" class="edge">
<title>storage_node&#45;&gt;row_stream_node</title>
<path fill="none" stroke="#333333" stroke-width="1.2"
d="M386.3,-533.09C396.67,-514.9 406.96,-496.84 416.34,-480.36"/>
<polygon fill="#333333" stroke="#333333" stroke-width="1.2"
points="419.26,-482.32 421.17,-471.9 413.18,-478.85 419.26,-482.32"/>
<text text-anchor="middle" x="437.14" y="-499.95" font-family="Helvetica,Arial,sans-serif" font-size="9.00"
fill="#555555">scan_iter yields
</text>
</g>
<!-- committed_tx_node -->
<g id="node3" class="node">
<title>committed_tx_node</title>
<path fill="#e8f5e9" stroke="#4caf50" stroke-width="1.5"
d="M344.88,-268C344.88,-268 113.13,-268 113.13,-268 107.13,-268 101.13,-262 101.13,-256 101.13,-256 101.13,-185 101.13,-185 101.13,-179 107.13,-173 113.13,-173 113.13,-173 344.88,-173 344.88,-173 350.88,-173 356.88,-179 356.88,-185 356.88,-185 356.88,-256 356.88,-256 356.88,-262 350.88,-268 344.88,-268"/>
<text text-anchor="start" x="160.38" y="-247.7" font-family="Helvetica,Arial,sans-serif" font-weight="bold"
font-size="14.00">CommittedTx
</text>
<text text-anchor="start" x="248.13" y="-247.7" font-family="Helvetica,Arial,sans-serif" font-size="14.00">
&#160;(struct)
</text>
<text text-anchor="start" x="113.13" y="-217.7" font-family="Helvetica,Arial,sans-serif" font-size="14.00">
resolutions: HashMap&lt;RowId, RowId&gt;
</text>
<text text-anchor="start" x="113.13" y="-188.7" font-family="Helvetica,Arial,sans-serif" font-size="14.00">
resolve(pending) &#45;&gt; RowId
</text>
</g>
<!-- transaction_node&#45;&gt;committed_tx_node -->
<g id="edge3" class="edge">
<title>transaction_node&#45;&gt;committed_tx_node</title>
<path fill="none" stroke="#333333" stroke-width="1.2"
d="M184.8,-345.75C192.41,-324.37 200.91,-300.45 208.42,-279.36"/>
<polygon fill="#333333" stroke="#333333" stroke-width="1.2"
points="211.69,-280.6 211.75,-270 205.1,-278.25 211.69,-280.6"/>
<text text-anchor="middle" x="228.73" y="-312.45" font-family="Helvetica,Arial,sans-serif" font-size="9.00"
fill="#555555">commit() yields
</text>
</g>
<!-- row_id_node -->
<g id="node5" class="node">
<title>row_id_node</title>
<path fill="#fff3e0" stroke="#ff9800" stroke-width="1.5"
d="M501.63,-95C501.63,-95 298.38,-95 298.38,-95 292.38,-95 286.38,-89 286.38,-83 286.38,-83 286.38,-12 286.38,-12 286.38,-6 292.38,0 298.38,0 298.38,0 501.63,0 501.63,0 507.63,0 513.63,-6 513.63,-12 513.63,-12 513.63,-83 513.63,-83 513.63,-89 507.63,-95 501.63,-95"/>
<text text-anchor="start" x="354.63" y="-74.7" font-family="Helvetica,Arial,sans-serif" font-weight="bold"
font-size="14.00">RowId
</text>
<text text-anchor="start" x="395.88" y="-74.7" font-family="Helvetica,Arial,sans-serif" font-size="14.00">
&#160;(struct)
</text>
<text text-anchor="start" x="298.38" y="-44.7" font-family="Helvetica,Arial,sans-serif" font-size="14.00">
SmallVec&lt;[u8; 36]&gt; (opaque)
</text>
<text text-anchor="start" x="298.38" y="-15.7" font-family="Helvetica,Arial,sans-serif" font-size="14.00">
new(bytes), as_bytes(), from(u64)
</text>
</g>
<!-- transaction_node&#45;&gt;row_id_node -->
<g id="edge8" class="edge">
<title>transaction_node&#45;&gt;row_id_node</title>
<path fill="none" stroke="#333333" stroke-width="1.2"
d="M69.82,-345.56C50.18,-327.75 32.13,-306.52 21.26,-282.5 -1.47,-232.29 -11.84,-202.57 21.26,-158.5 52.05,-117.48 176.52,-86.8 274.48,-68.4"/>
<polygon fill="#333333" stroke="#333333" stroke-width="1.2"
points="275,-71.87 284.19,-66.61 273.72,-64.98 275,-71.87"/>
<text text-anchor="middle" x="48.63" y="-218.7" font-family="Helvetica,Arial,sans-serif" font-size="9.00"
fill="#555555">insert() yields
</text>
</g>
<!-- committed_tx_node&#45;&gt;row_id_node -->
<g id="edge4" class="edge">
<title>committed_tx_node&#45;&gt;row_id_node</title>
<path fill="none" stroke="#333333" stroke-width="1.2"
d="M275.73,-172.77C297.13,-151.37 322.65,-125.86 344.97,-103.53"/>
<polygon fill="#333333" stroke="#333333" stroke-width="1.2"
points="347.16,-106.3 351.75,-96.75 342.21,-101.35 347.16,-106.3"/>
<text text-anchor="middle" x="355.87" y="-124.95" font-family="Helvetica,Arial,sans-serif" font-size="9.00"
fill="#555555">resolve() yields
</text>
</g>
<!-- row_stream_node&#45;&gt;row_id_node -->
<g id="edge6" class="edge">
<title>row_stream_node&#45;&gt;row_id_node</title>
<path fill="none" stroke="#333333" stroke-width="1.2"
d="M447.27,-345.78C436.64,-278.9 419.73,-172.56 409.28,-106.84"/>
<polygon fill="#333333" stroke="#333333" stroke-width="1.2"
points="412.78,-106.58 407.76,-97.25 405.87,-107.68 412.78,-106.58"/>
<text text-anchor="middle" x="471.44" y="-218.7" font-family="Helvetica,Arial,sans-serif" font-size="9.00"
fill="#555555">Item = (RowId, _)
</text>
</g>
<!-- value_node -->
<g id="node6" class="node">
<title>value_node</title>
<path fill="#fff3e0" stroke="#ff9800" stroke-width="1.5"
d="M675.51,-282.5C675.51,-282.5 588.51,-282.5 588.51,-282.5 582.51,-282.5 576.51,-276.5 576.51,-270.5 576.51,-270.5 576.51,-170.5 576.51,-170.5 576.51,-164.5 582.51,-158.5 588.51,-158.5 588.51,-158.5 675.51,-158.5 675.51,-158.5 681.51,-158.5 687.51,-164.5 687.51,-170.5 687.51,-170.5 687.51,-270.5 687.51,-270.5 687.51,-276.5 681.51,-282.5 675.51,-282.5"/>
<text text-anchor="start" x="588.51" y="-262.2" font-family="Helvetica,Arial,sans-serif" font-weight="bold"
font-size="14.00">Value
</text>
<text text-anchor="start" x="624.51" y="-262.2" font-family="Helvetica,Arial,sans-serif" font-size="14.00">
&#160;(enum)
</text>
<text text-anchor="start" x="588.51" y="-232.2" font-family="Helvetica,Arial,sans-serif" font-size="14.00">
Int(i64)
</text>
<text text-anchor="start" x="588.51" y="-203.2" font-family="Helvetica,Arial,sans-serif" font-size="14.00">
Str(String)
</text>
<text text-anchor="start" x="588.51" y="-174.2" font-family="Helvetica,Arial,sans-serif" font-size="14.00">
Id(RowId)
</text>
</g>
<!-- row_stream_node&#45;&gt;value_node -->
<g id="edge7" class="edge">
<title>row_stream_node&#45;&gt;value_node</title>
<path fill="none" stroke="#333333" stroke-width="1.2"
d="M514.79,-345.75C531.89,-327.63 550.7,-307.69 568.15,-289.18"/>
<polygon fill="#333333" stroke="#333333" stroke-width="1.2"
points="570.5,-291.79 574.82,-282.12 565.41,-286.99 570.5,-291.79"/>
<text text-anchor="middle" x="595.5" y="-312.45" font-family="Helvetica,Arial,sans-serif" font-size="9.00"
fill="#555555">Item = (_, Vec&lt;Value&gt;)
</text>
</g>
<!-- value_node&#45;&gt;row_id_node -->
<g id="edge9" class="edge">
<title>value_node&#45;&gt;row_id_node</title>
<path fill="none" stroke="#333333" stroke-width="1.2"
d="M576.14,-178.32C545.13,-155.47 506.2,-126.78 472.83,-102.18"/>
<polygon fill="#333333" stroke="#333333" stroke-width="1.2"
points="475.17,-99.55 465.04,-96.44 471.01,-105.19 475.17,-99.55"/>
<text text-anchor="middle" x="528.92" y="-124.95" font-family="Helvetica,Arial,sans-serif" font-size="9.00"
fill="#555555">Id(RowId)
</text>
</g>
<!-- table_node -->
<g id="node7" class="node">
<title>table_node</title>
<path fill="#e8f4fd" stroke="#2196f3" stroke-width="1.5"
d="M815.88,-455.5C815.88,-455.5 668.13,-455.5 668.13,-455.5 662.13,-455.5 656.13,-449.5 656.13,-443.5 656.13,-443.5 656.13,-372.5 656.13,-372.5 656.13,-366.5 662.13,-360.5 668.13,-360.5 668.13,-360.5 815.88,-360.5 815.88,-360.5 821.88,-360.5 827.88,-366.5 827.88,-372.5 827.88,-372.5 827.88,-443.5 827.88,-443.5 827.88,-449.5 821.88,-455.5 815.88,-455.5"/>
<text text-anchor="start" x="700.01" y="-435.2" font-family="Helvetica,Arial,sans-serif" font-weight="bold"
font-size="14.00">Table
</text>
<text text-anchor="start" x="734.51" y="-435.2" font-family="Helvetica,Arial,sans-serif" font-size="14.00">
&#160;(struct)
</text>
<text text-anchor="start" x="668.13" y="-405.2" font-family="Helvetica,Arial,sans-serif" font-size="14.00">
arity: usize
</text>
<text text-anchor="start" x="668.13" y="-376.2" font-family="Helvetica,Arial,sans-serif" font-size="14.00">
rows: Vec&lt;Vec&lt;Value&gt;&gt;
</text>
</g>
<!-- table_node&#45;&gt;value_node -->
<g id="edge10" class="edge">
<title>table_node&#45;&gt;value_node</title>
<path fill="none" stroke="#333333" stroke-width="1.2"
d="M714.25,-360.19C702.11,-339.72 687.59,-315.24 674.17,-292.6"/>
<polygon fill="#333333" stroke="#333333" stroke-width="1.2"
points="677.32,-291.06 669.21,-284.24 671.3,-294.63 677.32,-291.06"/>
<text text-anchor="middle" x="727.43" y="-312.45" font-family="Helvetica,Arial,sans-serif" font-size="9.00"
fill="#555555">Vec&lt;Vec&lt;Value&gt;&gt;
</text>
</g>
<!-- storage_error_node -->
<g id="node8" class="node">
<title>storage_error_node</title>
<path fill="#fbe9e7" stroke="#e64a19" stroke-width="1.5"
d="M718.76,-1106C718.76,-1106 531.26,-1106 531.26,-1106 525.26,-1106 519.26,-1100 519.26,-1094 519.26,-1094 519.26,-878 519.26,-878 519.26,-872 525.26,-866 531.26,-866 531.26,-866 718.76,-866 718.76,-866 724.76,-866 730.76,-872 730.76,-878 730.76,-878 730.76,-1094 730.76,-1094 730.76,-1100 724.76,-1106 718.76,-1106"/>
<text text-anchor="start" x="557.51" y="-1085.7" font-family="Helvetica,Arial,sans-serif" font-weight="bold"
font-size="14.00">StorageError
</text>
<text text-anchor="start" x="641.51" y="-1085.7" font-family="Helvetica,Arial,sans-serif" font-size="14.00">
&#160;(enum)
</text>
<text text-anchor="start" x="531.26" y="-1055.7" font-family="Helvetica,Arial,sans-serif" font-size="14.00">
RelationNotFound(String)
</text>
<text text-anchor="start" x="531.26" y="-1026.7" font-family="Helvetica,Arial,sans-serif" font-size="14.00">
RelationExists(String)
</text>
<text text-anchor="start" x="531.26" y="-997.7" font-family="Helvetica,Arial,sans-serif" font-size="14.00">
ArityMismatch { expected, got }
</text>
<text text-anchor="start" x="531.26" y="-968.7" font-family="Helvetica,Arial,sans-serif" font-size="14.00">
Validation(String)
</text>
<text text-anchor="start" x="531.26" y="-939.7" font-family="Helvetica,Arial,sans-serif" font-size="14.00">
Decode(CodecError)
</text>
<text text-anchor="start" x="531.26" y="-910.7" font-family="Helvetica,Arial,sans-serif" font-size="14.00">
Unsupported(&amp;&#39;static str)
</text>
<text text-anchor="start" x="531.26" y="-881.7" font-family="Helvetica,Arial,sans-serif" font-size="14.00">
Backend(Box&lt;dyn Error&gt;)
</text>
</g>
<!-- codec_error_node -->
<g id="node9" class="node">
<title>codec_error_node</title>
<path fill="#fbe9e7" stroke="#e64a19" stroke-width="1.5"
d="M736.76,-744.5C736.76,-744.5 513.26,-744.5 513.26,-744.5 507.26,-744.5 501.26,-738.5 501.26,-732.5 501.26,-732.5 501.26,-603.5 501.26,-603.5 501.26,-597.5 507.26,-591.5 513.26,-591.5 513.26,-591.5 736.76,-591.5 736.76,-591.5 742.76,-591.5 748.76,-597.5 748.76,-603.5 748.76,-603.5 748.76,-732.5 748.76,-732.5 748.76,-738.5 742.76,-744.5 736.76,-744.5"/>
<text text-anchor="start" x="562.38" y="-724.2" font-family="Helvetica,Arial,sans-serif" font-weight="bold"
font-size="14.00">CodecError
</text>
<text text-anchor="start" x="636.63" y="-724.2" font-family="Helvetica,Arial,sans-serif" font-size="14.00">
&#160;(enum)
</text>
<text text-anchor="start" x="513.26" y="-694.2" font-family="Helvetica,Arial,sans-serif" font-size="14.00">
UnexpectedEof
</text>
<text text-anchor="start" x="513.26" y="-665.2" font-family="Helvetica,Arial,sans-serif" font-size="14.00">
UnknownTag(u8)
</text>
<text text-anchor="start" x="513.26" y="-636.2" font-family="Helvetica,Arial,sans-serif" font-size="14.00">
LengthOverrun { declared, available }
</text>
<text text-anchor="start" x="513.26" y="-607.2" font-family="Helvetica,Arial,sans-serif" font-size="14.00">
InvalidUtf8
</text>
</g>
<!-- storage_error_node&#45;&gt;codec_error_node -->
<g id="edge11" class="edge">
<title>storage_error_node&#45;&gt;codec_error_node</title>
<path fill="none" stroke="#333333" stroke-width="1.2"
d="M625.01,-865.53C625.01,-829.26 625.01,-790.11 625.01,-756.36"/>
<polygon fill="#333333" stroke="#333333" stroke-width="1.2"
points="628.51,-756.48 625.01,-746.48 621.51,-756.48 628.51,-756.48"/>
<text text-anchor="middle" x="667.76" y="-832.45" font-family="Helvetica,Arial,sans-serif" font-size="9.00"
fill="#555555">Decode(CodecError)
</text>
</g>
<!-- adapters_node -->
<g id="node10" class="node">
<title>adapters_node</title>
<path fill="#f3e5f5" stroke="#9c27b0" stroke-width="1.5"
d="M418.38,-1091.5C418.38,-1091.5 201.63,-1091.5 201.63,-1091.5 195.63,-1091.5 189.63,-1085.5 189.63,-1079.5 189.63,-1079.5 189.63,-892.5 189.63,-892.5 189.63,-886.5 195.63,-880.5 201.63,-880.5 201.63,-880.5 418.38,-880.5 418.38,-880.5 424.38,-880.5 430.38,-886.5 430.38,-892.5 430.38,-892.5 430.38,-1079.5 430.38,-1079.5 430.38,-1085.5 424.38,-1091.5 418.38,-1091.5"/>
<text text-anchor="start" x="234.26" y="-1071.2" font-family="Helvetica,Arial,sans-serif" font-weight="bold"
font-size="14.00">Adapters
</text>
<text text-anchor="start" x="292.76" y="-1071.2" font-family="Helvetica,Arial,sans-serif" font-size="14.00">
&#160;(impl Storage)
</text>
<text text-anchor="start" x="201.63" y="-1041.2" font-family="Helvetica,Arial,sans-serif" font-size="14.00">
MemoryStorage
</text>
<text text-anchor="start" x="201.63" y="-1012.2" font-family="Helvetica,Arial,sans-serif" font-size="14.00">
SqliteStorage &#160;(feat sqlite)
</text>
<text text-anchor="start" x="201.63" y="-983.2" font-family="Helvetica,Arial,sans-serif" font-size="14.00">
RedbStorage &#160;(feat redb)
</text>
<text text-anchor="start" x="201.63" y="-954.2" font-family="Helvetica,Arial,sans-serif" font-size="14.00">
FjallStorage &#160;(feat fjall)
</text>
<text text-anchor="start" x="201.63" y="-925.2" font-family="Helvetica,Arial,sans-serif" font-size="14.00">
LmdbStorage &#160;(feat lmdb)
</text>
<text text-anchor="start" x="201.63" y="-896.2" font-family="Helvetica,Arial,sans-serif" font-size="14.00">
GeomergeStorage &#160;(feat geomerge)
</text>
</g>
<!-- adapters_node&#45;&gt;storage_node -->
<g id="edge1" class="edge">
<title>adapters_node&#45;&gt;storage_node</title>
<path fill="none" stroke="#333333" stroke-width="1.2" stroke-dasharray="5,2"
d="M310.01,-880.22C310.01,-859.1 310.01,-836.56 310.01,-814.34"/>
<polygon fill="#333333" stroke="#333333" stroke-width="1.2"
points="313.51,-814.58 310.01,-804.58 306.51,-814.58 313.51,-814.58"/>
<text text-anchor="middle" x="318.63" y="-832.45" font-family="Helvetica,Arial,sans-serif" font-size="9.00"
fill="#555555">impl
</text>
</g>
</g>
viewBox="0.00 0.00 835.88 1114.00" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<g id="graph0" class="graph" transform="scale(1 1) rotate(0) translate(4 1110)">
<title>StorageTypes</title>
<polygon fill="white" stroke="none" points="-4,4 -4,-1110 831.88,-1110 831.88,4 -4,4"/>
<!-- storage_node -->
<g id="node1" class="node">
<title>storage_node</title>
<path fill="#e8f4fd" stroke="#2196f3" stroke-width="1.5" d="M439.38,-802.5C439.38,-802.5 180.63,-802.5 180.63,-802.5 174.63,-802.5 168.63,-796.5 168.63,-790.5 168.63,-790.5 168.63,-545.5 168.63,-545.5 168.63,-539.5 174.63,-533.5 180.63,-533.5 180.63,-533.5 439.38,-533.5 439.38,-533.5 445.38,-533.5 451.38,-539.5 451.38,-545.5 451.38,-545.5 451.38,-790.5 451.38,-790.5 451.38,-796.5 445.38,-802.5 439.38,-802.5"/>
<text text-anchor="start" x="265.01" y="-782.2" font-family="Helvetica,Arial,sans-serif" font-weight="bold" font-size="14.00">Storage</text>
<text text-anchor="start" x="316.01" y="-782.2" font-family="Helvetica,Arial,sans-serif" font-size="14.00"> &#160;(trait)</text>
<text text-anchor="start" x="180.63" y="-752.2" font-family="Helvetica,Arial,sans-serif" font-size="14.00">create_relation(name, arity)</text>
<text text-anchor="start" x="180.63" y="-723.2" font-family="Helvetica,Arial,sans-serif" font-size="14.00">arity(name)</text>
<text text-anchor="start" x="180.63" y="-694.2" font-family="Helvetica,Arial,sans-serif" font-size="14.00">scan_iter(name) &#45;&gt; RowStream</text>
<text text-anchor="start" x="180.63" y="-665.2" font-family="Helvetica,Arial,sans-serif" font-size="14.00">scan(name) &#45;&gt; Vec&lt;(RowId, Vec&lt;Value&gt;)&gt;</text>
<text text-anchor="start" x="180.63" y="-636.2" font-family="Helvetica,Arial,sans-serif" font-size="14.00">scan_where(name, col, value)</text>
<text text-anchor="start" x="180.63" y="-607.2" font-family="Helvetica,Arial,sans-serif" font-size="14.00">transaction() &#45;&gt; Box&lt;dyn Transaction&gt;</text>
<text text-anchor="start" x="180.63" y="-578.2" font-family="Helvetica,Arial,sans-serif" font-size="14.00">insert(name, row) &#45;&gt; RowId</text>
<text text-anchor="start" x="180.63" y="-549.2" font-family="Helvetica,Arial,sans-serif" font-size="14.00">delete(name, id)</text>
</g>
<!-- transaction_node -->
<g id="node2" class="node">
<title>transaction_node</title>
<path fill="#e8f5e9" stroke="#4caf50" stroke-width="1.5" d="M245.51,-470C245.51,-470 80.51,-470 80.51,-470 74.51,-470 68.51,-464 68.51,-458 68.51,-458 68.51,-358 68.51,-358 68.51,-352 74.51,-346 80.51,-346 80.51,-346 245.51,-346 245.51,-346 251.51,-346 257.51,-352 257.51,-358 257.51,-358 257.51,-458 257.51,-458 257.51,-464 251.51,-470 245.51,-470"/>
<text text-anchor="start" x="105.63" y="-449.7" font-family="Helvetica,Arial,sans-serif" font-weight="bold" font-size="14.00">Transaction</text>
<text text-anchor="start" x="181.38" y="-449.7" font-family="Helvetica,Arial,sans-serif" font-size="14.00"> &#160;(trait)</text>
<text text-anchor="start" x="80.51" y="-419.7" font-family="Helvetica,Arial,sans-serif" font-size="14.00">insert(name, row) &#45;&gt; RowId</text>
<text text-anchor="start" x="80.51" y="-390.7" font-family="Helvetica,Arial,sans-serif" font-size="14.00">delete(name, id)</text>
<text text-anchor="start" x="80.51" y="-361.7" font-family="Helvetica,Arial,sans-serif" font-size="14.00">commit() &#45;&gt; CommittedTx</text>
</g>
<!-- storage_node&#45;&gt;transaction_node -->
<g id="edge2" class="edge">
<title>storage_node&#45;&gt;transaction_node</title>
<path fill="none" stroke="#333333" stroke-width="1.2" d="M233.71,-533.09C223.34,-514.9 213.06,-496.84 203.67,-480.36"/>
<polygon fill="#333333" stroke="#333333" stroke-width="1.2" points="206.83,-478.85 198.84,-471.9 200.75,-482.32 206.83,-478.85"/>
<text text-anchor="middle" x="257.64" y="-499.95" font-family="Helvetica,Arial,sans-serif" font-size="9.00" fill="#555555">transaction() yields</text>
</g>
<!-- row_stream_node -->
<g id="node4" class="node">
<title>row_stream_node</title>
<path fill="#eceff1" stroke="#607d8b" stroke-width="1.5" d="M594.26,-470C594.26,-470 319.76,-470 319.76,-470 313.76,-470 307.76,-464 307.76,-458 307.76,-458 307.76,-358 307.76,-358 307.76,-352 313.76,-346 319.76,-346 319.76,-346 594.26,-346 594.26,-346 600.26,-346 606.26,-352 606.26,-358 606.26,-358 606.26,-458 606.26,-458 606.26,-464 600.26,-470 594.26,-470"/>
<text text-anchor="start" x="368.88" y="-449.7" font-family="Helvetica,Arial,sans-serif" font-weight="bold" font-size="14.00">RowStream&lt;&#39;a&gt;</text>
<text text-anchor="start" x="471.63" y="-449.7" font-family="Helvetica,Arial,sans-serif" font-size="14.00"> &#160;(type alias)</text>
<text text-anchor="start" x="319.76" y="-419.7" font-family="Helvetica,Arial,sans-serif" font-size="14.00">Box&lt;dyn Iterator&lt;Item =</text>
<text text-anchor="start" x="319.76" y="-390.7" font-family="Helvetica,Arial,sans-serif" font-size="14.00"> &#160;Result&lt;(RowId, Vec&lt;Value&gt;), StorageError&gt;</text>
<text text-anchor="start" x="319.76" y="-361.7" font-family="Helvetica,Arial,sans-serif" font-size="14.00">&gt; + &#39;a&gt;</text>
</g>
<!-- storage_node&#45;&gt;row_stream_node -->
<g id="edge5" class="edge">
<title>storage_node&#45;&gt;row_stream_node</title>
<path fill="none" stroke="#333333" stroke-width="1.2" d="M386.3,-533.09C396.67,-514.9 406.96,-496.84 416.34,-480.36"/>
<polygon fill="#333333" stroke="#333333" stroke-width="1.2" points="419.26,-482.32 421.17,-471.9 413.18,-478.85 419.26,-482.32"/>
<text text-anchor="middle" x="437.14" y="-499.95" font-family="Helvetica,Arial,sans-serif" font-size="9.00" fill="#555555">scan_iter yields</text>
</g>
<!-- committed_tx_node -->
<g id="node3" class="node">
<title>committed_tx_node</title>
<path fill="#e8f5e9" stroke="#4caf50" stroke-width="1.5" d="M344.88,-268C344.88,-268 113.13,-268 113.13,-268 107.13,-268 101.13,-262 101.13,-256 101.13,-256 101.13,-185 101.13,-185 101.13,-179 107.13,-173 113.13,-173 113.13,-173 344.88,-173 344.88,-173 350.88,-173 356.88,-179 356.88,-185 356.88,-185 356.88,-256 356.88,-256 356.88,-262 350.88,-268 344.88,-268"/>
<text text-anchor="start" x="160.38" y="-247.7" font-family="Helvetica,Arial,sans-serif" font-weight="bold" font-size="14.00">CommittedTx</text>
<text text-anchor="start" x="248.13" y="-247.7" font-family="Helvetica,Arial,sans-serif" font-size="14.00"> &#160;(struct)</text>
<text text-anchor="start" x="113.13" y="-217.7" font-family="Helvetica,Arial,sans-serif" font-size="14.00">resolutions: HashMap&lt;RowId, RowId&gt;</text>
<text text-anchor="start" x="113.13" y="-188.7" font-family="Helvetica,Arial,sans-serif" font-size="14.00">resolve(pending) &#45;&gt; RowId</text>
</g>
<!-- transaction_node&#45;&gt;committed_tx_node -->
<g id="edge3" class="edge">
<title>transaction_node&#45;&gt;committed_tx_node</title>
<path fill="none" stroke="#333333" stroke-width="1.2" d="M184.8,-345.75C192.41,-324.37 200.91,-300.45 208.42,-279.36"/>
<polygon fill="#333333" stroke="#333333" stroke-width="1.2" points="211.69,-280.6 211.75,-270 205.1,-278.25 211.69,-280.6"/>
<text text-anchor="middle" x="228.73" y="-312.45" font-family="Helvetica,Arial,sans-serif" font-size="9.00" fill="#555555">commit() yields</text>
</g>
<!-- row_id_node -->
<g id="node5" class="node">
<title>row_id_node</title>
<path fill="#fff3e0" stroke="#ff9800" stroke-width="1.5" d="M501.63,-95C501.63,-95 298.38,-95 298.38,-95 292.38,-95 286.38,-89 286.38,-83 286.38,-83 286.38,-12 286.38,-12 286.38,-6 292.38,0 298.38,0 298.38,0 501.63,0 501.63,0 507.63,0 513.63,-6 513.63,-12 513.63,-12 513.63,-83 513.63,-83 513.63,-89 507.63,-95 501.63,-95"/>
<text text-anchor="start" x="354.63" y="-74.7" font-family="Helvetica,Arial,sans-serif" font-weight="bold" font-size="14.00">RowId</text>
<text text-anchor="start" x="395.88" y="-74.7" font-family="Helvetica,Arial,sans-serif" font-size="14.00"> &#160;(struct)</text>
<text text-anchor="start" x="298.38" y="-44.7" font-family="Helvetica,Arial,sans-serif" font-size="14.00">SmallVec&lt;[u8; 36]&gt; (opaque)</text>
<text text-anchor="start" x="298.38" y="-15.7" font-family="Helvetica,Arial,sans-serif" font-size="14.00">new(bytes), as_bytes(), from(u64)</text>
</g>
<!-- transaction_node&#45;&gt;row_id_node -->
<g id="edge8" class="edge">
<title>transaction_node&#45;&gt;row_id_node</title>
<path fill="none" stroke="#333333" stroke-width="1.2" d="M69.82,-345.56C50.18,-327.75 32.13,-306.52 21.26,-282.5 -1.47,-232.29 -11.84,-202.57 21.26,-158.5 52.05,-117.48 176.52,-86.8 274.48,-68.4"/>
<polygon fill="#333333" stroke="#333333" stroke-width="1.2" points="275,-71.87 284.19,-66.61 273.72,-64.98 275,-71.87"/>
<text text-anchor="middle" x="48.63" y="-218.7" font-family="Helvetica,Arial,sans-serif" font-size="9.00" fill="#555555">insert() yields</text>
</g>
<!-- committed_tx_node&#45;&gt;row_id_node -->
<g id="edge4" class="edge">
<title>committed_tx_node&#45;&gt;row_id_node</title>
<path fill="none" stroke="#333333" stroke-width="1.2" d="M275.73,-172.77C297.13,-151.37 322.65,-125.86 344.97,-103.53"/>
<polygon fill="#333333" stroke="#333333" stroke-width="1.2" points="347.16,-106.3 351.75,-96.75 342.21,-101.35 347.16,-106.3"/>
<text text-anchor="middle" x="355.87" y="-124.95" font-family="Helvetica,Arial,sans-serif" font-size="9.00" fill="#555555">resolve() yields</text>
</g>
<!-- row_stream_node&#45;&gt;row_id_node -->
<g id="edge6" class="edge">
<title>row_stream_node&#45;&gt;row_id_node</title>
<path fill="none" stroke="#333333" stroke-width="1.2" d="M447.27,-345.78C436.64,-278.9 419.73,-172.56 409.28,-106.84"/>
<polygon fill="#333333" stroke="#333333" stroke-width="1.2" points="412.78,-106.58 407.76,-97.25 405.87,-107.68 412.78,-106.58"/>
<text text-anchor="middle" x="471.44" y="-218.7" font-family="Helvetica,Arial,sans-serif" font-size="9.00" fill="#555555">Item = (RowId, _)</text>
</g>
<!-- value_node -->
<g id="node6" class="node">
<title>value_node</title>
<path fill="#fff3e0" stroke="#ff9800" stroke-width="1.5" d="M675.51,-282.5C675.51,-282.5 588.51,-282.5 588.51,-282.5 582.51,-282.5 576.51,-276.5 576.51,-270.5 576.51,-270.5 576.51,-170.5 576.51,-170.5 576.51,-164.5 582.51,-158.5 588.51,-158.5 588.51,-158.5 675.51,-158.5 675.51,-158.5 681.51,-158.5 687.51,-164.5 687.51,-170.5 687.51,-170.5 687.51,-270.5 687.51,-270.5 687.51,-276.5 681.51,-282.5 675.51,-282.5"/>
<text text-anchor="start" x="588.51" y="-262.2" font-family="Helvetica,Arial,sans-serif" font-weight="bold" font-size="14.00">Value</text>
<text text-anchor="start" x="624.51" y="-262.2" font-family="Helvetica,Arial,sans-serif" font-size="14.00"> &#160;(enum)</text>
<text text-anchor="start" x="588.51" y="-232.2" font-family="Helvetica,Arial,sans-serif" font-size="14.00">Int(i64)</text>
<text text-anchor="start" x="588.51" y="-203.2" font-family="Helvetica,Arial,sans-serif" font-size="14.00">Str(String)</text>
<text text-anchor="start" x="588.51" y="-174.2" font-family="Helvetica,Arial,sans-serif" font-size="14.00">Id(RowId)</text>
</g>
<!-- row_stream_node&#45;&gt;value_node -->
<g id="edge7" class="edge">
<title>row_stream_node&#45;&gt;value_node</title>
<path fill="none" stroke="#333333" stroke-width="1.2" d="M514.79,-345.75C531.89,-327.63 550.7,-307.69 568.15,-289.18"/>
<polygon fill="#333333" stroke="#333333" stroke-width="1.2" points="570.5,-291.79 574.82,-282.12 565.41,-286.99 570.5,-291.79"/>
<text text-anchor="middle" x="595.5" y="-312.45" font-family="Helvetica,Arial,sans-serif" font-size="9.00" fill="#555555">Item = (_, Vec&lt;Value&gt;)</text>
</g>
<!-- value_node&#45;&gt;row_id_node -->
<g id="edge9" class="edge">
<title>value_node&#45;&gt;row_id_node</title>
<path fill="none" stroke="#333333" stroke-width="1.2" d="M576.14,-178.32C545.13,-155.47 506.2,-126.78 472.83,-102.18"/>
<polygon fill="#333333" stroke="#333333" stroke-width="1.2" points="475.17,-99.55 465.04,-96.44 471.01,-105.19 475.17,-99.55"/>
<text text-anchor="middle" x="528.92" y="-124.95" font-family="Helvetica,Arial,sans-serif" font-size="9.00" fill="#555555">Id(RowId)</text>
</g>
<!-- table_node -->
<g id="node7" class="node">
<title>table_node</title>
<path fill="#e8f4fd" stroke="#2196f3" stroke-width="1.5" d="M815.88,-455.5C815.88,-455.5 668.13,-455.5 668.13,-455.5 662.13,-455.5 656.13,-449.5 656.13,-443.5 656.13,-443.5 656.13,-372.5 656.13,-372.5 656.13,-366.5 662.13,-360.5 668.13,-360.5 668.13,-360.5 815.88,-360.5 815.88,-360.5 821.88,-360.5 827.88,-366.5 827.88,-372.5 827.88,-372.5 827.88,-443.5 827.88,-443.5 827.88,-449.5 821.88,-455.5 815.88,-455.5"/>
<text text-anchor="start" x="700.01" y="-435.2" font-family="Helvetica,Arial,sans-serif" font-weight="bold" font-size="14.00">Table</text>
<text text-anchor="start" x="734.51" y="-435.2" font-family="Helvetica,Arial,sans-serif" font-size="14.00"> &#160;(struct)</text>
<text text-anchor="start" x="668.13" y="-405.2" font-family="Helvetica,Arial,sans-serif" font-size="14.00">arity: usize</text>
<text text-anchor="start" x="668.13" y="-376.2" font-family="Helvetica,Arial,sans-serif" font-size="14.00">rows: Vec&lt;Vec&lt;Value&gt;&gt;</text>
</g>
<!-- table_node&#45;&gt;value_node -->
<g id="edge10" class="edge">
<title>table_node&#45;&gt;value_node</title>
<path fill="none" stroke="#333333" stroke-width="1.2" d="M714.25,-360.19C702.11,-339.72 687.59,-315.24 674.17,-292.6"/>
<polygon fill="#333333" stroke="#333333" stroke-width="1.2" points="677.32,-291.06 669.21,-284.24 671.3,-294.63 677.32,-291.06"/>
<text text-anchor="middle" x="727.43" y="-312.45" font-family="Helvetica,Arial,sans-serif" font-size="9.00" fill="#555555">Vec&lt;Vec&lt;Value&gt;&gt;</text>
</g>
<!-- storage_error_node -->
<g id="node8" class="node">
<title>storage_error_node</title>
<path fill="#fbe9e7" stroke="#e64a19" stroke-width="1.5" d="M718.76,-1106C718.76,-1106 531.26,-1106 531.26,-1106 525.26,-1106 519.26,-1100 519.26,-1094 519.26,-1094 519.26,-878 519.26,-878 519.26,-872 525.26,-866 531.26,-866 531.26,-866 718.76,-866 718.76,-866 724.76,-866 730.76,-872 730.76,-878 730.76,-878 730.76,-1094 730.76,-1094 730.76,-1100 724.76,-1106 718.76,-1106"/>
<text text-anchor="start" x="557.51" y="-1085.7" font-family="Helvetica,Arial,sans-serif" font-weight="bold" font-size="14.00">StorageError</text>
<text text-anchor="start" x="641.51" y="-1085.7" font-family="Helvetica,Arial,sans-serif" font-size="14.00"> &#160;(enum)</text>
<text text-anchor="start" x="531.26" y="-1055.7" font-family="Helvetica,Arial,sans-serif" font-size="14.00">RelationNotFound(String)</text>
<text text-anchor="start" x="531.26" y="-1026.7" font-family="Helvetica,Arial,sans-serif" font-size="14.00">RelationExists(String)</text>
<text text-anchor="start" x="531.26" y="-997.7" font-family="Helvetica,Arial,sans-serif" font-size="14.00">ArityMismatch { expected, got }</text>
<text text-anchor="start" x="531.26" y="-968.7" font-family="Helvetica,Arial,sans-serif" font-size="14.00">Validation(String)</text>
<text text-anchor="start" x="531.26" y="-939.7" font-family="Helvetica,Arial,sans-serif" font-size="14.00">Decode(CodecError)</text>
<text text-anchor="start" x="531.26" y="-910.7" font-family="Helvetica,Arial,sans-serif" font-size="14.00">Unsupported(&amp;&#39;static str)</text>
<text text-anchor="start" x="531.26" y="-881.7" font-family="Helvetica,Arial,sans-serif" font-size="14.00">Backend(Box&lt;dyn Error&gt;)</text>
</g>
<!-- codec_error_node -->
<g id="node9" class="node">
<title>codec_error_node</title>
<path fill="#fbe9e7" stroke="#e64a19" stroke-width="1.5" d="M736.76,-744.5C736.76,-744.5 513.26,-744.5 513.26,-744.5 507.26,-744.5 501.26,-738.5 501.26,-732.5 501.26,-732.5 501.26,-603.5 501.26,-603.5 501.26,-597.5 507.26,-591.5 513.26,-591.5 513.26,-591.5 736.76,-591.5 736.76,-591.5 742.76,-591.5 748.76,-597.5 748.76,-603.5 748.76,-603.5 748.76,-732.5 748.76,-732.5 748.76,-738.5 742.76,-744.5 736.76,-744.5"/>
<text text-anchor="start" x="562.38" y="-724.2" font-family="Helvetica,Arial,sans-serif" font-weight="bold" font-size="14.00">CodecError</text>
<text text-anchor="start" x="636.63" y="-724.2" font-family="Helvetica,Arial,sans-serif" font-size="14.00"> &#160;(enum)</text>
<text text-anchor="start" x="513.26" y="-694.2" font-family="Helvetica,Arial,sans-serif" font-size="14.00">UnexpectedEof</text>
<text text-anchor="start" x="513.26" y="-665.2" font-family="Helvetica,Arial,sans-serif" font-size="14.00">UnknownTag(u8)</text>
<text text-anchor="start" x="513.26" y="-636.2" font-family="Helvetica,Arial,sans-serif" font-size="14.00">LengthOverrun { declared, available }</text>
<text text-anchor="start" x="513.26" y="-607.2" font-family="Helvetica,Arial,sans-serif" font-size="14.00">InvalidUtf8</text>
</g>
<!-- storage_error_node&#45;&gt;codec_error_node -->
<g id="edge11" class="edge">
<title>storage_error_node&#45;&gt;codec_error_node</title>
<path fill="none" stroke="#333333" stroke-width="1.2" d="M625.01,-865.53C625.01,-829.26 625.01,-790.11 625.01,-756.36"/>
<polygon fill="#333333" stroke="#333333" stroke-width="1.2" points="628.51,-756.48 625.01,-746.48 621.51,-756.48 628.51,-756.48"/>
<text text-anchor="middle" x="667.76" y="-832.45" font-family="Helvetica,Arial,sans-serif" font-size="9.00" fill="#555555">Decode(CodecError)</text>
</g>
<!-- adapters_node -->
<g id="node10" class="node">
<title>adapters_node</title>
<path fill="#f3e5f5" stroke="#9c27b0" stroke-width="1.5" d="M418.38,-1091.5C418.38,-1091.5 201.63,-1091.5 201.63,-1091.5 195.63,-1091.5 189.63,-1085.5 189.63,-1079.5 189.63,-1079.5 189.63,-892.5 189.63,-892.5 189.63,-886.5 195.63,-880.5 201.63,-880.5 201.63,-880.5 418.38,-880.5 418.38,-880.5 424.38,-880.5 430.38,-886.5 430.38,-892.5 430.38,-892.5 430.38,-1079.5 430.38,-1079.5 430.38,-1085.5 424.38,-1091.5 418.38,-1091.5"/>
<text text-anchor="start" x="234.26" y="-1071.2" font-family="Helvetica,Arial,sans-serif" font-weight="bold" font-size="14.00">Adapters</text>
<text text-anchor="start" x="292.76" y="-1071.2" font-family="Helvetica,Arial,sans-serif" font-size="14.00"> &#160;(impl Storage)</text>
<text text-anchor="start" x="201.63" y="-1041.2" font-family="Helvetica,Arial,sans-serif" font-size="14.00">MemoryStorage</text>
<text text-anchor="start" x="201.63" y="-1012.2" font-family="Helvetica,Arial,sans-serif" font-size="14.00">SqliteStorage &#160;(feat sqlite)</text>
<text text-anchor="start" x="201.63" y="-983.2" font-family="Helvetica,Arial,sans-serif" font-size="14.00">RedbStorage &#160;(feat redb)</text>
<text text-anchor="start" x="201.63" y="-954.2" font-family="Helvetica,Arial,sans-serif" font-size="14.00">FjallStorage &#160;(feat fjall)</text>
<text text-anchor="start" x="201.63" y="-925.2" font-family="Helvetica,Arial,sans-serif" font-size="14.00">LmdbStorage &#160;(feat lmdb)</text>
<text text-anchor="start" x="201.63" y="-896.2" font-family="Helvetica,Arial,sans-serif" font-size="14.00">GeomergeStorage &#160;(feat geomerge)</text>
</g>
<!-- adapters_node&#45;&gt;storage_node -->
<g id="edge1" class="edge">
<title>adapters_node&#45;&gt;storage_node</title>
<path fill="none" stroke="#333333" stroke-width="1.2" stroke-dasharray="5,2" d="M310.01,-880.22C310.01,-859.1 310.01,-836.56 310.01,-814.34"/>
<polygon fill="#333333" stroke="#333333" stroke-width="1.2" points="313.51,-814.58 310.01,-804.58 306.51,-814.58 313.51,-814.58"/>
<text text-anchor="middle" x="318.63" y="-832.45" font-family="Helvetica,Arial,sans-serif" font-size="9.00" fill="#555555">impl</text>
</g>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 24 KiB

After

Width:  |  Height:  |  Size: 19 KiB

View File

@ -5,355 +5,222 @@
-->
<!-- Title: StorageWorkflow Pages: 1 -->
<svg width="2196pt" height="573pt"
viewBox="0.00 0.00 2195.75 573.00" xmlns="http://www.w3.org/2000/svg">
<g id="graph0" class="graph" transform="scale(1 1) rotate(0) translate(4 569)">
<title>StorageWorkflow</title>
<polygon fill="white" stroke="none" points="-4,4 -4,-569 2191.75,-569 2191.75,4 -4,4"/>
<g id="clust1" class="cluster">
<title>cluster_inputs</title>
<polygon fill="white" stroke="#888888" stroke-dasharray="5,2"
points="23.5,-254 23.5,-557 211.75,-557 211.75,-254 23.5,-254"/>
<text text-anchor="middle" x="117.62" y="-539.7" font-family="Helvetica,Arial,sans-serif" font-size="14.00"
fill="#555555">Inputs
</text>
</g>
<g id="clust2" class="cluster">
<title>cluster_setup</title>
<polygon fill="white" stroke="#9c27b0" stroke-dasharray="5,2"
points="8,-65 8,-246 524.75,-246 524.75,-65 8,-65"/>
<text text-anchor="middle" x="266.38" y="-228.7" font-family="Helvetica,Arial,sans-serif" font-size="14.00"
fill="#7b1fa2">Setup &#160;(open backend, declare relations)
</text>
</g>
<g id="clust3" class="cluster">
<title>cluster_write</title>
<polygon fill="white" stroke="#4caf50" stroke-dasharray="5,2"
points="563.75,-323 563.75,-532 1882.5,-532 1882.5,-323 563.75,-323"/>
<text text-anchor="middle" x="1223.12" y="-514.7" font-family="Helvetica,Arial,sans-serif" font-size="14.00"
fill="#388e3c">Write &#160;(atomic batch via Transaction)
</text>
</g>
<g id="clust4" class="cluster">
<title>cluster_read</title>
<polygon fill="white" stroke="#ff9800" stroke-dasharray="5,2"
points="1568.25,-8 1568.25,-315 1841.75,-315 1841.75,-8 1568.25,-8"/>
<text text-anchor="middle" x="1705" y="-297.7" font-family="Helvetica,Arial,sans-serif" font-size="14.00"
fill="#f57c00">Read
</text>
</g>
<g id="clust5" class="cluster">
<title>cluster_output</title>
<polygon fill="white" stroke="#888888" stroke-dasharray="5,2"
points="1965.25,-117 1965.25,-306 2179.75,-306 2179.75,-117 1965.25,-117"/>
<text text-anchor="middle" x="2072.5" y="-288.7" font-family="Helvetica,Arial,sans-serif" font-size="14.00"
fill="#555555">Output
</text>
</g>
<!-- schema -->
<g id="node1" class="node">
<title>schema</title>
<path fill="#e8f4fd" stroke="#2196f3" stroke-width="1.5"
d="M181.75,-366.12C181.75,-366.12 53.5,-366.12 53.5,-366.12 47.5,-366.12 41.5,-360.12 41.5,-354.12 41.5,-354.12 41.5,-283.88 41.5,-283.88 41.5,-277.88 47.5,-271.88 53.5,-271.88 53.5,-271.88 181.75,-271.88 181.75,-271.88 187.75,-271.88 193.75,-277.88 193.75,-283.88 193.75,-283.88 193.75,-354.12 193.75,-354.12 193.75,-360.12 187.75,-366.12 181.75,-366.12"/>
<text text-anchor="start" x="91.38" y="-345.82" font-family="Helvetica,Arial,sans-serif" font-weight="bold"
font-size="14.00">Schema
</text>
<text text-anchor="start" x="53.5" y="-316.57" font-family="Helvetica,Arial,sans-serif" font-size="14.00">
relation name
</text>
<text text-anchor="start" x="53.5" y="-287.57" font-family="Helvetica,Arial,sans-serif" font-size="14.00">
arity (column count)
</text>
</g>
<!-- create_relation -->
<g id="node4" class="node">
<title>create_relation</title>
<path fill="#f3e5f5" stroke="#9c27b0" stroke-width="1.5"
d="M498.75,-184C498.75,-184 292.25,-184 292.25,-184 286.25,-184 280.25,-178 280.25,-172 280.25,-172 280.25,-160 280.25,-160 280.25,-154 286.25,-148 292.25,-148 292.25,-148 498.75,-148 498.75,-148 504.75,-148 510.75,-154 510.75,-160 510.75,-160 510.75,-172 510.75,-172 510.75,-178 504.75,-184 498.75,-184"/>
<text text-anchor="middle" x="395.5" y="-163.2" font-family="Helvetica,Arial,sans-serif" font-size="14.00">
storage.create_relation(name, arity)
</text>
</g>
<!-- schema&#45;&gt;create_relation -->
<g id="edge1" class="edge">
<title>schema&#45;&gt;create_relation</title>
<path fill="none" stroke="#2196f3" stroke-width="1.2"
d="M194.01,-277.19C244.45,-249.22 309.06,-213.38 351.1,-190.07"/>
<polygon fill="#2196f3" stroke="#2196f3" stroke-width="1.2"
points="352.64,-193.22 359.69,-185.31 349.24,-187.1 352.64,-193.22"/>
</g>
<!-- row_data -->
<g id="node2" class="node">
<title>row_data</title>
<path fill="#e8f4fd" stroke="#2196f3" stroke-width="1.5"
d="M178.75,-510.12C178.75,-510.12 56.5,-510.12 56.5,-510.12 50.5,-510.12 44.5,-504.12 44.5,-498.12 44.5,-498.12 44.5,-427.88 44.5,-427.88 44.5,-421.88 50.5,-415.88 56.5,-415.88 56.5,-415.88 178.75,-415.88 178.75,-415.88 184.75,-415.88 190.75,-421.88 190.75,-427.88 190.75,-427.88 190.75,-498.12 190.75,-498.12 190.75,-504.12 184.75,-510.12 178.75,-510.12"/>
<text text-anchor="start" x="86.5" y="-489.82" font-family="Helvetica,Arial,sans-serif" font-weight="bold"
font-size="14.00">Row Data
</text>
<text text-anchor="start" x="56.5" y="-460.57" font-family="Helvetica,Arial,sans-serif" font-size="14.00">
Vec&lt;Value&gt;
</text>
<text text-anchor="start" x="56.5" y="-431.57" font-family="Helvetica,Arial,sans-serif" font-size="14.00">
Int / Str / Id(RowId)
</text>
</g>
<!-- tx_ops -->
<g id="node6" class="node">
<title>tx_ops</title>
<path fill="#e8f5e9" stroke="#4caf50" stroke-width="1.5"
d="M1020.75,-474.62C1020.75,-474.62 824.25,-474.62 824.25,-474.62 818.25,-474.62 812.25,-468.62 812.25,-462.62 812.25,-462.62 812.25,-363.38 812.25,-363.38 812.25,-357.38 818.25,-351.38 824.25,-351.38 824.25,-351.38 1020.75,-351.38 1020.75,-351.38 1026.75,-351.38 1032.75,-357.38 1032.75,-363.38 1032.75,-363.38 1032.75,-462.62 1032.75,-462.62 1032.75,-468.62 1026.75,-474.62 1020.75,-474.62"/>
<text text-anchor="start" x="862.88" y="-454.32" font-family="Helvetica,Arial,sans-serif" font-weight="bold"
font-size="14.00">tx.insert / tx.delete
</text>
<text text-anchor="start" x="824.25" y="-425.07" font-family="Helvetica,Arial,sans-serif" font-size="14.00">
• insert yields pending RowId
</text>
<text text-anchor="start" x="824.25" y="-396.07" font-family="Helvetica,Arial,sans-serif" font-size="14.00">
• pending RowIds reused as FKs
</text>
<text text-anchor="start" x="824.25" y="-367.07" font-family="Helvetica,Arial,sans-serif" font-size="14.00">
• delete by RowId
</text>
</g>
<!-- row_data&#45;&gt;tx_ops -->
<g id="edge5" class="edge">
<title>row_data&#45;&gt;tx_ops</title>
<path fill="none" stroke="#2196f3" stroke-width="1.2" stroke-dasharray="5,2"
d="M190.94,-458.5C328.87,-449.91 630.86,-431.1 800.21,-420.55"/>
<polygon fill="#2196f3" stroke="#2196f3" stroke-width="1.2"
points="800.34,-424.05 810.11,-419.94 799.91,-417.07 800.34,-424.05"/>
</g>
<!-- open_backend -->
<g id="node3" class="node">
<title>open_backend</title>
<path fill="#f3e5f5" stroke="#9c27b0" stroke-width="1.5"
d="M201.25,-202.62C201.25,-202.62 34,-202.62 34,-202.62 28,-202.62 22,-196.62 22,-190.62 22,-190.62 22,-91.38 22,-91.38 22,-85.38 28,-79.38 34,-79.38 34,-79.38 201.25,-79.38 201.25,-79.38 207.25,-79.38 213.25,-85.38 213.25,-91.38 213.25,-91.38 213.25,-190.62 213.25,-190.62 213.25,-196.62 207.25,-202.62 201.25,-202.62"/>
<text text-anchor="start" x="70" y="-182.32" font-family="Helvetica,Arial,sans-serif" font-weight="bold"
font-size="14.00">Open Backend
</text>
<text text-anchor="start" x="34" y="-153.07" font-family="Helvetica,Arial,sans-serif" font-size="14.00">
MemoryStorage::new() /
</text>
<text text-anchor="start" x="34" y="-124.08" font-family="Helvetica,Arial,sans-serif" font-size="14.00">
SqliteStorage::open(path) /
</text>
<text text-anchor="start" x="34" y="-95.08" font-family="Helvetica,Arial,sans-serif" font-size="14.00">
FjallStorage::open(path) / ...
</text>
</g>
<!-- open_backend&#45;&gt;create_relation -->
<g id="edge2" class="edge">
<title>open_backend&#45;&gt;create_relation</title>
<path fill="none" stroke="#9c27b0" stroke-width="1.2"
d="M213.57,-149.6C231.17,-151.2 249.8,-152.89 268.16,-154.55"/>
<polygon fill="#9c27b0" stroke="#9c27b0" stroke-width="1.2"
points="267.78,-158.03 278.05,-155.45 268.41,-151.06 267.78,-158.03"/>
</g>
<!-- begin_tx -->
<g id="node5" class="node">
<title>begin_tx</title>
<path fill="#e8f5e9" stroke="#4caf50" stroke-width="1.5"
d="M733.25,-387C733.25,-387 589.75,-387 589.75,-387 583.75,-387 577.75,-381 577.75,-375 577.75,-375 577.75,-349 577.75,-349 577.75,-343 583.75,-337 589.75,-337 589.75,-337 733.25,-337 733.25,-337 739.25,-337 745.25,-343 745.25,-349 745.25,-349 745.25,-375 745.25,-375 745.25,-381 739.25,-387 733.25,-387"/>
<text text-anchor="middle" x="661.5" y="-369.7" font-family="Helvetica,Arial,sans-serif" font-size="14.00">
storage.transaction()
</text>
<text text-anchor="middle" x="661.5" y="-348.7" font-family="Helvetica,Arial,sans-serif" font-size="14.00">
&#45;&gt; Box&lt;dyn Transaction&gt;
</text>
</g>
<!-- create_relation&#45;&gt;begin_tx -->
<g id="edge3" class="edge">
<title>create_relation&#45;&gt;begin_tx</title>
<path fill="none" stroke="#4caf50" stroke-width="1.2"
d="M416.8,-184.23C444.61,-208.87 496.48,-253.65 543.75,-288 564.01,-302.72 587.16,-317.74 607.47,-330.37"/>
<polygon fill="#4caf50" stroke="#4caf50" stroke-width="1.2"
points="605.6,-333.32 615.94,-335.59 609.27,-327.37 605.6,-333.32"/>
</g>
<!-- scan_iter -->
<g id="node9" class="node">
<title>scan_iter</title>
<path fill="#fff3e0" stroke="#ff9800" stroke-width="1.5"
d="M1774.5,-272C1774.5,-272 1635.5,-272 1635.5,-272 1629.5,-272 1623.5,-266 1623.5,-260 1623.5,-260 1623.5,-234 1623.5,-234 1623.5,-228 1629.5,-222 1635.5,-222 1635.5,-222 1774.5,-222 1774.5,-222 1780.5,-222 1786.5,-228 1786.5,-234 1786.5,-234 1786.5,-260 1786.5,-260 1786.5,-266 1780.5,-272 1774.5,-272"/>
<text text-anchor="middle" x="1705" y="-254.7" font-family="Helvetica,Arial,sans-serif" font-size="14.00">
storage.scan_iter(name)
</text>
<text text-anchor="middle" x="1705" y="-233.7" font-family="Helvetica,Arial,sans-serif" font-size="14.00">
&#45;&gt; RowStream
</text>
</g>
<!-- create_relation&#45;&gt;scan_iter -->
<g id="edge8" class="edge">
<title>create_relation&#45;&gt;scan_iter</title>
<path fill="none" stroke="#9c27b0" stroke-width="1.2" stroke-dasharray="5,2"
d="M430.83,-184.43C480.17,-209.16 574.65,-250 660.5,-250 660.5,-250 660.5,-250 1261.62,-250 1382.54,-250 1522.03,-248.87 1611.53,-247.99"/>
<polygon fill="#9c27b0" stroke="#9c27b0" stroke-width="1.2"
points="1611.5,-251.49 1621.46,-247.89 1611.43,-244.49 1611.5,-251.49"/>
</g>
<!-- scan_where -->
<g id="node10" class="node">
<title>scan_where</title>
<path fill="#fff3e0" stroke="#ff9800" stroke-width="1.5"
d="M1815.75,-72C1815.75,-72 1594.25,-72 1594.25,-72 1588.25,-72 1582.25,-66 1582.25,-60 1582.25,-60 1582.25,-34 1582.25,-34 1582.25,-28 1588.25,-22 1594.25,-22 1594.25,-22 1815.75,-22 1815.75,-22 1821.75,-22 1827.75,-28 1827.75,-34 1827.75,-34 1827.75,-60 1827.75,-60 1827.75,-66 1821.75,-72 1815.75,-72"/>
<text text-anchor="middle" x="1705" y="-54.7" font-family="Helvetica,Arial,sans-serif" font-size="14.00">
storage.scan_where(name, col, value)
</text>
<text text-anchor="middle" x="1705" y="-33.7" font-family="Helvetica,Arial,sans-serif" font-size="14.00">
&#45;&gt; RowStream &#160;(filtered)
</text>
</g>
<!-- create_relation&#45;&gt;scan_where -->
<g id="edge9" class="edge">
<title>create_relation&#45;&gt;scan_where</title>
<path fill="none" stroke="#9c27b0" stroke-width="1.2" stroke-dasharray="5,2"
d="M419.28,-147.51C463.15,-113.85 563.58,-46 660.5,-46 660.5,-46 660.5,-46 1261.62,-46 1365.26,-46 1482.56,-46.28 1570.31,-46.54"/>
<polygon fill="#9c27b0" stroke="#9c27b0" stroke-width="1.2"
points="1570.12,-50.04 1580.13,-46.57 1570.15,-43.04 1570.12,-50.04"/>
</g>
<!-- scan_full -->
<g id="node11" class="node">
<title>scan_full</title>
<path fill="#fff3e0" stroke="#ff9800" stroke-width="1.5"
d="M1792.5,-172C1792.5,-172 1617.5,-172 1617.5,-172 1611.5,-172 1605.5,-166 1605.5,-160 1605.5,-160 1605.5,-134 1605.5,-134 1605.5,-128 1611.5,-122 1617.5,-122 1617.5,-122 1792.5,-122 1792.5,-122 1798.5,-122 1804.5,-128 1804.5,-134 1804.5,-134 1804.5,-160 1804.5,-160 1804.5,-166 1798.5,-172 1792.5,-172"/>
<text text-anchor="middle" x="1705" y="-154.7" font-family="Helvetica,Arial,sans-serif" font-size="14.00">
storage.scan(name)
</text>
<text text-anchor="middle" x="1705" y="-133.7" font-family="Helvetica,Arial,sans-serif" font-size="14.00">
&#45;&gt; Vec&lt;(RowId, Vec&lt;Value&gt;)&gt;
</text>
</g>
<!-- create_relation&#45;&gt;scan_full -->
<g id="edge10" class="edge">
<title>create_relation&#45;&gt;scan_full</title>
<path fill="none" stroke="#9c27b0" stroke-width="1.2" stroke-dasharray="5,2"
d="M511.06,-154.78C557.41,-151.13 611.46,-148 660.5,-148 660.5,-148 660.5,-148 1261.62,-148 1374.63,-148 1503.88,-147.67 1593.36,-147.39"/>
<polygon fill="#9c27b0" stroke="#9c27b0" stroke-width="1.2"
points="1593.37,-150.89 1603.35,-147.36 1593.34,-143.89 1593.37,-150.89"/>
</g>
<!-- begin_tx&#45;&gt;tx_ops -->
<g id="edge4" class="edge">
<title>begin_tx&#45;&gt;tx_ops</title>
<path fill="none" stroke="#4caf50" stroke-width="1.2"
d="M745.52,-378.35C763.04,-381.8 781.87,-385.51 800.47,-389.17"/>
<polygon fill="#4caf50" stroke="#4caf50" stroke-width="1.2"
points="799.73,-392.59 810.22,-391.09 801.09,-385.72 799.73,-392.59"/>
</g>
<!-- commit -->
<g id="node7" class="node">
<title>commit</title>
<path fill="#e8f5e9" stroke="#4caf50" stroke-width="1.5"
d="M1409.5,-489.12C1409.5,-489.12 1111.75,-489.12 1111.75,-489.12 1105.75,-489.12 1099.75,-483.12 1099.75,-477.12 1099.75,-477.12 1099.75,-348.88 1099.75,-348.88 1099.75,-342.88 1105.75,-336.88 1111.75,-336.88 1111.75,-336.88 1409.5,-336.88 1409.5,-336.88 1415.5,-336.88 1421.5,-342.88 1421.5,-348.88 1421.5,-348.88 1421.5,-477.12 1421.5,-477.12 1421.5,-483.12 1415.5,-489.12 1409.5,-489.12"/>
<text text-anchor="start" x="1223.5" y="-468.82" font-family="Helvetica,Arial,sans-serif" font-weight="bold"
font-size="14.00">tx.commit()
</text>
<text text-anchor="start" x="1111.75" y="-439.57" font-family="Helvetica,Arial,sans-serif"
font-size="14.00">• native commit (LMDB, redb, SQLite, geomerge)
</text>
<text text-anchor="start" x="1111.75" y="-410.57" font-family="Helvetica,Arial,sans-serif"
font-size="14.00">• buffered apply (memory, fjall)
</text>
<text text-anchor="start" x="1111.75" y="-381.57" font-family="Helvetica,Arial,sans-serif"
font-size="14.00">• law validation (geomerge)
</text>
<text text-anchor="start" x="1111.75" y="-352.57" font-family="Helvetica,Arial,sans-serif"
font-size="14.00">• yields CommittedTx
</text>
</g>
<!-- tx_ops&#45;&gt;commit -->
<g id="edge6" class="edge">
<title>tx_ops&#45;&gt;commit</title>
<path fill="none" stroke="#4caf50" stroke-width="1.2"
d="M1033.09,-413C1050.7,-413 1069.29,-413 1087.94,-413"/>
<polygon fill="#4caf50" stroke="#4caf50" stroke-width="1.2"
points="1087.74,-416.5 1097.74,-413 1087.74,-409.5 1087.74,-416.5"/>
</g>
<!-- resolve_ids -->
<g id="node8" class="node">
<title>resolve_ids</title>
<path fill="#e8f5e9" stroke="#4caf50" stroke-width="1.5"
d="M1856.5,-438.12C1856.5,-438.12 1553.5,-438.12 1553.5,-438.12 1547.5,-438.12 1541.5,-432.12 1541.5,-426.12 1541.5,-426.12 1541.5,-355.88 1541.5,-355.88 1541.5,-349.88 1547.5,-343.88 1553.5,-343.88 1553.5,-343.88 1856.5,-343.88 1856.5,-343.88 1862.5,-343.88 1868.5,-349.88 1868.5,-355.88 1868.5,-355.88 1868.5,-426.12 1868.5,-426.12 1868.5,-432.12 1862.5,-438.12 1856.5,-438.12"/>
<text text-anchor="start" x="1633" y="-417.82" font-family="Helvetica,Arial,sans-serif" font-weight="bold"
font-size="14.00">CommittedTx::resolve
</text>
<text text-anchor="start" x="1553.5" y="-388.57" font-family="Helvetica,Arial,sans-serif" font-size="14.00">
• KV: pending == real
</text>
<text text-anchor="start" x="1553.5" y="-359.57" font-family="Helvetica,Arial,sans-serif" font-size="14.00">
• geomerge: pending counter → (commit, counter)
</text>
</g>
<!-- commit&#45;&gt;resolve_ids -->
<g id="edge7" class="edge">
<title>commit&#45;&gt;resolve_ids</title>
<path fill="none" stroke="#4caf50" stroke-width="1.2"
d="M1421.96,-405.03C1456.99,-403.28 1494.23,-401.43 1529.77,-399.66"/>
<polygon fill="#4caf50" stroke="#4caf50" stroke-width="1.2"
points="1529.61,-403.18 1539.43,-399.18 1529.27,-396.19 1529.61,-403.18"/>
<text text-anchor="middle" x="1481.5" y="-408.27" font-family="Helvetica,Arial,sans-serif" font-size="9.00"
fill="#555555">CommittedTx
</text>
</g>
<!-- commit&#45;&gt;scan_iter -->
<g id="edge11" class="edge">
<title>commit&#45;&gt;scan_iter</title>
<path fill="none" stroke="#4caf50" stroke-width="1.2" stroke-dasharray="5,2"
d="M1421.96,-352.84C1491.15,-326.87 1568.97,-297.67 1625.3,-276.53"/>
<polygon fill="#4caf50" stroke="#4caf50" stroke-width="1.2"
points="1626.44,-279.84 1634.58,-273.05 1623.98,-273.29 1626.44,-279.84"/>
<text text-anchor="middle" x="1481.5" y="-344.89" font-family="Helvetica,Arial,sans-serif" font-size="9.00"
fill="#555555">after commit
</text>
</g>
<!-- rows_out -->
<g id="node12" class="node">
<title>rows_out</title>
<path fill="#eceff1" stroke="#607d8b" stroke-width="1.5"
d="M2149.75,-258.62C2149.75,-258.62 1995.25,-258.62 1995.25,-258.62 1989.25,-258.62 1983.25,-252.62 1983.25,-246.62 1983.25,-246.62 1983.25,-147.38 1983.25,-147.38 1983.25,-141.38 1989.25,-135.38 1995.25,-135.38 1995.25,-135.38 2149.75,-135.38 2149.75,-135.38 2155.75,-135.38 2161.75,-141.38 2161.75,-147.38 2161.75,-147.38 2161.75,-246.62 2161.75,-246.62 2161.75,-252.62 2155.75,-258.62 2149.75,-258.62"/>
<text text-anchor="start" x="2054.5" y="-238.32" font-family="Helvetica,Arial,sans-serif" font-weight="bold"
font-size="14.00">Rows
</text>
<text text-anchor="start" x="1995.25" y="-209.07" font-family="Helvetica,Arial,sans-serif"
font-size="14.00">• (RowId, Vec&lt;Value&gt;)
</text>
<text text-anchor="start" x="1995.25" y="-180.07" font-family="Helvetica,Arial,sans-serif"
font-size="14.00">• consumed by query&#45;ops
</text>
<text text-anchor="start" x="1995.25" y="-151.07" font-family="Helvetica,Arial,sans-serif"
font-size="14.00">&#160;via scan_as_table
</text>
</g>
<!-- resolve_ids&#45;&gt;rows_out -->
<g id="edge15" class="edge">
<title>resolve_ids&#45;&gt;rows_out</title>
<path fill="none" stroke="#4caf50" stroke-width="1.2" stroke-dasharray="5,2"
d="M1847.09,-343.54C1859.24,-338.34 1871.2,-332.82 1882.5,-327 1916.36,-309.58 1951.29,-287.04 1981.77,-265.63"/>
<polygon fill="#4caf50" stroke="#4caf50" stroke-width="1.2"
points="1983.46,-268.72 1989.6,-260.08 1979.41,-263.01 1983.46,-268.72"/>
<text text-anchor="middle" x="1925.88" y="-320.11" font-family="Helvetica,Arial,sans-serif" font-size="9.00"
fill="#555555">real RowIds
</text>
</g>
<!-- scan_iter&#45;&gt;rows_out -->
<g id="edge12" class="edge">
<title>scan_iter&#45;&gt;rows_out</title>
<path fill="none" stroke="#ff9800" stroke-width="1.2"
d="M1786.66,-235.97C1841.16,-228.51 1913.48,-218.62 1971.62,-210.66"/>
<polygon fill="#ff9800" stroke="#ff9800" stroke-width="1.2"
points="1971.81,-214.17 1981.25,-209.35 1970.86,-207.23 1971.81,-214.17"/>
</g>
<!-- scan_where&#45;&gt;rows_out -->
<g id="edge13" class="edge">
<title>scan_where&#45;&gt;rows_out</title>
<path fill="none" stroke="#ff9800" stroke-width="1.2"
d="M1809.89,-72.49C1834.05,-79.5 1859.44,-87.75 1882.5,-97 1912.57,-109.06 1944.11,-124.52 1972.62,-139.66"/>
<polygon fill="#ff9800" stroke="#ff9800" stroke-width="1.2"
points="1970.91,-142.71 1981.37,-144.36 1974.22,-136.55 1970.91,-142.71"/>
</g>
<!-- scan_full&#45;&gt;rows_out -->
<g id="edge14" class="edge">
<title>scan_full&#45;&gt;rows_out</title>
<path fill="none" stroke="#ff9800" stroke-width="1.2"
d="M1804.91,-160.53C1856.5,-167.59 1919.48,-176.2 1971.27,-183.29"/>
<polygon fill="#ff9800" stroke="#ff9800" stroke-width="1.2"
points="1970.63,-186.73 1981.02,-184.62 1971.58,-179.8 1970.63,-186.73"/>
</g>
</g>
viewBox="0.00 0.00 2195.75 573.00" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<g id="graph0" class="graph" transform="scale(1 1) rotate(0) translate(4 569)">
<title>StorageWorkflow</title>
<polygon fill="white" stroke="none" points="-4,4 -4,-569 2191.75,-569 2191.75,4 -4,4"/>
<g id="clust1" class="cluster">
<title>cluster_inputs</title>
<polygon fill="white" stroke="#888888" stroke-dasharray="5,2" points="23.5,-254 23.5,-557 211.75,-557 211.75,-254 23.5,-254"/>
<text text-anchor="middle" x="117.62" y="-539.7" font-family="Helvetica,Arial,sans-serif" font-size="14.00" fill="#555555">Inputs</text>
</g>
<g id="clust2" class="cluster">
<title>cluster_setup</title>
<polygon fill="white" stroke="#9c27b0" stroke-dasharray="5,2" points="8,-65 8,-246 524.75,-246 524.75,-65 8,-65"/>
<text text-anchor="middle" x="266.38" y="-228.7" font-family="Helvetica,Arial,sans-serif" font-size="14.00" fill="#7b1fa2">Setup &#160;(open backend, declare relations)</text>
</g>
<g id="clust3" class="cluster">
<title>cluster_write</title>
<polygon fill="white" stroke="#4caf50" stroke-dasharray="5,2" points="563.75,-323 563.75,-532 1882.5,-532 1882.5,-323 563.75,-323"/>
<text text-anchor="middle" x="1223.12" y="-514.7" font-family="Helvetica,Arial,sans-serif" font-size="14.00" fill="#388e3c">Write &#160;(atomic batch via Transaction)</text>
</g>
<g id="clust4" class="cluster">
<title>cluster_read</title>
<polygon fill="white" stroke="#ff9800" stroke-dasharray="5,2" points="1568.25,-8 1568.25,-315 1841.75,-315 1841.75,-8 1568.25,-8"/>
<text text-anchor="middle" x="1705" y="-297.7" font-family="Helvetica,Arial,sans-serif" font-size="14.00" fill="#f57c00">Read</text>
</g>
<g id="clust5" class="cluster">
<title>cluster_output</title>
<polygon fill="white" stroke="#888888" stroke-dasharray="5,2" points="1965.25,-117 1965.25,-306 2179.75,-306 2179.75,-117 1965.25,-117"/>
<text text-anchor="middle" x="2072.5" y="-288.7" font-family="Helvetica,Arial,sans-serif" font-size="14.00" fill="#555555">Output</text>
</g>
<!-- schema -->
<g id="node1" class="node">
<title>schema</title>
<path fill="#e8f4fd" stroke="#2196f3" stroke-width="1.5" d="M181.75,-366.12C181.75,-366.12 53.5,-366.12 53.5,-366.12 47.5,-366.12 41.5,-360.12 41.5,-354.12 41.5,-354.12 41.5,-283.88 41.5,-283.88 41.5,-277.88 47.5,-271.88 53.5,-271.88 53.5,-271.88 181.75,-271.88 181.75,-271.88 187.75,-271.88 193.75,-277.88 193.75,-283.88 193.75,-283.88 193.75,-354.12 193.75,-354.12 193.75,-360.12 187.75,-366.12 181.75,-366.12"/>
<text text-anchor="start" x="91.38" y="-345.82" font-family="Helvetica,Arial,sans-serif" font-weight="bold" font-size="14.00">Schema</text>
<text text-anchor="start" x="53.5" y="-316.57" font-family="Helvetica,Arial,sans-serif" font-size="14.00">• relation name</text>
<text text-anchor="start" x="53.5" y="-287.57" font-family="Helvetica,Arial,sans-serif" font-size="14.00">• arity (column count)</text>
</g>
<!-- create_relation -->
<g id="node4" class="node">
<title>create_relation</title>
<path fill="#f3e5f5" stroke="#9c27b0" stroke-width="1.5" d="M498.75,-184C498.75,-184 292.25,-184 292.25,-184 286.25,-184 280.25,-178 280.25,-172 280.25,-172 280.25,-160 280.25,-160 280.25,-154 286.25,-148 292.25,-148 292.25,-148 498.75,-148 498.75,-148 504.75,-148 510.75,-154 510.75,-160 510.75,-160 510.75,-172 510.75,-172 510.75,-178 504.75,-184 498.75,-184"/>
<text text-anchor="middle" x="395.5" y="-163.2" font-family="Helvetica,Arial,sans-serif" font-size="14.00">storage.create_relation(name, arity)</text>
</g>
<!-- schema&#45;&gt;create_relation -->
<g id="edge1" class="edge">
<title>schema&#45;&gt;create_relation</title>
<path fill="none" stroke="#2196f3" stroke-width="1.2" d="M194.01,-277.19C244.45,-249.22 309.06,-213.38 351.1,-190.07"/>
<polygon fill="#2196f3" stroke="#2196f3" stroke-width="1.2" points="352.64,-193.22 359.69,-185.31 349.24,-187.1 352.64,-193.22"/>
</g>
<!-- row_data -->
<g id="node2" class="node">
<title>row_data</title>
<path fill="#e8f4fd" stroke="#2196f3" stroke-width="1.5" d="M178.75,-510.12C178.75,-510.12 56.5,-510.12 56.5,-510.12 50.5,-510.12 44.5,-504.12 44.5,-498.12 44.5,-498.12 44.5,-427.88 44.5,-427.88 44.5,-421.88 50.5,-415.88 56.5,-415.88 56.5,-415.88 178.75,-415.88 178.75,-415.88 184.75,-415.88 190.75,-421.88 190.75,-427.88 190.75,-427.88 190.75,-498.12 190.75,-498.12 190.75,-504.12 184.75,-510.12 178.75,-510.12"/>
<text text-anchor="start" x="86.5" y="-489.82" font-family="Helvetica,Arial,sans-serif" font-weight="bold" font-size="14.00">Row Data</text>
<text text-anchor="start" x="56.5" y="-460.57" font-family="Helvetica,Arial,sans-serif" font-size="14.00">• Vec&lt;Value&gt;</text>
<text text-anchor="start" x="56.5" y="-431.57" font-family="Helvetica,Arial,sans-serif" font-size="14.00">• Int / Str / Id(RowId)</text>
</g>
<!-- tx_ops -->
<g id="node6" class="node">
<title>tx_ops</title>
<path fill="#e8f5e9" stroke="#4caf50" stroke-width="1.5" d="M1020.75,-474.62C1020.75,-474.62 824.25,-474.62 824.25,-474.62 818.25,-474.62 812.25,-468.62 812.25,-462.62 812.25,-462.62 812.25,-363.38 812.25,-363.38 812.25,-357.38 818.25,-351.38 824.25,-351.38 824.25,-351.38 1020.75,-351.38 1020.75,-351.38 1026.75,-351.38 1032.75,-357.38 1032.75,-363.38 1032.75,-363.38 1032.75,-462.62 1032.75,-462.62 1032.75,-468.62 1026.75,-474.62 1020.75,-474.62"/>
<text text-anchor="start" x="862.88" y="-454.32" font-family="Helvetica,Arial,sans-serif" font-weight="bold" font-size="14.00">tx.insert / tx.delete</text>
<text text-anchor="start" x="824.25" y="-425.07" font-family="Helvetica,Arial,sans-serif" font-size="14.00">• insert yields pending RowId</text>
<text text-anchor="start" x="824.25" y="-396.07" font-family="Helvetica,Arial,sans-serif" font-size="14.00">• pending RowIds reused as FKs</text>
<text text-anchor="start" x="824.25" y="-367.07" font-family="Helvetica,Arial,sans-serif" font-size="14.00">• delete by RowId</text>
</g>
<!-- row_data&#45;&gt;tx_ops -->
<g id="edge5" class="edge">
<title>row_data&#45;&gt;tx_ops</title>
<path fill="none" stroke="#2196f3" stroke-width="1.2" stroke-dasharray="5,2" d="M190.94,-458.5C328.87,-449.91 630.86,-431.1 800.21,-420.55"/>
<polygon fill="#2196f3" stroke="#2196f3" stroke-width="1.2" points="800.34,-424.05 810.11,-419.94 799.91,-417.07 800.34,-424.05"/>
</g>
<!-- open_backend -->
<g id="node3" class="node">
<title>open_backend</title>
<path fill="#f3e5f5" stroke="#9c27b0" stroke-width="1.5" d="M201.25,-202.62C201.25,-202.62 34,-202.62 34,-202.62 28,-202.62 22,-196.62 22,-190.62 22,-190.62 22,-91.38 22,-91.38 22,-85.38 28,-79.38 34,-79.38 34,-79.38 201.25,-79.38 201.25,-79.38 207.25,-79.38 213.25,-85.38 213.25,-91.38 213.25,-91.38 213.25,-190.62 213.25,-190.62 213.25,-196.62 207.25,-202.62 201.25,-202.62"/>
<text text-anchor="start" x="70" y="-182.32" font-family="Helvetica,Arial,sans-serif" font-weight="bold" font-size="14.00">Open Backend</text>
<text text-anchor="start" x="34" y="-153.07" font-family="Helvetica,Arial,sans-serif" font-size="14.00">MemoryStorage::new() /</text>
<text text-anchor="start" x="34" y="-124.08" font-family="Helvetica,Arial,sans-serif" font-size="14.00">SqliteStorage::open(path) /</text>
<text text-anchor="start" x="34" y="-95.08" font-family="Helvetica,Arial,sans-serif" font-size="14.00">FjallStorage::open(path) / ...</text>
</g>
<!-- open_backend&#45;&gt;create_relation -->
<g id="edge2" class="edge">
<title>open_backend&#45;&gt;create_relation</title>
<path fill="none" stroke="#9c27b0" stroke-width="1.2" d="M213.57,-149.6C231.17,-151.2 249.8,-152.89 268.16,-154.55"/>
<polygon fill="#9c27b0" stroke="#9c27b0" stroke-width="1.2" points="267.78,-158.03 278.05,-155.45 268.41,-151.06 267.78,-158.03"/>
</g>
<!-- begin_tx -->
<g id="node5" class="node">
<title>begin_tx</title>
<path fill="#e8f5e9" stroke="#4caf50" stroke-width="1.5" d="M733.25,-387C733.25,-387 589.75,-387 589.75,-387 583.75,-387 577.75,-381 577.75,-375 577.75,-375 577.75,-349 577.75,-349 577.75,-343 583.75,-337 589.75,-337 589.75,-337 733.25,-337 733.25,-337 739.25,-337 745.25,-343 745.25,-349 745.25,-349 745.25,-375 745.25,-375 745.25,-381 739.25,-387 733.25,-387"/>
<text text-anchor="middle" x="661.5" y="-369.7" font-family="Helvetica,Arial,sans-serif" font-size="14.00">storage.transaction()</text>
<text text-anchor="middle" x="661.5" y="-348.7" font-family="Helvetica,Arial,sans-serif" font-size="14.00">&#45;&gt; Box&lt;dyn Transaction&gt;</text>
</g>
<!-- create_relation&#45;&gt;begin_tx -->
<g id="edge3" class="edge">
<title>create_relation&#45;&gt;begin_tx</title>
<path fill="none" stroke="#4caf50" stroke-width="1.2" d="M416.8,-184.23C444.61,-208.87 496.48,-253.65 543.75,-288 564.01,-302.72 587.16,-317.74 607.47,-330.37"/>
<polygon fill="#4caf50" stroke="#4caf50" stroke-width="1.2" points="605.6,-333.32 615.94,-335.59 609.27,-327.37 605.6,-333.32"/>
</g>
<!-- scan_iter -->
<g id="node9" class="node">
<title>scan_iter</title>
<path fill="#fff3e0" stroke="#ff9800" stroke-width="1.5" d="M1774.5,-272C1774.5,-272 1635.5,-272 1635.5,-272 1629.5,-272 1623.5,-266 1623.5,-260 1623.5,-260 1623.5,-234 1623.5,-234 1623.5,-228 1629.5,-222 1635.5,-222 1635.5,-222 1774.5,-222 1774.5,-222 1780.5,-222 1786.5,-228 1786.5,-234 1786.5,-234 1786.5,-260 1786.5,-260 1786.5,-266 1780.5,-272 1774.5,-272"/>
<text text-anchor="middle" x="1705" y="-254.7" font-family="Helvetica,Arial,sans-serif" font-size="14.00">storage.scan_iter(name)</text>
<text text-anchor="middle" x="1705" y="-233.7" font-family="Helvetica,Arial,sans-serif" font-size="14.00">&#45;&gt; RowStream</text>
</g>
<!-- create_relation&#45;&gt;scan_iter -->
<g id="edge8" class="edge">
<title>create_relation&#45;&gt;scan_iter</title>
<path fill="none" stroke="#9c27b0" stroke-width="1.2" stroke-dasharray="5,2" d="M430.83,-184.43C480.17,-209.16 574.65,-250 660.5,-250 660.5,-250 660.5,-250 1261.62,-250 1382.54,-250 1522.03,-248.87 1611.53,-247.99"/>
<polygon fill="#9c27b0" stroke="#9c27b0" stroke-width="1.2" points="1611.5,-251.49 1621.46,-247.89 1611.43,-244.49 1611.5,-251.49"/>
</g>
<!-- scan_where -->
<g id="node10" class="node">
<title>scan_where</title>
<path fill="#fff3e0" stroke="#ff9800" stroke-width="1.5" d="M1815.75,-72C1815.75,-72 1594.25,-72 1594.25,-72 1588.25,-72 1582.25,-66 1582.25,-60 1582.25,-60 1582.25,-34 1582.25,-34 1582.25,-28 1588.25,-22 1594.25,-22 1594.25,-22 1815.75,-22 1815.75,-22 1821.75,-22 1827.75,-28 1827.75,-34 1827.75,-34 1827.75,-60 1827.75,-60 1827.75,-66 1821.75,-72 1815.75,-72"/>
<text text-anchor="middle" x="1705" y="-54.7" font-family="Helvetica,Arial,sans-serif" font-size="14.00">storage.scan_where(name, col, value)</text>
<text text-anchor="middle" x="1705" y="-33.7" font-family="Helvetica,Arial,sans-serif" font-size="14.00">&#45;&gt; RowStream &#160;(filtered)</text>
</g>
<!-- create_relation&#45;&gt;scan_where -->
<g id="edge9" class="edge">
<title>create_relation&#45;&gt;scan_where</title>
<path fill="none" stroke="#9c27b0" stroke-width="1.2" stroke-dasharray="5,2" d="M419.28,-147.51C463.15,-113.85 563.58,-46 660.5,-46 660.5,-46 660.5,-46 1261.62,-46 1365.26,-46 1482.56,-46.28 1570.31,-46.54"/>
<polygon fill="#9c27b0" stroke="#9c27b0" stroke-width="1.2" points="1570.12,-50.04 1580.13,-46.57 1570.15,-43.04 1570.12,-50.04"/>
</g>
<!-- scan_full -->
<g id="node11" class="node">
<title>scan_full</title>
<path fill="#fff3e0" stroke="#ff9800" stroke-width="1.5" d="M1792.5,-172C1792.5,-172 1617.5,-172 1617.5,-172 1611.5,-172 1605.5,-166 1605.5,-160 1605.5,-160 1605.5,-134 1605.5,-134 1605.5,-128 1611.5,-122 1617.5,-122 1617.5,-122 1792.5,-122 1792.5,-122 1798.5,-122 1804.5,-128 1804.5,-134 1804.5,-134 1804.5,-160 1804.5,-160 1804.5,-166 1798.5,-172 1792.5,-172"/>
<text text-anchor="middle" x="1705" y="-154.7" font-family="Helvetica,Arial,sans-serif" font-size="14.00">storage.scan(name)</text>
<text text-anchor="middle" x="1705" y="-133.7" font-family="Helvetica,Arial,sans-serif" font-size="14.00">&#45;&gt; Vec&lt;(RowId, Vec&lt;Value&gt;)&gt;</text>
</g>
<!-- create_relation&#45;&gt;scan_full -->
<g id="edge10" class="edge">
<title>create_relation&#45;&gt;scan_full</title>
<path fill="none" stroke="#9c27b0" stroke-width="1.2" stroke-dasharray="5,2" d="M511.06,-154.78C557.41,-151.13 611.46,-148 660.5,-148 660.5,-148 660.5,-148 1261.62,-148 1374.63,-148 1503.88,-147.67 1593.36,-147.39"/>
<polygon fill="#9c27b0" stroke="#9c27b0" stroke-width="1.2" points="1593.37,-150.89 1603.35,-147.36 1593.34,-143.89 1593.37,-150.89"/>
</g>
<!-- begin_tx&#45;&gt;tx_ops -->
<g id="edge4" class="edge">
<title>begin_tx&#45;&gt;tx_ops</title>
<path fill="none" stroke="#4caf50" stroke-width="1.2" d="M745.52,-378.35C763.04,-381.8 781.87,-385.51 800.47,-389.17"/>
<polygon fill="#4caf50" stroke="#4caf50" stroke-width="1.2" points="799.73,-392.59 810.22,-391.09 801.09,-385.72 799.73,-392.59"/>
</g>
<!-- commit -->
<g id="node7" class="node">
<title>commit</title>
<path fill="#e8f5e9" stroke="#4caf50" stroke-width="1.5" d="M1409.5,-489.12C1409.5,-489.12 1111.75,-489.12 1111.75,-489.12 1105.75,-489.12 1099.75,-483.12 1099.75,-477.12 1099.75,-477.12 1099.75,-348.88 1099.75,-348.88 1099.75,-342.88 1105.75,-336.88 1111.75,-336.88 1111.75,-336.88 1409.5,-336.88 1409.5,-336.88 1415.5,-336.88 1421.5,-342.88 1421.5,-348.88 1421.5,-348.88 1421.5,-477.12 1421.5,-477.12 1421.5,-483.12 1415.5,-489.12 1409.5,-489.12"/>
<text text-anchor="start" x="1223.5" y="-468.82" font-family="Helvetica,Arial,sans-serif" font-weight="bold" font-size="14.00">tx.commit()</text>
<text text-anchor="start" x="1111.75" y="-439.57" font-family="Helvetica,Arial,sans-serif" font-size="14.00">• native commit (LMDB, redb, SQLite, geomerge)</text>
<text text-anchor="start" x="1111.75" y="-410.57" font-family="Helvetica,Arial,sans-serif" font-size="14.00">• buffered apply (memory, fjall)</text>
<text text-anchor="start" x="1111.75" y="-381.57" font-family="Helvetica,Arial,sans-serif" font-size="14.00">• law validation (geomerge)</text>
<text text-anchor="start" x="1111.75" y="-352.57" font-family="Helvetica,Arial,sans-serif" font-size="14.00">• yields CommittedTx</text>
</g>
<!-- tx_ops&#45;&gt;commit -->
<g id="edge6" class="edge">
<title>tx_ops&#45;&gt;commit</title>
<path fill="none" stroke="#4caf50" stroke-width="1.2" d="M1033.09,-413C1050.7,-413 1069.29,-413 1087.94,-413"/>
<polygon fill="#4caf50" stroke="#4caf50" stroke-width="1.2" points="1087.74,-416.5 1097.74,-413 1087.74,-409.5 1087.74,-416.5"/>
</g>
<!-- resolve_ids -->
<g id="node8" class="node">
<title>resolve_ids</title>
<path fill="#e8f5e9" stroke="#4caf50" stroke-width="1.5" d="M1856.5,-438.12C1856.5,-438.12 1553.5,-438.12 1553.5,-438.12 1547.5,-438.12 1541.5,-432.12 1541.5,-426.12 1541.5,-426.12 1541.5,-355.88 1541.5,-355.88 1541.5,-349.88 1547.5,-343.88 1553.5,-343.88 1553.5,-343.88 1856.5,-343.88 1856.5,-343.88 1862.5,-343.88 1868.5,-349.88 1868.5,-355.88 1868.5,-355.88 1868.5,-426.12 1868.5,-426.12 1868.5,-432.12 1862.5,-438.12 1856.5,-438.12"/>
<text text-anchor="start" x="1633" y="-417.82" font-family="Helvetica,Arial,sans-serif" font-weight="bold" font-size="14.00">CommittedTx::resolve</text>
<text text-anchor="start" x="1553.5" y="-388.57" font-family="Helvetica,Arial,sans-serif" font-size="14.00">• KV: pending == real</text>
<text text-anchor="start" x="1553.5" y="-359.57" font-family="Helvetica,Arial,sans-serif" font-size="14.00">• geomerge: pending counter → (commit, counter)</text>
</g>
<!-- commit&#45;&gt;resolve_ids -->
<g id="edge7" class="edge">
<title>commit&#45;&gt;resolve_ids</title>
<path fill="none" stroke="#4caf50" stroke-width="1.2" d="M1421.96,-405.03C1456.99,-403.28 1494.23,-401.43 1529.77,-399.66"/>
<polygon fill="#4caf50" stroke="#4caf50" stroke-width="1.2" points="1529.61,-403.18 1539.43,-399.18 1529.27,-396.19 1529.61,-403.18"/>
<text text-anchor="middle" x="1481.5" y="-408.27" font-family="Helvetica,Arial,sans-serif" font-size="9.00" fill="#555555">CommittedTx</text>
</g>
<!-- commit&#45;&gt;scan_iter -->
<g id="edge11" class="edge">
<title>commit&#45;&gt;scan_iter</title>
<path fill="none" stroke="#4caf50" stroke-width="1.2" stroke-dasharray="5,2" d="M1421.96,-352.84C1491.15,-326.87 1568.97,-297.67 1625.3,-276.53"/>
<polygon fill="#4caf50" stroke="#4caf50" stroke-width="1.2" points="1626.44,-279.84 1634.58,-273.05 1623.98,-273.29 1626.44,-279.84"/>
<text text-anchor="middle" x="1481.5" y="-344.89" font-family="Helvetica,Arial,sans-serif" font-size="9.00" fill="#555555">after commit</text>
</g>
<!-- rows_out -->
<g id="node12" class="node">
<title>rows_out</title>
<path fill="#eceff1" stroke="#607d8b" stroke-width="1.5" d="M2149.75,-258.62C2149.75,-258.62 1995.25,-258.62 1995.25,-258.62 1989.25,-258.62 1983.25,-252.62 1983.25,-246.62 1983.25,-246.62 1983.25,-147.38 1983.25,-147.38 1983.25,-141.38 1989.25,-135.38 1995.25,-135.38 1995.25,-135.38 2149.75,-135.38 2149.75,-135.38 2155.75,-135.38 2161.75,-141.38 2161.75,-147.38 2161.75,-147.38 2161.75,-246.62 2161.75,-246.62 2161.75,-252.62 2155.75,-258.62 2149.75,-258.62"/>
<text text-anchor="start" x="2054.5" y="-238.32" font-family="Helvetica,Arial,sans-serif" font-weight="bold" font-size="14.00">Rows</text>
<text text-anchor="start" x="1995.25" y="-209.07" font-family="Helvetica,Arial,sans-serif" font-size="14.00">• (RowId, Vec&lt;Value&gt;)</text>
<text text-anchor="start" x="1995.25" y="-180.07" font-family="Helvetica,Arial,sans-serif" font-size="14.00">• consumed by query&#45;ops</text>
<text text-anchor="start" x="1995.25" y="-151.07" font-family="Helvetica,Arial,sans-serif" font-size="14.00"> &#160;via scan_as_table</text>
</g>
<!-- resolve_ids&#45;&gt;rows_out -->
<g id="edge15" class="edge">
<title>resolve_ids&#45;&gt;rows_out</title>
<path fill="none" stroke="#4caf50" stroke-width="1.2" stroke-dasharray="5,2" d="M1847.09,-343.54C1859.24,-338.34 1871.2,-332.82 1882.5,-327 1916.36,-309.58 1951.29,-287.04 1981.77,-265.63"/>
<polygon fill="#4caf50" stroke="#4caf50" stroke-width="1.2" points="1983.46,-268.72 1989.6,-260.08 1979.41,-263.01 1983.46,-268.72"/>
<text text-anchor="middle" x="1925.88" y="-320.11" font-family="Helvetica,Arial,sans-serif" font-size="9.00" fill="#555555">real RowIds</text>
</g>
<!-- scan_iter&#45;&gt;rows_out -->
<g id="edge12" class="edge">
<title>scan_iter&#45;&gt;rows_out</title>
<path fill="none" stroke="#ff9800" stroke-width="1.2" d="M1786.66,-235.97C1841.16,-228.51 1913.48,-218.62 1971.62,-210.66"/>
<polygon fill="#ff9800" stroke="#ff9800" stroke-width="1.2" points="1971.81,-214.17 1981.25,-209.35 1970.86,-207.23 1971.81,-214.17"/>
</g>
<!-- scan_where&#45;&gt;rows_out -->
<g id="edge13" class="edge">
<title>scan_where&#45;&gt;rows_out</title>
<path fill="none" stroke="#ff9800" stroke-width="1.2" d="M1809.89,-72.49C1834.05,-79.5 1859.44,-87.75 1882.5,-97 1912.57,-109.06 1944.11,-124.52 1972.62,-139.66"/>
<polygon fill="#ff9800" stroke="#ff9800" stroke-width="1.2" points="1970.91,-142.71 1981.37,-144.36 1974.22,-136.55 1970.91,-142.71"/>
</g>
<!-- scan_full&#45;&gt;rows_out -->
<g id="edge14" class="edge">
<title>scan_full&#45;&gt;rows_out</title>
<path fill="none" stroke="#ff9800" stroke-width="1.2" d="M1804.91,-160.53C1856.5,-167.59 1919.48,-176.2 1971.27,-183.29"/>
<polygon fill="#ff9800" stroke="#ff9800" stroke-width="1.2" points="1970.63,-186.73 1981.02,-184.62 1971.58,-179.8 1970.63,-186.73"/>
</g>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 23 KiB

After

Width:  |  Height:  |  Size: 19 KiB

View File

@ -93,6 +93,17 @@ fn decode_pending_row_id(bytes: &[u8]) -> Result<TempRowId, StorageError> {
}
/// Geomerge-backed [`Storage`] implementation.
/// Primitive column type used by [`GeomergeStorage::with_relations`] to
/// synthesize a theory from an untyped `(name, arity)` schema. Geomerge
/// supports `PrimInt`, `PrimString`, and entity types; only the two
/// primitives are exposed here, since callers using this constructor by
/// definition don't carry entity-target information.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ColumnKind {
Int,
String,
}
pub struct GeomergeStorage {
store: Store,
declared: HashSet<String>,
@ -138,6 +149,52 @@ impl GeomergeStorage {
}
}
/// Build a store with a theory synthesized from a flat list of
/// `(relation_name, column_kinds)`. Each `ColumnKind` is mapped to the
/// matching `PrimType`. No entity columns and no laws are declared.
///
/// This is the convenience constructor for callers (e.g., the
/// `plan-runner` CLI) whose schema only carries arity plus a column-by-
/// column primitive-type guess taken from a sample row. It exists so
/// those callers don't have to depend on `geolog-lang::ir` directly.
///
/// # Errors
/// Returns [`StorageError::Backend`] if geomerge rejects the synthesized
/// theory.
pub fn with_relations<I, S>(relations: I) -> Result<Self, StorageError>
where
I: IntoIterator<Item = (S, Vec<ColumnKind>)>,
S: Into<String>,
{
let tables: Vec<ir::TableEntry> = relations
.into_iter()
.map(|(name, kinds)| {
let columns = kinds
.into_iter()
.map(|k| ir::ColType::PrimType {
prim: match k {
ColumnKind::Int => ir::PrimType::PrimInt,
ColumnKind::String => ir::PrimType::PrimString,
},
})
.collect();
let name: String = name.into();
ir::TableEntry {
path: name.into(),
table: ir::Schema {
columns,
primary_key: None,
},
}
})
.collect();
let theory = ir::FlatTheory {
tables,
laws: Vec::new(),
};
Self::from_theory(theory)
}
/// Borrow the underlying geomerge store (for backend-specific operations
/// like persistence, dump, or law inspection that aren't on the trait).
#[must_use]

View File

@ -154,12 +154,12 @@ impl Transaction for LmdbTx<'_> {
let Some(wtxn) = self.wtxn.as_ref() else {
unreachable!("transaction was already committed")
};
let raw = self
let encoded = self
.meta
.get(wtxn, name.as_bytes())
.map_err(backend)?
.ok_or_else(|| StorageError::RelationNotFound(name.to_string()))?;
let entry = decode_meta(raw)?;
let entry = decode_meta(encoded)?;
self.next_ids.insert(name.to_string(), entry);
entry
};

View File

@ -1,4 +1,4 @@
//! SQLite adapter via the `rusqlite` crate (bundled libsqlite3).
//! `SQLite` adapter via the `rusqlite` crate (bundled libsqlite3).
//!
//! Storage layout:
//!
@ -35,13 +35,13 @@ CREATE TABLE IF NOT EXISTS __rows (
);
";
/// SQLite-backed [`Storage`] implementation.
/// `SQLite`-backed [`Storage`] implementation.
pub struct SqliteStorage {
conn: Connection,
}
impl SqliteStorage {
/// Open or create a SQLite database at `path`. Pass `":memory:"` for
/// Open or create a `SQLite` database at `path`. Pass `":memory:"` for
/// an in-process database (useful in tests).
///
/// # Errors

75
tools/exporter/README.md Normal file
View File

@ -0,0 +1,75 @@
## Plan Exporter
Haskell tool that turns a hand-authored scenario into the JSON IR consumed by
[`crates/plan-runner`](../../crates/plan-runner).
Each scenario carries a small schema, a set of ground facts, a list of conjunctive-query atoms, and an expected-bindings oracle.
The exporter parses the scenario, runs `Geolog.DB.Plan.planConjunction` from
[`external/geolog`](../../external/geolog) to produce a Yannakakis-style plan,
self-checks the planned bindings against the oracle, then writes the runner IR to stdout.
This is the only producer of runner IR today;
any other frontend that emits the same JSON shape can drive the runner instead.
### Layout
```text
tools/exporter/
├── plan-exporter.cabal # cabal package: executable `plan-export`
├── cabal.project # points at geolog-lang and its siblings in external/geolog
├── src/Main.hs # the exporter itself; rustdoc-style header explains the IR
└── examples/ # hand-authored scenario inputs
├── cartesian.scenario.json
├── self_loop.scenario.json
├── three_atom_chain.scenario.json
└── two_atom_join.scenario.json
```
### Run It
The exporter needs GHC 9.12 and Cabal.
The repository's Nix dev shell provides both;
enter it with `make shell` (or `nix develop`) before running the commands below.
```sh
# Build the executable:
cd tools/exporter && cabal build plan-export
# Export one scenario to runner IR JSON:
cabal run -v0 plan-export -- examples/two_atom_join.scenario.json
# Regenerate every fixture in crates/plan-runner/fixtures/ from these scenarios:
make export-fixtures # run from the repository root
```
### Scenario Format
Each `.scenario.json` carries four blocks:
| Block | Maps to |
|---------------------|----------------------------------------------------------------------|
| `schema` | `FlatTheory` from `geolog-lang`: relation paths and column types. |
| `facts` | Ground tuples per relation, in the shape `InMemory` ingests. |
| `atoms` | The conjunctive-query body, as a list of `QAtom`. |
| `expected_bindings` | Variables and rows the planner is expected to produce. Used as a self-check; lifted through to the runner-side oracle. |
See the four committed examples for the exact JSON shape.
The runner-side IR shape (what the exporter emits) is documented in
[`crates/plan-runner/src/lib.rs`](../../crates/plan-runner/src/lib.rs).
### Self-Check
Before emitting JSON, the exporter runs the planned query through
`Geolog.DB.InMemory.evalConjunctionPlanned` and asserts the bindings match
the scenario's `expected_bindings`.
A mismatched scenario fails at export time, so the Rust side never sees a broken fixture.
### Notes
- **Cross-language contract.**
The JSON shape is the contract between this tool and `plan-runner`.
Changing it means editing both sides.
- **Subset of the source language.**
The exporter only handles conjunctive queries that fit `planConjunction`.
Negation, recursion, and laws are out of scope here; they belong to a different planner pass.
- **No durable references to ignored paths.**
This tool depends on `external/geolog` (a submodule), not on any ad-hoc clone or generated copy in an ignored path.

View File

@ -7,7 +7,7 @@
-- against.
packages:
glog-exporter.cabal
plan-exporter.cabal
../../external/geolog/geolog-lang/geolog-lang.cabal
../../external/geolog/data-partition/data-partition.cabal
../../external/geolog/diagnostician/diagnostician.cabal

View File

@ -0,0 +1,140 @@
{
"name": "cartesian",
"_description": "Two disjoint atoms over different tables. Exercises the 'stray' branch of planConjunction's spanning forest (no shared variables = no edge in the intersection graph) and the linear chain of natural-joins that fullJoinForest emits over disconnected components.",
"schema": {
"left": {
"columns": [
{
"entity": "left"
}
]
},
"right": {
"columns": [
{
"entity": "right"
}
]
}
},
"facts": {
"left": [
[
{
"entity": [
"left",
1
]
}
],
[
{
"entity": [
"left",
2
]
}
]
],
"right": [
[
{
"entity": [
"right",
10
]
}
],
[
{
"entity": [
"right",
20
]
}
]
]
},
"atoms": [
{
"table": "left",
"values": {
"0": {
"var": "a"
}
}
},
{
"table": "right",
"values": {
"0": {
"var": "b"
}
}
}
],
"expected_bindings": {
"columns": [
"a",
"b"
],
"rows": [
[
{
"entity": [
"left",
1
]
},
{
"entity": [
"right",
10
]
}
],
[
{
"entity": [
"left",
1
]
},
{
"entity": [
"right",
20
]
}
],
[
{
"entity": [
"left",
2
]
},
{
"entity": [
"right",
10
]
}
],
[
{
"entity": [
"left",
2
]
},
{
"entity": [
"right",
20
]
}
]
]
}
}

View File

@ -0,0 +1,119 @@
{
"name": "self-loop",
"_description": "Single-atom query with a repeated variable across two columns: edge(x, x, _). Exercises evalAtom's equality-enforcement path; the planner emits one PlanEvalAtom node and no joins.",
"schema": {
"edge": {
"columns": [
{
"entity": "node"
},
{
"entity": "node"
},
{
"entity": "edge"
}
]
}
},
"facts": {
"edge": [
[
{
"entity": [
"node",
1
]
},
{
"entity": [
"node",
2
]
},
{
"entity": [
"edge",
1
]
}
],
[
{
"entity": [
"node",
2
]
},
{
"entity": [
"node",
2
]
},
{
"entity": [
"edge",
2
]
}
],
[
{
"entity": [
"node",
3
]
},
{
"entity": [
"node",
3
]
},
{
"entity": [
"edge",
3
]
}
]
]
},
"atoms": [
{
"table": "edge",
"values": {
"0": {
"var": "x"
},
"1": {
"var": "x"
}
}
}
],
"expected_bindings": {
"columns": [
"x"
],
"rows": [
[
{
"entity": [
"node",
2
]
}
],
[
{
"entity": [
"node",
3
]
}
]
]
}
}

View File

@ -0,0 +1,156 @@
{
"name": "three-atom-chain",
"schema": {
"node": {
"columns": [
{
"entity": "node"
}
]
},
"edge": {
"columns": [
{
"entity": "node"
},
{
"entity": "node"
},
{
"entity": "edge"
}
]
}
},
"facts": {
"node": [
[
{
"entity": [
"node",
1
]
}
],
[
{
"entity": [
"node",
2
]
}
],
[
{
"entity": [
"node",
3
]
}
]
],
"edge": [
[
{
"entity": [
"node",
1
]
},
{
"entity": [
"node",
2
]
},
{
"entity": [
"edge",
1
]
}
],
[
{
"entity": [
"node",
2
]
},
{
"entity": [
"node",
3
]
},
{
"entity": [
"edge",
2
]
}
]
]
},
"atoms": [
{
"table": "node",
"values": {
"0": {
"var": "a"
}
}
},
{
"table": "edge",
"values": {
"0": {
"var": "a"
},
"1": {
"var": "b"
}
}
},
{
"table": "edge",
"values": {
"0": {
"var": "b"
},
"1": {
"var": "c"
}
}
}
],
"expected_bindings": {
"columns": [
"a",
"b",
"c"
],
"rows": [
[
{
"entity": [
"node",
1
]
},
{
"entity": [
"node",
2
]
},
{
"entity": [
"node",
3
]
}
]
]
}
}

View File

@ -0,0 +1,144 @@
{
"name": "two-atom-join",
"schema": {
"node": {
"columns": [
{
"entity": "node"
}
]
},
"edge": {
"columns": [
{
"entity": "node"
},
{
"entity": "node"
},
{
"entity": "edge"
}
]
}
},
"facts": {
"node": [
[
{
"entity": [
"node",
1
]
}
],
[
{
"entity": [
"node",
2
]
}
]
],
"edge": [
[
{
"entity": [
"node",
1
]
},
{
"entity": [
"node",
2
]
},
{
"entity": [
"edge",
1
]
}
],
[
{
"entity": [
"node",
2
]
},
{
"entity": [
"node",
1
]
},
{
"entity": [
"edge",
2
]
}
]
]
},
"atoms": [
{
"table": "node",
"values": {
"0": {
"var": "a"
}
}
},
{
"table": "edge",
"values": {
"0": {
"var": "a"
},
"1": {
"var": "b"
}
}
}
],
"expected_bindings": {
"columns": [
"a",
"b"
],
"rows": [
[
{
"entity": [
"node",
1
]
},
{
"entity": [
"node",
2
]
}
],
[
{
"entity": [
"node",
2
]
},
{
"entity": [
"node",
1
]
}
]
]
}
}

View File

@ -1,19 +1,20 @@
cabal-version: 3.4
name: glog-exporter
name: plan-exporter
version: 0.1.0.0
license: MIT OR Apache-2.0
author: storage-engine-playground
synopsis: Export geolog-lang join plans as JSON for the Rust runner.
synopsis: Export conjunctive-query plans as JSON for the Rust plan-runner.
description:
Builds a FlatTheory + facts + a list of QAtoms for a named scenario,
runs Geolog.DB.Plan.planConjunction, and emits a JSON document that
crates/glog-runner consumes. This allows the playground use query-ops and
storage end-to-end with a real Yannakakis plan produced by the geolog
frontend, not a hand-written fixture.
Reads a scenario (FlatTheory + facts + a list of QAtoms) from JSON,
runs Geolog.DB.Plan.planConjunction, and emits a plan IR JSON document
that crates/plan-runner consumes. The IR is the contract between the
Haskell frontend and the Rust executor; this tool is currently the only
producer, but any frontend that emits the same JSON shape can drive the
runner.
build-type: Simple
executable glog-export
executable plan-export
main-is: Main.hs
hs-source-dirs: src
default-language: GHC2024
@ -32,5 +33,6 @@ executable glog-export
, base
, bytestring
, containers
, fnotation
, geolog-lang
, text

View File

@ -1,31 +1,41 @@
-- | Exports a geolog-lang join plan as JSON for the Rust runner in
-- @crates/glog-runner@.
-- | Reads a @.scenario.json@ example, plans its conjunction with
-- @Geolog.DB.Plan.planConjunction@, and writes a runner-IR JSON plan that
-- @crates\/plan-runner@ consumes.
--
-- Invocation:
--
-- @
-- cabal run glog-export -- <scenario> > plan.json
-- cabal run plan-export -- <scenario.json>
-- @
--
-- Available scenarios: @three-atom-chain@.
-- The scenario format is documented in @examples\/README@ or by example
-- (@examples\/*.scenario.json@); the output shape is documented in
-- @crates\/plan-runner\/src\/lib.rs@.
--
-- The output shape is documented in @crates\/glog-runner\/src\/lib.rs@.
-- This program is the canonical producer: any change to the IR should
-- start here, with the Rust runner updated to match.
-- The exporter is also a self-check: before emitting, it runs the planned
-- query through @evalConjunctionPlanned@ and verifies the bindings match
-- the scenario's @expected_bindings@. A mismatched scenario fails loudly
-- here rather than handing a bad fixture to the Rust runner.
module Main (main) where
import Algebra.Graph qualified as AG
import Data.Aeson ((.=))
import Control.Monad (unless)
import Data.Aeson ((.!=), (.:), (.:?), (.=))
import Data.Aeson qualified as Aeson
import Data.Aeson.Encode.Pretty qualified as AesonPretty
import Data.Aeson.Key qualified as Key
import Data.Aeson.KeyMap qualified as KM
import Data.Aeson.Types (Parser)
import Data.ByteString.Lazy.Char8 qualified as LBS8
import Data.Foldable (toList)
import Data.List (sortOn)
import Data.Map.Strict (Map)
import Data.Map.Strict qualified as Map
import Data.Set qualified as Set
import Data.Text (Text)
import Data.Text qualified as T
import Data.String (fromString)
import FNotation.Names (Name)
import Geolog.DB.InMemory
import Geolog.DB.Plan
import Geolog.IR qualified as IR
@ -33,74 +43,142 @@ import System.Environment (getArgs)
import System.Exit (die)
import System.IO (hPutStrLn, stderr)
-- * Scenario plumbing
-- * Scenario file format
--
-- A scenario fixes a schema, a set of ground facts, and a conjunction of
-- query atoms. The exporter is intentionally code-driven (not @.glog@
-- driven): @.glog@ files declare theories, not queries, so the query
-- side has to live in Haskell either way.
-- Mirrors @Geolog.IR.FlatTheory@ + @[(Path, [Val])]@ + @[QAtom]@. The
-- 'Expected' block is optional but, when present, the exporter cross-
-- checks it against the planner's own evaluation before emitting.
data Scenario = Scenario
{ scName :: String
, scTheory :: IR.FlatTheory
{ scName :: Text
, scSchema :: Map IR.Path SchemaEntry
, scFacts :: [(IR.Path, [Val])]
, scAtoms :: [QAtom]
, scExpected :: Maybe Expected
}
deriving (Show)
-- * three-atom-chain
--
-- Mirrors @DB.InMemoryTest@ "matches evalConjunction on three-atom chain".
-- node = {e1, e2, e3}, edge = {(e1,e2,ee1), (e2,e3,ee2)}.
-- Conjunction: node(a), edge(a, b, _), edge(b, c, _).
data SchemaEntry = SchemaEntry
{ seColumns :: [IR.ColType]
, sePrimaryKey :: Maybe [Int]
}
deriving (Show)
nodePath, edgePath :: IR.Path
nodePath = ["node"]
edgePath = ["edge"]
data Expected = Expected
{ exColumns :: [Text]
, exRows :: [[Val]]
}
deriving (Show)
threeAtomChain :: Scenario
threeAtomChain =
Scenario
{ scName = "three-atom-chain"
, scTheory =
IR.FlatTheory
{ tables =
-- ** JSON parsers
parsePath :: Aeson.Value -> Parser IR.Path
parsePath = Aeson.withText "path" \t -> pure [nameFromText t]
-- | Build a single-segment 'Name' from text. Multi-segment names (which
-- would carry a non-empty 'init' field) aren't needed by any current
-- example; if a scenario wants @"a/b"@-style paths, extend this helper.
nameFromText :: Text -> Name
nameFromText = fromString . T.unpack
instance Aeson.FromJSON SchemaEntry where
parseJSON = Aeson.withObject "SchemaEntry" \o ->
SchemaEntry <$> o .: "columns" <*> o .:? "primaryKey"
instance Aeson.FromJSON IR.ColType where
parseJSON = Aeson.withObject "ColType" \o -> do
case KM.toList o of
[("entity", v)] -> IR.EntityType <$> parsePath v
[("prim", v)] -> IR.PrimType <$> parsePrim v
_ -> fail "ColType: expected {\"entity\": <path>} or {\"prim\": \"int\"|\"string\"}"
parsePrim :: Aeson.Value -> Parser IR.PrimType
parsePrim = Aeson.withText "prim type" \case
"int" -> pure IR.PrimInt
"string" -> pure IR.PrimString
other -> fail ("unknown primitive type: " <> T.unpack other)
parseVal :: Aeson.Value -> Parser Val
parseVal = Aeson.withObject "Val" \o ->
case KM.toList o of
[("int", v)] -> ValInt <$> Aeson.parseJSON v
[("str", v)] -> ValText <$> Aeson.parseJSON v
[("entity", v)] -> parseEntity v
_ -> fail "Val: expected {\"int\": ..} | {\"str\": ..} | {\"entity\": [<path>, <id>]}"
where
parseEntity = Aeson.withArray "entity" \arr -> case toList arr of
[pv, nv] -> do
p <- parsePath pv
n <- Aeson.parseJSON nv
pure (ValEntity p n)
_ -> fail "entity: expected [<path>, <id>]"
parseQVal :: Aeson.Value -> Parser QVal
parseQVal = Aeson.withObject "QVal" \o ->
case KM.toList o of
[("var", v)] -> QVar . Var <$> Aeson.parseJSON v
[("lit", v)] -> QLit <$> parseVal v
_ -> fail "QVal: expected {\"var\": \"name\"} or {\"lit\": <value>}"
parseAtom :: Aeson.Value -> Parser QAtom
parseAtom = Aeson.withObject "QAtom" \o -> do
qaTable <- o .: "table" >>= parsePath
qaRowId <- o .:? "rowId" >>= traverse parseQVal
values <- o .: "values" :: Parser (Map Text Aeson.Value)
qaValues <-
Map.fromList
[ (nodePath, IR.Table {columns = [IR.EntityType nodePath], primaryKey = Nothing})
, (edgePath, IR.Table {columns = [IR.EntityType nodePath, IR.EntityType nodePath, IR.EntityType edgePath], primaryKey = Nothing})
]
<$> traverse
( \(k, v) -> case reads (T.unpack k) of
[(i, "")] -> (i,) <$> parseQVal v
_ -> fail ("non-integer key in atom values: " <> T.unpack k)
)
(Map.toList values)
pure QAtom {qaTable, qaRowId, qaValues}
parseExpected :: Aeson.Value -> Parser Expected
parseExpected = Aeson.withObject "Expected" \o -> do
exColumns <- o .: "columns"
rawRows <- o .: "rows" :: Parser [[Aeson.Value]]
exRows <- traverse (traverse parseVal) rawRows
pure Expected {exColumns, exRows}
instance Aeson.FromJSON Scenario where
parseJSON = Aeson.withObject "Scenario" \o -> do
scName <- o .:? "name" .!= "unnamed"
rawSchema <- o .: "schema" :: Parser (Map Text SchemaEntry)
let scSchema = Map.fromList [([nameFromText k], v) | (k, v) <- Map.toList rawSchema]
rawFacts <- o .:? "facts" .!= mempty :: Parser (Map Text [[Aeson.Value]])
scFacts <-
concat
<$> traverse
( \(name, rows) -> do
let path = [nameFromText name]
parsedRows <- traverse (traverse parseVal) rows
pure [(path, row) | row <- parsedRows]
)
(Map.toList rawFacts)
rawAtoms <- o .: "atoms" :: Parser [Aeson.Value]
scAtoms <- traverse parseAtom rawAtoms
scExpected <- o .:? "expected_bindings" >>= traverse parseExpected
pure Scenario {scName, scSchema, scFacts, scAtoms, scExpected}
-- * Scenario → FlatTheory + DB + atoms
toFlatTheory :: Scenario -> IR.FlatTheory
toFlatTheory sc =
IR.FlatTheory
{ tables = Map.map (\e -> IR.Table {columns = seColumns e, primaryKey = sePrimaryKey e}) sc.scSchema
, laws = Map.empty
}
, scFacts =
[ (nodePath, [ValEntity nodePath 1])
, (nodePath, [ValEntity nodePath 2])
, (nodePath, [ValEntity nodePath 3])
, (edgePath, [ValEntity nodePath 1, ValEntity nodePath 2, ValEntity edgePath 1])
, (edgePath, [ValEntity nodePath 2, ValEntity nodePath 3, ValEntity edgePath 2])
]
, scAtoms =
[ QAtom {qaTable = nodePath, qaRowId = Nothing, qaValues = Map.singleton 0 (QVar (Var "a"))}
, QAtom {qaTable = edgePath, qaRowId = Nothing, qaValues = Map.fromList [(0, QVar (Var "a")), (1, QVar (Var "b"))]}
, QAtom {qaTable = edgePath, qaRowId = Nothing, qaValues = Map.fromList [(0, QVar (Var "b")), (1, QVar (Var "c"))]}
]
}
scenarios :: [Scenario]
scenarios = [threeAtomChain]
populateDB :: Scenario -> DB
populateDB sc = foldl (\d (p, row) -> insertRow p row d) (fromTheory (toFlatTheory sc)) sc.scFacts
-- * JSON encoding
-- * JSON encoding for the plan-runner IR
--
-- The shape mirrors the IR in @crates/glog-runner/src/lib.rs@:
--
-- > {
-- > "schema": {<name>: <arity>, ...},
-- > "facts": {<name>: [[<value>, ...], ...], ...},
-- > "query": {"root": <id>, "nodes": [{"id": <id>, "action": <action>}, ...]}
-- > }
-- The shape is the same one we settled on earlier; see
-- @crates/plan-runner/src/lib.rs@.
-- | Render a 'Geolog.IR.Path' (a list of 'FNotation.Names.Name') as a flat
-- string for use as a relation name on the Rust side. Each 'Name' is
-- already shown with @\/@ between its own init segments and last, so we
-- reuse 'show' and join Names with @\/@ too.
pathText :: IR.Path -> Text
pathText = T.intercalate "/" . map (T.pack . show)
@ -119,10 +197,6 @@ encodeTerm = \case
QVar (Var name) -> Aeson.object ["var" .= name]
QLit v -> Aeson.object ["lit" .= encodeValue v]
-- | Flatten an atom into one term per stored column, mirroring
-- @Geolog.DB.InMemory.toFlatArgs@: @qaValues@ keys map to positions
-- @0..n-2@, @qaRowId@ (if present) maps to position @n-1@, and any
-- missing positions become wildcard variables with locally-unique names.
flattenAtom :: Int -> Int -> QAtom -> [Aeson.Value]
flattenAtom atomIdx arity qa =
[ encodeTerm (Map.findWithDefault (wildcard atomIdx pos) pos merged)
@ -145,9 +219,6 @@ encodeAtom tables atomIdx qa =
Just t -> length t.columns
Nothing -> error ("encodeAtom: unknown table " <> show qa.qaTable)
-- | Stable atom indexing keyed by atom identity, so the wildcard names in
-- @flattenAtom@ are deterministic across runs even if the planner's node
-- ordering changes.
atomIndex :: [QAtom] -> Map QAtom Int
atomIndex atoms = Map.fromList (zip (Set.toList (Set.fromList atoms)) [0 ..])
@ -176,9 +247,6 @@ encodeNode tables idx n =
]
]
-- | Render a 'PlanGraph' as the JSON the runner consumes. Empty graphs
-- produce @{"root": 0, "nodes": []}@, which the runner treats as a
-- well-formed but empty query.
encodeQuery :: Map IR.Path IR.Table -> Map QAtom Int -> PlanGraph -> Aeson.Value
encodeQuery tables idx (PlanGraph g)
| null nodes =
@ -192,24 +260,30 @@ encodeQuery tables idx (PlanGraph g)
nodes = sortOn (.graphId.unPlanNodeId) (AG.vertexList g)
rootId = case graphRoot (PlanGraph g) of
Just (PlanNodeId i) -> i
-- Non-empty graph with no topological root means a cycle, which
-- planConjunction never produces. Fall back to the last id rather
-- than crashing so a bug here is still inspectable.
Nothing -> (.graphId.unPlanNodeId) (last nodes)
encodeExpected :: Expected -> Aeson.Value
encodeExpected ex =
Aeson.object
[ "columns" .= exColumns ex
, "rows" .= map (map encodeValue) (exRows ex)
]
encodePlan :: Scenario -> Aeson.Value
encodePlan sc =
Aeson.object
[ "_scenario" .= sc.scName
, "schema" .= Aeson.object
[pathKey p .= length t.columns | (p, t) <- Map.toList sc.scTheory.tables]
, "facts" .= Aeson.object
[pathKey p .= map (map encodeValue) rows | (p, rows) <- groupedFacts sc.scFacts]
, "query" .= encodeQuery sc.scTheory.tables (atomIndex sc.scAtoms) (planConjunction sc.scAtoms)
( [ "_scenario" .= scName sc
, "schema" .= Aeson.object [pathKey p .= length (seColumns t) | (p, t) <- Map.toList sc.scSchema]
, "facts"
.= Aeson.object
[ pathKey p .= map (map encodeValue) rows
| (p, rows) <- groupedFacts sc.scFacts
]
, "query" .= encodeQuery (toFlatTheory sc).tables (atomIndex sc.scAtoms) (planConjunction sc.scAtoms)
]
++ maybe [] (\e -> ["expected_bindings" .= encodeExpected e]) sc.scExpected
)
-- | Group facts by table while preserving table-first-seen order and
-- per-table insertion order.
groupedFacts :: [(IR.Path, [Val])] -> [(IR.Path, [[Val]])]
groupedFacts = go []
where
@ -222,17 +296,45 @@ groupedFacts = go []
-- * Self-check
--
-- Run the planner's @evalConjunctionPlanned@ against the scenario's DB
-- to confirm the plan we're about to emit is well-formed and produces
-- non-error output. Catches malformed scenarios before they hand a bad
-- plan to the Rust runner.
-- Cross-check the planned bindings against any user-supplied
-- 'expected_bindings'. Detects two classes of bug before they reach the
-- Rust side: a scenario whose 'expected' is wrong, and a planner output
-- that disagrees with 'evalConjunction'.
selfCheck :: Scenario -> IO ()
selfCheck sc = do
let db = foldl (\d (p, row) -> insertRow p row d) (fromTheory sc.scTheory) sc.scFacts
let db = populateDB sc
case evalConjunctionPlanned db sc.scAtoms of
Left err -> die ("self-check failed for " <> sc.scName <> ": " <> show err)
Right _ -> pure ()
Left err -> die ("self-check failed for " <> T.unpack sc.scName <> ": " <> show err)
Right actual -> case sc.scExpected of
Nothing -> pure ()
Just expected -> verifyAgainstExpected sc.scName expected actual
verifyAgainstExpected :: Text -> Expected -> Bindings -> IO ()
verifyAgainstExpected name expected actual = do
let actualCols = actual.cols
expectedCols = Set.fromList (map Var (exColumns expected))
unless (Set.isSubsetOf expectedCols actualCols) $
die $
"self-check failed for "
<> T.unpack name
<> ": expected_bindings names columns not produced by the plan: "
<> show (Set.difference expectedCols actualCols)
let projectedActual = Set.map (`projectOn` exColumns expected) actual.table
expectedProjected = Set.fromList (map (zip (exColumns expected)) (exRows expected))
expectedSet = Set.map (Map.fromList . map (\(v, x) -> (Var v, x))) expectedProjected
unless (projectedActual == expectedSet) $
die $
"self-check failed for "
<> T.unpack name
<> ":\n expected: "
<> show expectedSet
<> "\n actual: "
<> show projectedActual
projectOn :: Map Var Val -> [Text] -> Map Var Val
projectOn row keys =
Map.fromList [(Var k, v) | k <- keys, Just v <- [Map.lookup (Var k) row]]
-- * Entry point
@ -240,13 +342,13 @@ main :: IO ()
main = do
args <- getArgs
case args of
[name] -> case lookup name [(s.scName, s) | s <- scenarios] of
Just sc -> do
[path] -> do
raw <- LBS8.readFile path
sc <- case Aeson.eitherDecode raw of
Left err -> die ("failed to parse " <> path <> ": " <> err)
Right sc -> pure sc
selfCheck sc
LBS8.putStrLn (AesonPretty.encodePretty (encodePlan sc))
Nothing ->
die ("unknown scenario: " <> name <> "\navailable: " <> unwords (map (.scName) scenarios))
_ -> do
hPutStrLn stderr "usage: glog-export <scenario>"
hPutStrLn stderr ("scenarios: " <> unwords (map (.scName) scenarios))
hPutStrLn stderr "usage: plan-export <scenario.json>"
die ""