Result Sets and Output Modes
Let the executor produce results and something else print them, then print them two ways
Separate producing a result from printing it: the executor hands over column names, rows and counts, and a printer chosen by .mode writes them as a table or as CSV. INSERT reports how many rows it added.
- A result is data, not text: column names, then rows, then how many; or a count of affected rows; or an error
- The printer: one place that turns a result into lines, so a new output format touches nothing else
- Streaming: the printer gets rows one at a time, as the executor produces them
- CSV and quoting: when a value can contain the separator, the format needs a way to say 'this comma is data'
- Session state: a setting that lasts until the session ends, and starts over in the next
Why this stage exists
So far the executor prints. The code that pulls rows from the plan also writes 1 | ada | [email protected] and (3 rows). That was fine while there was one way to show a result. There will be more: a second output mode in this stage, and later a network protocol that sends rows to a client instead of printing them. If every one of them lives inside the executor, the executor is mostly about formatting.
So this stage separates the two jobs. The executor produces a result: the column names, the rows one at a time, how many there were. Or, for an INSERT, how many rows it added. Or an error. A printer turns a result into lines. The table format you already print becomes one printer, and CSV becomes a second, chosen with .mode csv.
What changes
The executor no longer writes lines. It tells a printer what it has, in order, and the printer writes:
The rows still travel one at a time, as the Volcano executor produces them. A printer that needed every row before it could print the first one, to line up columns for instance, would have to keep the whole result in memory. Neither printer here needs to, so neither does.
The affected count
A SELECT's result is rows. An INSERT's result is a number: how many rows it added. The printer now says it: Insert statement executed successfully (1 rows).
An INSERT always adds exactly one row, so the count is always 1 here, and still rows, like the SELECT footer. It matters more in delete-update, where one statement can change any number of rows and the count is how you know. A statement that fails prints its error and no success line, as before.
CSV, and the comma that is data
CSV is comma-separated values: the header and each row on one line, the values separated by commas, and no footer. It is what a spreadsheet opens and what most tools import.
The problem is a value that holds a comma. Smith, Bob written as it is would read back as two values. So a value that contains a comma, or a double quote, is written between double quotes, and every double quote inside it is written twice:
| Value | Field |
|---|---|
ada | ada |
Smith, Bob | "Smith, Bob" |
Cy "the kid" | "Cy ""the kid""" |
a b | a b |
| (empty) | (empty) |
Nothing else is quoted: not spaces, not numbers, not empty strings. A row with only an id is 2,,. These are the common CSV rules, the ones spreadsheets follow when they read a file.
What the mode does not touch
Only SELECT's rows change with the mode. Error lines, the INSERT success line, [PLAN], [BTREE] and [PAGER] lines, and .help print exactly the same in both modes. They are not rows, so no printer formats them.
The mode lasts until the session ends. The next session, and every -c run, starts in table mode again.
The algorithm in words
Hand results to a printer
The executor stops printing. It tells a printer what it has, in order.
- 1.For a SELECT: first the names of the selected columns, then each row as the top plan node produces it, then the end, with the number of rows.
- 2.For an INSERT that succeeded: the number of rows it added, which is always 1.
- 3.For an error: the error, and nothing else.
- 4.The printer decides every character of the output. The executor never writes a line itself.
- 5.Invariant: in table mode every output is exactly what it was before this stage, apart from the count on the INSERT line.
Print as a table
The format of every earlier stage.
- 1.Columns: print the names joined by space, bar, space.
- 2.Each row: print its values joined the same way.
- 3.The end: print (n rows).
Print as CSV
One line per row, values separated by commas, and no footer.
- 1.Columns: print the names joined by commas.
- 2.Each row: write each value as a field and join the fields with commas.
- 3.A field is the value as it is, unless the value contains a comma or a double quote. Then write a double quote, the value with every double quote in it written twice, and a closing double quote.
- 4.The end: print nothing.
- 5.Invariant: reading the line back, splitting on commas outside quotes and undoing the doubled quotes, gives back exactly the values.
Change the mode
.mode is a meta command, like .help.
- 1.Read the word after .mode, ignoring case. table and csv choose that printer for the rest of the session, and print nothing.
- 2.No word, another word, or anything after the word is 01001, and the printer stays the one it was.
- 3.Every session starts with the table printer.
Output contract for this stage
These are the exact lines the tests check. The full definition is on the output contract page, sections 10 and 11.
| Input | Output |
|---|---|
| a successful INSERT | Insert statement executed successfully (1 rows) |
.mode csv, .mode table (any case) | nothing |
.mode alone, .mode <anything else>, .mode csv <more> | [ERROR:01001] <message>, and the mode does not change |
| SELECT in CSV mode | the column names joined by ,, then one line per row, values joined by ,; no footer |
a CSV value containing , or " | ", the value with each " doubled, " |
| any other line in CSV mode | exactly as in table mode |
What you should see when done
$droid --db users.dbWelcome to droid!>insert into users (id, name, email) values (1, 'ada', '[email protected]');Insert statement executed successfully (1 rows)>insert into users (id, name, email) values (2, 'Smith, Bob', '[email protected]');Insert statement executed successfully (1 rows)>insert into users (id, name, email) values (3, 'Cy "the kid"', '[email protected]');Insert statement executed successfully (1 rows)>insert into users (id, name, email) values (1, 'eve', '[email protected]');[ERROR:00701] Key 1 already exists>select * from users;id | name | email1 | ada | [email protected]2 | Smith, Bob | [email protected]3 | Cy "the kid" | [email protected](3 rows)>.mode csv>select * from users;id,name,email1,ada,[email protected]2,"Smith, Bob",[email protected]3,"Cy ""the kid""",[email protected]>select name from users where id > 5;name>btree find 2[BTREE] Find key=2: FOUND (page=0 cell=1)>.mode json[ERROR:01001] Unknown output mode>.mode table>select id, name from users where id = 2;id | name2 | Smith, Bob(1 rows)>.exitexiting.. good bye!
The CSV SELECT that matched nothing printed its header line alone. btree find printed the same lines it prints in table mode, comma and all. After the refused .mode json the session was still in CSV mode, until .mode table.
Common bugs in this stage
- The footer in CSV.
(3 rows)at the end of a CSV result is a fourth row to anything that reads the file. CSV has no footer. - Quoting everything, or nothing. Quoting every field is valid CSV to some readers and fails the tests, which follow the contract. Quote only a value with a comma or a double quote.
- Forgetting to double the quotes.
"Cy "the kid""ends the field at the second quote. Every"inside a quoted field is written"". - Quoting the padding. The value is the string up to its first zero byte, as in table mode, not the 28-byte field.
- CSV leaking into other lines. An error or a
[PLAN]line with a comma in it gets quoted, or loses its brackets. Only rows go through the CSV printer. - A refused mode switches anyway.
.mode jsonprints 01001 and then leaves the session in some third state. An error changes nothing. - The mode survives the session. Saving it with the database, or in a global that outlives a
-crun, makes the next session start in CSV. Every session starts in table mode. - Counting what failed. A duplicate INSERT prints
(1 rows), or(0 rows), after its error. A failed statement prints its error and nothing else.
Language notes
The page above is the same for every language. These are the only language-specific hints.
- C: a printer can be a struct of three function pointers, columns, row and end, with the table and CSV printers as two instances of it. The executor takes a pointer to whichever the session chose.
- C++: an abstract
Printerclass with virtualcolumns,rowandend, and two subclasses. - Rust: a trait with three methods, and the session holding a
Box<dyn Printer>, or a two-variantenumwith amatchin each method. - Zig: a tagged union of the two printers with a
switchin each operation is enough for two modes.
Checklist
One line per thing the tests check. The tags name the test cases.
- A successful INSERT prints
Insert statement executed successfully (1 rows); a failed one prints only its errorinsert-affected-count - The count is there with -c tooinsert-affected-count-c-mode
.mode csvprints nothing, and SELECT then prints a header line and one comma-separated line per row, with no footermode-csv-select- A field with a comma or a double quote is quoted, inner quotes doubled; spaces and empty strings are not quotedmode-csv-quoting
- A CSV SELECT with no rows prints the header alonemode-csv-empty-result
- CSV reads every leaf of a deep tree, in id ordermode-csv-deep-tree
.mode tablegoes back, and mode names ignore casemode-switch.modealone, an unknown mode or an extra word is 01001, and the mode does not changemode-errors- Errors, debug lines, plans, success lines and .help look the same in both modesmode-leaves-other-lines-alone
- Every session, and every -c run, starts in table modemode-per-session