PREPARE, EXECUTE, DEALLOCATE
Parse and check a statement once, give its values later, and run it as often as you like
Read $1, $2, ... as parameters; PREPARE a SELECT, INSERT, UPDATE or DELETE under a name, checking it once and giving each parameter a type from where it stands; EXECUTE it with literal values, refusing a wrong count or a value of the wrong type; DEALLOCATE one name or all; and keep every session's statements to itself.
This part needs a network, which the platform's test sandbox does not have, so its tests run on your computer. Download the test kit (it needs only Python 3) and your project, build your program, and run:
python3 run.py --stage sql-prepare --bin ./c-dbSign in to download the code you wrote here.
- A parameter is a hole in a statement where a value will go: $1, $2, ...
- Parse and check once, run many times: the work that does not depend on the values is done at PREPARE
- A parameter's type comes from where it stands: compared with an INT column, it is an INT
- Prepared statements belong to a session, like its transaction
- Values are still literals here: the protocol's own prepared statements, next, send them apart from the SQL
Why this stage exists
A web application runs the same few statements thousands of times, with different values. Look up the user with this id, insert this order. Every time, the server tokenizes the text, parses it, checks every name and type, and plans it, and all of that gives the same answer as the time before. Only the values changed.
A prepared statement splits the two. PREPARE does the work that does not depend on the values, once. EXECUTE supplies the values and runs.
$droid --db shop.dbWelcome to droid!>prepare by_id as select id, name from t where id = $1;Prepare statement executed successfully>execute by_id (2);id | name2 | bob(1 rows)[EXEC] rows_examined=1 index_lookups=1>execute by_id (1);id | name1 | ann(1 rows)[EXEC] rows_examined=1 index_lookups=1>deallocate by_id;Deallocate statement executed successfully
This stage is the SQL form of the idea, and it needs no network: it works in the REPL as well as over the wire. The next stage does the same through the protocol's own messages, which is what drivers actually use. The stage after that keeps the plan too, and deals with plans that go stale.
Where a parameter's type comes from
$1 has no type of its own. It gets one from where it stands. In where id = $1, it is compared with an INT column, so it is an INT. In values ($1, $2) it is assigned to a column, so it has that column's type. In $1 + 1 it is an operand of arithmetic, so it is an INT. Anywhere else nothing fixes it, and it is a VARCHAR.
Knowing the types at PREPARE time pays off at EXECUTE. A value that cannot be of its parameter's type is refused before anything runs: 'abc' for an INT parameter is 22P02. A string that holds a number, such as '2', is that number, because that is how PostgreSQL reads a quoted value for an int, and clients rely on it.
Every number from $1 to the highest one used must appear somewhere: a statement that uses $2 and never $1 is 42P18, the rule PostgreSQL also applies. A parameter that does appear but stands where nothing fixes its type is simply a VARCHAR.
A statement checked once
PREPARE does not merely store text. It parses the statement and checks it, as if about to run it. A missing table is reported at PREPARE, not at the first EXECUTE, and nothing is kept under that name. What survives PREPARE is a statement that was correct when it was prepared. Whether it is still correct after someone drops an index or a table is the plan-cache stage's question.
Prepared statements belong to the session, like its transaction. Another connection has its own names, and can use the same ones for different statements. When a session ends, its statements go with it.
The algorithm in words
PREPARE
Everything a statement needs that does not depend on the values happens here, once.
- 1.Parse the statement after AS. Only SELECT, INSERT, UPDATE and DELETE may follow; anything else is 00302. Each $k is a parameter, where a literal could be.
- 2.If the session already has a statement with this name, stop with 01910.
- 3.Give each parameter a type: whatever it is compared with (a column, a literal, an expression) or the column it is assigned to; INT if it is an operand of arithmetic; VARCHAR otherwise. The first place that fixes a type gives it, and a later place that needs another type is 00403. If a number between 1 and the highest one used never appears, stop with 01914.
- 4.Check it as if it were about to run: tables, columns, types. An error stops PREPARE with that error's own code, and nothing is kept.
- 5.Keep the checked statement and the types under the name, in the session.
EXECUTE
Fill the holes, then run the statement as if it had just been typed.
- 1.Find the name in the session, or stop with 01911.
- 2.Count the values. A different number than the statement has parameters is 01912.
- 3.Convert each value to its parameter's type: a string holding a number becomes that number for an INT; a string that is not a number, or a number given for a VARCHAR, is 01913. NULL is NULL.
- 4.Run the statement with the values in place of the parameters, and print what it prints.
DEALLOCATE
The end of a statement's life, or of all of them.
- 1.DEALLOCATE name forgets that statement, or is 01911 if there is none.
- 2.DEALLOCATE ALL forgets every statement of the session, and never fails.
- 3.When a session ends, its statements end with it.
Output contract for this stage
These are the exact lines, tags and codes the tests check. The full definition is on the output contract page, section 98.
| Statement | REPL prints | Tag over the wire |
|---|---|---|
PREPARE p AS ...; | Prepare statement executed successfully | PREPARE |
EXECUTE p (v, ...); | what the statement prints | the statement's |
DEALLOCATE p; | Deallocate statement executed successfully | DEALLOCATE |
DEALLOCATE ALL; | Deallocate statement executed successfully | DEALLOCATE ALL |
| Condition | droid code | SQLSTATE |
|---|---|---|
| PREPARE of a name already used | 01910 | 42P05 |
| EXECUTE or DEALLOCATE of an unknown name | 01911 | 26000 |
| a wrong number of values | 01912 | 42601 |
| a value not of its parameter's type | 01913 | 22P02 |
| a parameter number that never appears | 01914 | 42P18 |
What you should see when done
$droid --db s.dbWelcome to droid!>create table t (id int primary key, name varchar(20));Create table statement executed successfully>prepare ins as insert into t (id, name) values ($1, $2);Prepare statement executed successfully>execute ins (1, 'ann');Insert statement executed successfully (1 rows)>execute ins ('two', 'bob');[ERROR:01913] parameter $1 is an INT, and 'two' is not a number>execute ins (2);[ERROR:01912] ins takes 2 values, got 1>prepare ins as select 1;[ERROR:01910] a statement named ins is already prepared>deallocate all;Deallocate statement executed successfully>execute ins (3, 'cy');[ERROR:01911] no statement named ins is prepared>.exitexiting.. good bye!
Common bugs in this stage
- Substituting the values into the text and parsing again. It works, but it throws away the point of preparing. Worse, a value containing a quote can change the statement. Keep the parsed statement and put values into it.
- Checking only at EXECUTE. A PREPARE over a missing table must fail at once, and keep nothing.
- One global table of statements. Names belong to a session; two clients can both have a
p. - Forgetting statements when a session ends. The next session with that number is someone else.
- Reading
'2'as a string for an INT parameter. PostgreSQL clients send numbers that way, and expect it to work.
Language notes
The page above is the same for every language. These are the only language-specific hints.
- C: a small array of prepared statements in each session struct, searched by name. Parameters in the AST as a node holding their number, filled from an array of values at EXECUTE.
- C++: a
std::unordered_map<std::string, Prepared>in the session; aParam{index}alternative in the expression variant. - Rust:
HashMap<String, Prepared>in the session; anExpr::Param(usize)variant, replaced by walking the tree with the values. - Zig: a
std.StringHashMap(Prepared)owned by the session, freed with it.
Checklist
One line per thing the tests check. The tags name the test cases.
- A prepared statement runs with different values until it is deallocatedprepare-execute-deallocate
- A name is prepared once, and only a prepared name runs: 42P05 and 26000names
- EXECUTE gives exactly as many values as there are parameters: 42601wrong-argument-count
- A value must be of its parameter's type: 22P02 for 'abc', while '2' is the number 2type-mismatch
- Every parameter number up to the highest must appear: 42P18missing-parameter-number
- PREPARE reports what running the statement would, and keeps nothingprepare-checks-the-statement
- Prepared statements belong to their sessionper-session
- DEALLOCATE ALL forgets every statement of the sessiondeallocate-all
- Parameters stand for values in INSERT and UPDATE tooparameters-in-insert-and-update
- The three statements print as the REPL prints statementsrepl-lines