Skip to content

A second connection made from the first - #28

Merged
tamnd merged 1 commit into
mainfrom
dx-cursor
Aug 19, 2026
Merged

A second connection made from the first#28
tamnd merged 1 commit into
mainfrom
dx-cursor

Conversation

@tamnd

@tamnd tamnd commented Aug 19, 2026

Copy link
Copy Markdown
Owner

Milestone tamnd/zu#169, item 6: cursor() in both clients, which is how a pool is written. The engine half is tamnd/zu#409, and this pins it.

conn.cursor() gives back another zudb.Connection on the same database. conn.duplicate() is the same call under the name that says what it does. Two names because cursor() is what every other embedded database calls this and a caller who learned the word elsewhere should not have to learn another one, while a reader of this code who has not should not have to guess.

It forks off the database the connection already holds rather than opening the file again, so it costs a schema load and no path lookup, and it works on a database in memory, where there is no path to open a second time. That was the gap worth closing: a pool that seeds itself and lets the first connection go had no way to a second one at all.

The two are connections in every sense rather than two names for one. Each has its own prepared statements, its own caches and its own transaction, so a thread taking one from a pool is not in whatever transaction the last borrower left open, and closing one does not close the other. What they share is the write side, so they queue behind each other to write and each sees what the other has committed.

On an event loop conn.cursor() is awaited, because forking reaches the engine, and the new connection gets a thread of its own, which is the point: one connection runs one statement at a time, so two results in flight means two connections. There is a test that gathers two statements on the two of them.

In zudb.dbapi the name is duplicate() alone. cursor() there is the thing PEP 249 means by it, which shares its connection and its transaction rather than making new ones, and the two are worth keeping apart in a docstring as well as in the code. A duplicate carries autocommit across and starts outside a transaction.

Twelve tests: the pool case both ways, the properties carried across, a duplicate outliving the connection it was made from, a transaction of its own, a closed connection refusing, the async pair running at once, the DB-API pair, and the README program.

ruff check, ruff format --check, cargo clippy --all-targets -- -D warnings, cargo fmt --check and pytest all green locally.

cursor() is another connection to the same database, made from a
connection rather than from a path, which is how a pool is written.
duplicate() is the same call under the name that says what it does;
cursor() is what every other embedded database calls it and a caller
who learned the word elsewhere should not have to learn another one
here.

It forks off the database the connection already holds rather than
opening the file again, so it costs a schema load and no path lookup,
and it works on a database in memory, where there is no path to open a
second time. Each of the two has its own prepared statements, its own
caches and its own transaction, and closing one does not close the
other. What they share is the write side.

On an event loop it is awaited and the new connection gets a thread of
its own, which is the point: one connection runs one statement at a
time, so two results in flight means two connections. In zudb.dbapi the
name is duplicate() alone, because cursor() there is the thing PEP 249
means by it.
@tamnd
tamnd merged commit 7d7c385 into main Aug 19, 2026
26 of 33 checks passed
@tamnd
tamnd deleted the dx-cursor branch August 19, 2026 12:18
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