Skip to content

Prepare a statement, and see what one would do - #19

Merged
tamnd merged 1 commit into
mainfrom
prepared
Aug 19, 2026
Merged

Prepare a statement, and see what one would do#19
tamnd merged 1 commit into
mainfrom
prepared

Conversation

@tamnd

@tamnd tamnd commented Aug 19, 2026

Copy link
Copy Markdown
Owner

conn.prepare, conn.explain and conn.profile, and a Prepared class for the first of them.

Preparing

prepare compiles a statement and hands back something that runs it later, as often as it is asked to, with different values bound each run. It answers the same three ways a connection does, query, exec and columnar, and the options go on the run rather than on the prepare, since which run a caller wants to stop or spell as numbers is a property of that run.

That came almost free: QueryTask grew a Source that is either a text or a pinned id, so the three answer shapes were already written and a prepared statement reaches all of them by handing them an id. Handles carries the three counted handles every call runs against, which is also what keeps the constructor under clippy's argument limit.

There is no stream on a prepared statement, deliberately. The engine's streaming path takes a source text rather than a pinned id, so a streamed prepared statement would be this client quietly running the text again behind the caller, and a method that does not do what its name says is worse than one that is not there.

What preparing buys is not throughput, and the bench prints that rather than hiding it. A driver prepares to save a round trip and there is none here, and the engine already caches a plan by its text. npm run bench:prepared over 100 rows and 5000 runs:

prepared, bound per run                     13756 ns each
the same text, bound per run                13812 ns each
a new text per run                          21209 ns each

preparing itself
prepare and close                           10142 ns each
explain                                     13311 ns each

The first two being equal is the honest result. The third is the line to read: a statement whose text differs every run, which is what a program that pastes its values into the string is writing, pays the compile every time. What preparing does buy is the compile happening at the line that asked, at startup, where a statement that does not compile fails on the way up, and the parameter names coming back in params.

Explaining and profiling

The plan comes back twice on purpose. root is the tree, so a test can assert that a scan became a seek without matching on a string, and every operator carries its detail, binds, tables, children, the bracket it is inside and the name the listing gives it. text is the engine's own rendering of that same tree, so the two cannot drift apart from one release to the next. explain takes no parameters, since a plan is chosen from the shape of the statement and a plan asked for with values would suggest the values changed it.

profile takes bindings, since it is a run. Every count is a number and not a bigint, which is the one place in this client an integer is spelled as a double on purpose: nothing a profile counts comes near 2^53 and a caller doing arithmetic on a measurement should not have to convert first. A statement that writes is refused rather than profiled, because a profile that also inserted two rows changed the thing it measured.

Checked

39 new tests, over the lifetime of a prepared statement rather than only over the rows it answers: what it says after it is closed, what happens to one whose connection went first, closing twice, await using, a read-only connection, a signal on one run, a mode on one run, and three prepared at once. The plan tests assert the listing against the tree it was rendered from rather than against a string written here, so an optimizer that learns to print one better does not fail them.

Full suite is 311 tests, all green, along with cargo fmt, cargo clippy --all-features, npm run check:types, npm run check:package and the api report.

Milestone: DX3 (tamnd/zu#169).

Three calls on a connection, and a class for the first of them.
`prepare` compiles a statement now and hands back something that runs
it later, as often as it is asked to, with different values bound each
time. `explain` compiles one and answers the plan without running it.
`profile` runs it and answers what the operators really did.

A prepared statement answers the same three ways a connection does,
because `QueryTask` grew a `Source` that is either a text or a pinned
id and the three answer shapes were already written against it. So
`query`, `exec` and `columnar` are on `Prepared` for the cost of
handing them an id. There is no `stream`, deliberately: the engine's
streaming path takes a text rather than an id, so a streamed prepared
statement would be this client running the text again behind the
caller.

What preparing buys here is not throughput, and the bench prints that
rather than hiding it. The engine caches a plan by its text, so a loop
that prepares and a loop that repeats the same string are the same
speed, 13756 ns against 13812 ns. A text that is new every run costs
21209 ns, and that gap is the whole of what binding parameters saves a
program that would otherwise paste its values in. What preparing does
buy is the compile happening at the line that asked, at startup, and
the parameter names coming back.

The plan comes back twice: `root` is the tree, for a program that
wants to assert an operator without matching a string, and `text` is
the engine's own listing, for a person. It is the engine's rendering
rather than this client's so the two cannot drift. Profile counts are
`number` and not `bigint`, which is the one place here an integer is
spelled as a double on purpose, since nothing a profile counts comes
near 2^53.

39 tests over the lifetime, the shapes and the refusals, a bench, the
README sections and a type fixture in each format.
@tamnd
tamnd merged commit a22e7b4 into main Aug 19, 2026
23 checks passed
@tamnd
tamnd deleted the prepared branch August 19, 2026 09:15
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