Simple Query: Rows Over the Wire
Run the SQL a Query message carries and send its result back as messages a driver can read: described columns, rows of text, a tag, and the signal that it is the client's turn
Answer a Query message by running its statement as the connection's session and sending what the REPL would print as messages: RowDescription with each column's name and type id, one DataRow per row with NULL as length -1, CommandComplete with the statement's tag, EmptyQueryResponse for an empty query, an ErrorResponse for a failure, and ReadyForQuery after every answer.
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 pg-simple-query --bin ./c-dbSign in to download the code you wrote here.
- Results and their presentation are separate: the same result becomes table text in the REPL and messages on the wire
- Four kinds of result: rows, a status, an error, nothing
- A column is described once, by its name and a type id; then every value travels as text
- NULL is not an empty string: the length -1 says 'no value', the length 0 says 'a value with no characters'
- The command tag is how a driver learns what happened and how many rows it touched
- ReadyForQuery ends every answer: the client may send again
Why this stage exists
Back in The Core Engine, the result-set stage made you separate what a statement produced from how it is printed: rows, a count of affected rows, a message, an error, each its own kind of result, with a printer deciding how it looks. Table mode and CSV mode were the first two printers. This stage adds a third, one that prints for programs instead of people.
When a client sends a Query, the server runs the statement exactly as a REPL line, as the session of that connection. Then, instead of printing text, it sends the result as messages:
After this stage, psql works, and so does any program using a PostgreSQL driver. Not every feature works, but a SELECT does.
Four kinds of result, four kinds of answer
| The REPL prints | The wire sends |
|---|---|
a header, rows, (n rows) | RowDescription, a DataRow per row, CommandComplete SELECT n |
| a success line | CommandComplete with a tag: INSERT 0 1, UPDATE 3, CREATE TABLE |
| an error line | ErrorResponse |
| nothing, for an empty line | EmptyQueryResponse |
ReadyForQuery comes after every one of them. It is the client's signal that the answer is complete, so a client that sent a query reads until it sees one. Everything the REPL adds for a person stays out: the [EXEC] line, the (n rows) footer, the column separators. The row count goes into the tag instead. The 0 in INSERT 0 1 is a relic PostgreSQL keeps for old clients (it was the new row's object id), and drivers expect it.
Describing columns: names and type ids
RowDescription comes first, once per result, and tells the client how many columns there are, what they are called and what type each one has. Types are not names but numbers, PostgreSQL's type ids (OIDs). A driver uses them to turn the text of each value into the right kind of value in its own language: 23 becomes an integer, 1043 a string, 16 a boolean.
- typebytes 0–0 (1 bytes) · 'T'
- lengthbytes 1–4 (4 bytes) · 27: everything after the type byte
- columnsbytes 5–6 (2 bytes) · 1
- "id"bytes 7–9 (3 bytes) · the name, with its zero byte
- tablebytes 10–13 (4 bytes) · 0: droid does not number tables
- columnbytes 14–15 (2 bytes) · 0
- type idbytes 16–19 (4 bytes) · 23: int4
- sizebytes 20–21 (2 bytes) · 4 bytes
- modifierbytes 22–25 (4 bytes) · -1: none
- formatbytes 26–27 (2 bytes) · 0: text
A RowDescription is sent even when no row matches. "Two columns and no rows" is a different answer from "no result at all", and a driver must be able to tell them apart.
Values are text, and NULL is not
Every value travels as text, written the way the REPL writes it: 42, ann, 1.50. A truth value is the exception: PostgreSQL sends t and f, and drivers expect those, so that is what goes on the wire where the REPL prints true and false.
Each value is preceded by its length. That length is also how NULL travels: -1 means "no value", with no bytes after it. An empty string is something else, a value whose length is 0:
- typebytes 0–0 (1 bytes) · 'D'
- lengthbytes 1–4 (4 bytes) · 15
- valuesbytes 5–6 (2 bytes) · 2
- lengthbytes 7–10 (4 bytes) · 1
- "1"bytes 11–11 (1 bytes) · the id, as text
- lengthbytes 12–15 (4 bytes) · -1: NULL, no bytes follow
Debug commands over the wire
tx status, sessions and the other debug commands are not SQL, but they are too useful to lose when you connect with psql. So they answer as rows: one text column named droid, one row per line the REPL would print, and the tag SHOW. explain does the same with a column named QUERY PLAN, as PostgreSQL's own EXPLAIN does, and the tag EXPLAIN.
The algorithm in words
Answering a Query
The connection's input buffer now holds typed messages. A Q message is one statement to run.
- 1.Take the string out of the message. If it holds nothing but spaces, line breaks and a ;, send EmptyQueryResponse, then ReadyForQuery, and stop.
- 2.Run the statement as the connection's session would run a line, without needing the final ;. A line break anywhere in the query is whitespace, like a space. Do not print: ask for the result instead.
- 3.Rows: send RowDescription for the result's columns, then one DataRow per row, then CommandComplete SELECT and the number of rows sent.
- 4.A status: send CommandComplete with the statement's tag, and the count for INSERT, UPDATE and DELETE.
- 5.An error: send an ErrorResponse with severity ERROR (not FATAL: the connection goes on), any SQLSTATE for now, and the message. pg-errors-txn-status says which SQLSTATE.
- 6.Always end with ReadyForQuery. Never send the [EXEC] line or anything else meant for a person at a keyboard.
Describing the columns
One RowDescription per result, before its first row, even when it has no rows.
- 1.For each column, its name as the REPL's header shows it: the column name, or the alias after AS.
- 2.Its type id from the contract's table: 23 for INT, 1043 for VARCHAR, 1700 for AVG's DECIMAL, 16 for a truth value. Then the type's size.
- 3.Table id 0, column number 0, type modifier -1, format code 0 for text.
Writing a row
Every value is text, so the REPL's own value formatting does most of the work.
- 1.Write the number of values, then for each value its length and its bytes.
- 2.A NULL is the length -1 and no bytes. It must never be written as the empty string or as the word NULL.
- 3.A truth value is the single character t or f.
- 4.Invariant: a DataRow's length equals 4, plus 2, plus for each value 4 and the length of its bytes. Get this wrong once and every message after it is read from the wrong place.
Output contract for this stage
These are the exact messages the tests check. The full definition is on the output contract page, section 95.
| Query | Messages, before ReadyForQuery I |
|---|---|
| a SELECT | T (name, type id per column), D per row, C SELECT <n> |
| an INSERT | C INSERT 0 1 |
| UPDATE, DELETE | C UPDATE <n>, C DELETE <n> |
| CREATE TABLE, DROP TABLE, CREATE INDEX, DROP INDEX | C with the statement's name: CREATE TABLE, ... |
"", ";", only spaces | I (EmptyQueryResponse) |
| a statement that fails | E (ErrorResponse, severity ERROR) |
| a debug command | T droid (25), D per line, C SHOW |
| droid type | Type id |
|---|---|
INT | 23 |
VARCHAR(n) | 1043 |
DECIMAL (from AVG) | 1700 |
| truth value | 16, values t and f |
What you should see when done
psql now works against your server:
$psql -h 127.0.0.1 -p 5433 -U droid droiddroid=> create table t (id int primary key, name varchar(20));CREATE TABLEdroid=> insert into t (id, name) values (1, 'ann');INSERT 0 1droid=> insert into t (id, name) values (2, null);INSERT 0 1droid=> select id, name, id > 1 as big from t order by id;id | name | big----+------+-----1 | ann | f2 | | t(2 rows)droid=> tx statusdroid-------------------[TX] Status: IDLE(1 row)
psql printed the table itself, in its own format, from your RowDescription and DataRows. It even right-aligned the id column, because type 23 told it the column is a number.
Common bugs in this stage
- A DataRow length that is off by a few bytes. The client then reads the next message from the wrong place, and fails on a message that is correct. Compute every length from what was actually appended, never by hand.
- NULL sent as an empty string, or as the text
NULL. Both break the difference that NULL exists to make. - Sending the
[EXEC]line, or(2 rows). They belong to the REPL's printer. The row count is in the tag. - No RowDescription for an empty result.
SELECT 0with no description leaves the driver without the columns. - Forgetting ReadyForQuery after an error or an empty query. The client then waits forever.
trueandfalsefor truth values. psql shows them, but drivers that parse booleans expecttandf.
Language notes
The page above is the same for every language. These are the only language-specific hints.
- C: give the printer an interface, a struct of function pointers such as
begin_rows,rowandstatus, with one implementation for the table, one for CSV and one that appends wire messages to a connection's output buffer. The engine calls the interface and never knows which one it has. - C++: a
ResultSinkbase class with virtual methods, and aWireSinkthat holds a reference to the connection. - Rust: a
trait ResultSink, implemented by the REPL printer and by aWireSink<'a>that borrows the connection's output buffer. - Zig: an interface struct holding a context pointer and function pointers, or a tagged union of the three printers, switched on where a result is emitted.
Checklist
One line per thing the tests check. The tags name the test cases.
- A SELECT is RowDescription with the columns' names and type ids, one DataRow per row, CommandComplete SELECT n and ReadyForQuery Iselect-sequence
- A statement with no rows sends CommandComplete alone: CREATE TABLE, INSERT 0 1insert-has-no-row-description
- UPDATE, DELETE, CREATE INDEX, DROP INDEX and DROP TABLE send their tags, with the row countscommand-tags
- NULL is the length -1 and an empty string the length 0null-vs-empty
- An empty query, a lone ; or only spaces get EmptyQueryResponseempty-query
- A SELECT matching nothing still sends its RowDescription, then SELECT 0zero-rows-still-described
- The end of the query ends the statement, and line breaks are spacesno-semicolon-and-lines
- Columns carry their type ids, and a truth value is t or fcolumn-types
- COUNT is described as an INT and AVG as numeric, two decimalsaggregate-types
- A debug command answers as rows of one text column, tag SHOWdebug-command-as-rows
- A failing statement sends an ErrorResponse and ReadyForQuery, and the next query runserror-then-next-query
- 5184 rows arrive as 5184 DataRows with the right valueslarge-result