droid
Simple Query: Rows Over the Wire
Wire Protocol · Stage 3 of 11

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.

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 pg-simple-query --bin ./c-db
Download the test kit

Sign in to download the code you wrote here.

Concepts in this stage
  • 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
Where this stage sits in the engine
Talking to the user
REPLSessions
Printer
Server
Wire protocol
Understanding SQL
LexerParserValidator
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

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:

Drawing the diagram…

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 printsThe wire sends
a header, rows, (n rows)RowDescription, a DataRow per row, CommandComplete SELECT n
a success lineCommandComplete with a tag: INSERT 0 1, UPDATE 3, CREATE TABLE
an error lineErrorResponse
nothing, for an empty lineEmptyQueryResponse

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.

0
54
00
00
00
1b
00
01
69
64
00
00
00
12
00
00
00
00
00
00
00
17
00
04
ff
ff
24
ff
ff
00
00
  • 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
The RowDescription for select id from t. Every column repeats the last seven fields; only the name, the type id and the size change from one droid column to the next.

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:

0
44
00
00
00
0f
00
02
00
00
00
01
31
ff
ff
ff
15
ff
  • 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
A DataRow for the row (1, NULL). The row (2, '') would carry the length 0 in the last field, and then nothing either. Only the length tells the two apart.

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

1

Answering a Query

The connection's input buffer now holds typed messages. A Q message is one statement to run.

  1. 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. 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. 3.Rows: send RowDescription for the result's columns, then one DataRow per row, then CommandComplete SELECT and the number of rows sent.
  4. 4.A status: send CommandComplete with the statement's tag, and the count for INSERT, UPDATE and DELETE.
  5. 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. 6.Always end with ReadyForQuery. Never send the [EXEC] line or anything else meant for a person at a keyboard.
2

Describing the columns

One RowDescription per result, before its first row, even when it has no rows.

  1. 1.For each column, its name as the REPL's header shows it: the column name, or the alias after AS.
  2. 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. 3.Table id 0, column number 0, type modifier -1, format code 0 for text.
3

Writing a row

Every value is text, so the REPL's own value formatting does most of the work.

  1. 1.Write the number of values, then for each value its length and its bytes.
  2. 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. 3.A truth value is the single character t or f.
  4. 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.

QueryMessages, before ReadyForQuery I
a SELECTT (name, type id per column), D per row, C SELECT <n>
an INSERTC INSERT 0 1
UPDATE, DELETEC UPDATE <n>, C DELETE <n>
CREATE TABLE, DROP TABLE, CREATE INDEX, DROP INDEXC with the statement's name: CREATE TABLE, ...
"", ";", only spacesI (EmptyQueryResponse)
a statement that failsE (ErrorResponse, severity ERROR)
a debug commandT droid (25), D per line, C SHOW
droid typeType id
INT23
VARCHAR(n)1043
DECIMAL (from AVG)1700
truth value16, values t and f

What you should see when done

psql now works against your server:

shell
$psql -h 127.0.0.1 -p 5433 -U droid droid
droid=> create table t (id int primary key, name varchar(20));
CREATE TABLE
droid=> insert into t (id, name) values (1, 'ann');
INSERT 0 1
droid=> insert into t (id, name) values (2, null);
INSERT 0 1
droid=> select id, name, id > 1 as big from t order by id;
id | name | big
----+------+-----
1 | ann | f
2 | | t
(2 rows)
 
droid=> tx status
droid
-------------------
[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 0 with no description leaves the driver without the columns.
  • Forgetting ReadyForQuery after an error or an empty query. The client then waits forever.
  • true and false for truth values. psql shows them, but drivers that parse booleans expect t and f.

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, row and status, 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 ResultSink base class with virtual methods, and a WireSink that holds a reference to the connection.
  • Rust: a trait ResultSink, implemented by the REPL printer and by a WireSink<'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 I
    select-sequence
  • A statement with no rows sends CommandComplete alone: CREATE TABLE, INSERT 0 1
    insert-has-no-row-description
  • UPDATE, DELETE, CREATE INDEX, DROP INDEX and DROP TABLE send their tags, with the row counts
    command-tags
  • NULL is the length -1 and an empty string the length 0
    null-vs-empty
  • An empty query, a lone ; or only spaces get EmptyQueryResponse
    empty-query
  • A SELECT matching nothing still sends its RowDescription, then SELECT 0
    zero-rows-still-described
  • The end of the query ends the statement, and line breaks are spaces
    no-semicolon-and-lines
  • Columns carry their type ids, and a truth value is t or f
    column-types
  • COUNT is described as an INT and AVG as numeric, two decimals
    aggregate-types
  • A debug command answers as rows of one text column, tag SHOW
    debug-command-as-rows
  • A failing statement sends an ErrorResponse and ReadyForQuery, and the next query runs
    error-then-next-query
  • 5184 rows arrive as 5184 DataRows with the right values
    large-result