Prepare a statement, and see what one would do - #25
Merged
Conversation
`prepare` compiles a statement once and gives back an object that runs it, `explain` says what the engine would run without running it, and `profile` runs it and says what it really did. The honest part is in the README. There is no round trip to save here, and the engine already caches the plan it compiled under the text of the statement, so a prepared statement bound per run costs 30.1 microseconds and the same text handed to `execute` costs 30.1 as well. What preparing buys is the compile happening at the line that asked for it, and `params` saying which names the statement wants, and an object a program can hold and close rather than a string it passes around. A plan and a profile each answer twice: `print` gives the engine's own listing, rendered by the engine so the two cannot drift apart, and `root` gives the same plan as objects for a program to walk. An operator carries `op` for what it is and `name` for what the listing calls it, which differ inside a bracket, and a program should match on `op` because that is the one that does not change with the company an operator keeps. Profile counts are `int`, exact at any size, and an operator the optimizer had nothing to say about carries `None` rather than a zero somebody would read as an estimate. A prepared statement runs through the same call `execute` does, so it gets the GIL released, the interrupt handling and the catalog names by being that call rather than a second path beside it. `zudb.aio` mirrors all three, and `statement`, `params` and `closed` are properties there because the names were read at the compile and nothing waits to say them again. Forty-two tests: nineteen for the lifetime of a prepared statement and twenty-three for the shape of a plan and a profile, including the listing asserted against the tree it was rendered from rather than against a string written out here.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
preparecompiles a statement once and gives back an object that runs it,explainsays what the engine would run without running it, andprofileruns it and says what it really did. This is the Python half of the milestone item that the api-map scorecard was blocked on; the Node half landed in zu-node #19.Preparing
The README says what this saves, which is less than the word usually promises, and the numbers are printed rather than hidden. The database is in this process, so there is no round trip to skip, and the engine already keeps the plan it compiled for a statement under the text of that statement. Over two thousand rows on this machine a prepared statement bound per run costs 30.1 microseconds, the same text handed to
executeand bound per run costs 30.1, and a statement whose text differs every time costs 35.8. A statement of 1.2 KB with sixty predicates says the same thing: 34.9 microseconds either way.What preparing does buy is worth having anyway. The compile happens at the line that asked for it, so a statement that will not compile fails where it was written rather than in the middle of a loop.
paramssays which names the statement wants, which is the difference between binding what it asked for and finding out at the run that a key was misspelled. And it gives the intent a name: a prepared statement is an object a program can hold on a class, pass around and close.A prepared statement runs through the same call
executedoes, by way of aSourceenum inside the connection, so it gets the GIL released, the interrupt handling and the catalog names by being that call rather than a second path beside it.Explaining and profiling
Both calls answer twice over.
printgives the engine's own listing, rendered by the engine rather than assembled here so the two cannot drift apart from one release to the next, androotgives the same plan as objects, which is what a program walks: a test that wants to know a scan became a seek asks the tree rather than matching on a string written to be read. An operator carriesopfor what it is andnamefor what the listing calls it, which differ inside a bracket, so an expand under anOPTIONAL MATCHisopExpand,nameOptionalExpand,bracketOptional, and a program should match onopbecause that is the one that does not change with the company an operator keeps.explaintakes no parameters, which is not an oversight: a plan is chosen from the shape of the statement and the values are bound when it runs, so a plan asked for with values would suggest the values had changed it.profiledoes take them, because it is a run. Its counts areint, exact at any size, and an operator the optimizer had nothing to say about carriesNoneforestimate,boundandqerrorrather than a zero somebody would read as an estimate of none.stage.opsruns from the operator that read to the one that fed the sink, which is the reverse of the order the listing prints them, and there is a test that pins exactly that. Explaining costs 1.3 microseconds since the plan is already cached; profiling costs what the statement costs plus the counters, and a statement that writes is refused rather than profiled.The rest of the surface
zudb.aiomirrors all three, withAsyncPreparedopened byasync with, andstatement,paramsandclosedas properties there because the names were read at the compile and nothing has to wait to say them again. Seven new classes are exported fromzudband stubbed in_zudb.pyi, which the griffe gate checks against the built module.PlanandProfilehave__str__and_repr_html_, soprint(plan)in a shell and a plan in a notebook cell both give the listing, preformatted because the indentation is what says which operator pulls from which.Tests
Forty-two: nineteen for the lifetime of a prepared statement, covering closing twice, a closed one refusing, a connection that closed first, the
withblock and the block an exception leaves, and twenty-three for the shape of a plan and a profile, including the listing asserted against the tree it was rendered from rather than against a string written out here, which is a test that would otherwise fail every time the optimizer learned to print one better.Local gate is green:
pytest644 passed and 5 skipped,ruff check,ruff format --check,cargo clippy --all-targets -- -D warnings,cargo fmt --check.