droid
PREPARE, EXECUTE, DEALLOCATE
Prepared Statements · Stage 6 of 11

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.

Run the tests on your machine

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-db
Download the test kit

Sign in to download the code you wrote here.

Concepts in this stage
  • 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
Where this stage sits in the engine
Talking to the user
REPL
Sessions
PrinterServerWire protocol
Understanding SQL
Lexer
Parser
Validator
Answering a query
PlannerExecutor
Keeping changes safe
TransactionsWrite-ahead logUndo logLock managerRead views
Storing rows
Row formatSlotted pageB+Tree
Reaching the disk
PagerDatabase file
this stagebuilt alreadystill to comeA statement enters at the top; rows come back up.

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.

shell
$droid --db shop.db
Welcome to droid!
>prepare by_id as select id, name from t where id = $1;
Prepare statement executed successfully
>execute by_id (2);
id | name
2 | bob
(1 rows)
[EXEC] rows_examined=1 index_lookups=1
>execute by_id (1);
id | name
1 | 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

1

PREPARE

Everything a statement needs that does not depend on the values happens here, once.

  1. 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. 2.If the session already has a statement with this name, stop with 01910.
  3. 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. 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. 5.Keep the checked statement and the types under the name, in the session.
2

EXECUTE

Fill the holes, then run the statement as if it had just been typed.

  1. 1.Find the name in the session, or stop with 01911.
  2. 2.Count the values. A different number than the statement has parameters is 01912.
  3. 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. 4.Run the statement with the values in place of the parameters, and print what it prints.
3

DEALLOCATE

The end of a statement's life, or of all of them.

  1. 1.DEALLOCATE name forgets that statement, or is 01911 if there is none.
  2. 2.DEALLOCATE ALL forgets every statement of the session, and never fails.
  3. 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.

StatementREPL printsTag over the wire
PREPARE p AS ...;Prepare statement executed successfullyPREPARE
EXECUTE p (v, ...);what the statement printsthe statement's
DEALLOCATE p;Deallocate statement executed successfullyDEALLOCATE
DEALLOCATE ALL;Deallocate statement executed successfullyDEALLOCATE ALL
Conditiondroid codeSQLSTATE
PREPARE of a name already used0191042P05
EXECUTE or DEALLOCATE of an unknown name0191126000
a wrong number of values0191242601
a value not of its parameter's type0191322P02
a parameter number that never appears0191442P18

What you should see when done

shell
$droid --db s.db
Welcome 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
>.exit
exiting.. 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; a Param{index} alternative in the expression variant.
  • Rust: HashMap<String, Prepared> in the session; an Expr::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 deallocated
    prepare-execute-deallocate
  • A name is prepared once, and only a prepared name runs: 42P05 and 26000
    names
  • EXECUTE gives exactly as many values as there are parameters: 42601
    wrong-argument-count
  • A value must be of its parameter's type: 22P02 for 'abc', while '2' is the number 2
    type-mismatch
  • Every parameter number up to the highest must appear: 42P18
    missing-parameter-number
  • PREPARE reports what running the statement would, and keeps nothing
    prepare-checks-the-statement
  • Prepared statements belong to their session
    per-session
  • DEALLOCATE ALL forgets every statement of the session
    deallocate-all
  • Parameters stand for values in INSERT and UPDATE too
    parameters-in-insert-and-update
  • The three statements print as the REPL prints statements
    repl-lines