Skip to content

The loader: a node table's columns, the edges between them, one file - #2

Merged
tamnd merged 1 commit into
mainfrom
python-loader
Aug 18, 2026
Merged

tamnd merged 1 commit into
mainfrom
python-loader

Conversation

@tamnd

@tamnd tamnd commented Aug 18, 2026

Copy link
Copy Markdown
Owner

A statement writes a row at a time, and the first row of a table that does not exist yet has to be written out rather than parameterized, because a value that is worked out says nothing about the column it would go in. A rel table is worse than that: no statement makes one, since an INSERT names the two tables an edge joins only for the edge it is writing. That left this client unable to build the thing the database is for, and left Rel and Path with nothing in the suite to read.

zudb.load is the other shape, and it is the one the bulk loader has: a node table's columns whole, an edge list whole, one file written once.

zudb.load(
    "social.zu1",
    nodes="person",
    rels="knows",
    columns={"uid": [1, 2, 3], "name": ["ada", "grace", "kay"]},
    edges=[(0, 1), (1, 2)],
)

Columns hold booleans, integers, floats, strings, dates, times, datetimes or durations, one kind to a column, settled by the first value and checked against every value after it. Edges name rows by position, counting from zero, because at load time a row has no other name, and an edge naming a row the table has not got is refused where it is written rather than invented or dropped. Either half may be left out: a graph with no properties is a graph, and so is one with no edges. The path must not exist, since a load builds a database rather than adding to one, and overwriting would be the worst possible reading of the call.

Reading the Python objects needs the GIL and the write does not, so the GIL is released around the sort, the graph and the columns, which is the whole cost of a load. Two hundred thousand nodes and as many edges take about six hundred milliseconds and leave the main thread free the whole time, which is what the new threading test measures.

Two things found while writing it. Path.every was inside pymethods, so a helper that exists to write two getters was reachable from Python as a third method nobody documented; it is a plain impl now. And Rel.ord is the edge's place in the load order, which is where its properties sit, not its ordinal among the edges joining that pair, so the class doc now says what the engine does.

Thirty-four new tests, and the suite is a hundred and eighteen. The interesting ones are the first in this repository to read a real Rel and a real Path, including a two-hop walk whose elements alternate node, edge, node. The refusals name the column and the row and read as sentences, and every one of them is a test.

Local gates green: ruff check, ruff format --check, cargo fmt --all --check, cargo clippy --all-targets -D warnings, pytest.

Toward tamnd/zu#168.

A statement writes a row at a time, and the first row of a table that
does not exist yet has to be written out rather than parameterized,
because a value that is worked out says nothing about the column it
would go in. A rel table is worse than that: no statement makes one,
since an INSERT names the two tables an edge joins only for the edge it
is writing. That left the client unable to build the thing the database
is for, and left Rel and Path with nothing in the suite to read.

zudb.load is the other shape, and it is the one the bulk loader has: a
node table's columns whole, an edge list whole, one file written once.
Columns hold booleans, integers, floats, strings, dates, times,
datetimes or durations, one kind to a column, settled by the first
value and checked against every value after it. Edges name rows by
position, counting from zero, because at load time a row has no other
name, and an edge naming a row the table has not got is refused where
it is written rather than invented or dropped. Either half may be left
out: a graph with no properties is a graph, and so is one with no
edges. The path must not exist, since a load builds a database rather
than adding to one.

Reading the Python objects needs the GIL and the write does not, so the
GIL is released around the sort, the graph and the columns, which is
the whole cost of a load. Two hundred thousand nodes and as many edges
take about six hundred milliseconds and leave the main thread free the
whole time, which is what the new threading test measures.

Two things found while writing it. Path.every was inside pymethods, so
a helper that exists to write two getters was reachable from Python as
a third method nobody documented; it is a plain impl now. And Rel.ord
is the edge's place in the load order, which is where its properties
sit, not its ordinal among the edges joining that pair, so the class
doc says what the engine does.

The refusals name the column and the row and read as sentences, and
every one of them is a test.
@tamnd
tamnd merged commit 3f3e357 into main Aug 18, 2026
6 of 7 checks passed
@tamnd
tamnd deleted the python-loader branch August 18, 2026 00:00
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant