Editing data
The rows of a result can be changed in the grid: a cell’s value, new rows, rows to delete. Nothing is sent while you edit. The changes stay pending on the result until you look at the statements that apply them and say yes; then they run in one go, each row checked first.
What can be edited
The first key that changes a cell or a row reads the table’s details fresh (the status bar
says “Loading the table’s details…”) and then does what was pressed. From then on the result tab
strip says what the rows are to editing: editable · shop.users · key id (the key that finds a
row), rows matched by values (no key in the result: a row is found by the values it shows), or
read-only · <reason> for a reason about the statement, the table or finding the rows. The
other reasons below, the connection’s among them, are said by the key only.
A result is editable when its statement is one SELECT of one plain table, its connection is
open and may write, and its rows can be found again. A pressed key that cannot do its work says
“Read-only: …” or “Can’t edit: …” and changes nothing. The reasons are:
- The statement. It is not a single
SELECT; it usesWITH; it reads more than one table (a join or a comma inFROM); it has a subquery in the select list or inFROM; it combines results (UNION,INTERSECT,EXCEPT); it groups rows (GROUP BY,HAVING,DISTINCT,DISTINCT ON); it uses a window function;FROMnames no plain table (a function,LATERAL,VALUES, a sample) or there is none; its columns do not come from the table named inFROM; it reads a view. - The table. Its columns are not known yet (the keys are still being read, or could not be read), or it is not known at all (created outside the app: the explorer’s refresh reads it again); no column of the result is a column of the table; the rows are not of a plain table (a view, a materialised view or a foreign table); the table changed since the query ran; its details could not be read.
- Finding the rows. No column of the result compares exactly, so its rows can’t be found again (see How a row is found).
- A selection. Its cells are of more than one column (“Select cells of one column to edit
them together”); it holds both new rows and rows read (“Select new rows or rows read, not both,
to edit them together”);
Don whole columns (“Select rows (V) to delete them, not whole columns”). - The connection. The tab’s connection is not open; the connection’s environment is read-only (or was when the tab’s session opened); this connection type can’t edit data; the connection changed since these rows were read (run the query again).
- A column. It is computed in the query, not a column of the table; it is generated by the
database; it is
GENERATED ALWAYS AS IDENTITY(it takes no new value in a row read, but a new row may give one); it is shown more than once. - A value. A row is not shown whole, so it can’t be found again; a column’s values (or one
value) are not shown as stored, for example text read in another character set than
utf8mb4or a MySQLTIMESTAMPin a time zone that is not a fixed offset; a MySQLFLOATcolumn is shown rounded, so its selected cells are not typed together (one at a time is); a row could not be read; a row is marked for deletion; no column of the result takes a value in a new row. - While applying or after. The pending changes are being applied; changes were applied to these rows (re-run the query to edit them); the table’s details are no longer known while changes are pending (run the query again).
Keys
In the grid:
| Keys | What it does |
|---|---|
e (Space d e) | Edit the cell, or the selected cells of one column |
Space d n | Set the cell (the selected cells) to NULL |
Space d D | Set the cell (the selected cells) to the column’s default |
o (Space d o) | Add a new row, every value its column’s default |
O (Space d O) | Add a new row with the values of the cursor’s row |
D (Space d d) | Mark the row (the selected rows) to be deleted, or unmark it; a new row is removed |
Space d r | Drop the pending changes of the cell (the selection) |
u (Space d u), Ctrl+R (Space d U) | Undo, redo |
Space d p (:changes) | Show the pending changes in the Changes tab |
Space d a (:apply) | Apply them, once asked on the status line (in a dialog when one is needed) |
Space d x (:discard) | Drop every pending change of the result (it asks first) |
Your terminal’s paste (Cmd+V, Ctrl+Shift+V) | Paste a range at the cursor |
Space d v | Paste the system clipboard at the cursor (with clipboard = "system", or "auto" outside SSH) |
Space d V | Read the last paste again as the next format, in its place |
The full lists are in the keybindings reference: the grid,
the Space d group under nav, the
cell editor, the
editor of several lines, the
Changes tab, the
status line question and the
Apply dialog.
While changes are pending the strip shows 3 pending and a Changes tab, and the hint line
offers Space d a apply and u undo. A pending change is a row: a row read with changed cells
(two cells of one row are one change), a new row or a row to be deleted. The row number’s
gutter marks them ~, + and -, or ! when an Apply is refused for the row. Changed cells
take their own colour, new rows another on a background of their own, rows to be deleted are
crossed out, and a new row’s cell left to its default shows what the server will put there, or
! value needed where the column has none (see New rows). The theme tokens are
edit_changed, edit_inserted, edit_inserted_row, edit_deleted, and edit_pending for the
count (see Theme files).
With the mouse
A right click on a cell selects it and opens the action menu; inside a selected range it keeps
the range, and the menu is the selection’s (Space Space opens the same one next to the
selection). Its first section lists the edits after the copies, each with its key: edit the
cell, set it to NULL or to the column’s default, add a row, add a copy of the row, mark the row
to be deleted; once changes are pending, undo, showing the changes (the Changes tab) and
dropping them. On a new row it lists only its edits: nothing there reads a row of the result. An item
runs as its key does.
The menu offers the edits whose keys would act: each is checked as its key checks before it
changes anything. An edit that would refuse is shown dimmed, with why in a few words where its
key would be; the keys and the wheel step over it, and a click on it says the reason in full
(as its key does) and keeps the menu open. A result that can’t be edited (a join, a read-only
environment, a connection that is not open, a connection type that can’t edit data) shows that
once, on “Edit the cell” (for example read-only, not connected, not editable), and offers
no other edit but undo, the Changes tab and dropping the changes already pending (none of them
while those are being applied). On a result that can be edited, an edit that does not act on
this cell or selection is dimmed not here: for example setting NULL on a NOT NULL column,
setting the default of a column that has none, editing cells of several columns at once,
editing a column the server computes, deleting whole columns, or changing a cell of a row
marked to be deleted. Adding a row to a table none of whose columns shown takes a value is
dimmed no column to fill. Undo, the Changes tab and dropping the changes are listed only
while changes are pending. Until the first edit key has read the table’s details, what its columns take is not
known yet: the menu then offers NULL and DEFAULT on any column, and the key says why when one
refuses it.
The cell editor
e opens a line over the cell with its pending value, or its value as read. A NULL or a
pending DEFAULT opens as an empty line.
Enterkeeps the text. An empty text is the empty string, never NULL:Ctrl+NandCtrl+Dset NULL and DEFAULT again.Esccancels; so does a click outside the line.Ctrl+Ltakes the text typed so far to the editor of several lines.- With cells of one column selected, each takes the value, as one change (
uundoes it). - An enum or boolean column lists its values instead, then NULL and
DEFAULTwhere the column takes them.
NULL is refused in a NOT NULL column. In a row read, DEFAULT is refused when the column has
no default, and when its default would take a new number (an identity, AUTO_INCREMENT, a
sequence): type a value instead. A new row takes any column’s default.
Values of several lines
A value with line breaks, and a JSON column’s, opens in a box with the query editor’s vim keys
instead (Ctrl+L gets there from the line too). In Normal mode Enter or :w (:wq, :x)
keeps the text, Esc or :q (:q!) cancels; / searches and :s substitutes as in the
query editor. A value whose line breaks are all \r\n keeps them; one that mixes kinds of line
break is refused (“mixes kinds of line break; it can’t be edited here”). The one-line editor
does not take a paste with line breaks.
Value checks
A typed value must be a value of the column’s type, in the form the grid shows it; it is refused, with the reason, when it would be stored as something else. Some of the reasons:
- “not a whole number”, “out of the range of smallint”, “not a number”, “more digits than numeric(6,2) holds”
- “not true or false”, “not a UUID”, “not one of the column’s values”
- “not a date (YYYY-MM-DD)”, “not a date and time (YYYY-MM-DD HH:MM:SS)”, “more than 3 digits of
a second” (a PostgreSQL value with a time zone needs its offset, such as
+09) - “longer than 20 characters”, “ends in spaces, which a CHAR column drops” (MySQL)
- “not bytes as shown: \x and hexadecimal digits in pairs” (MySQL:
0xand upper-case digits) - “real would store it rounded, as …” (a floating-point value must read back as typed)
What the app cannot judge, such as a check constraint, is left to the server.
New rows
o adds a row with every value its column’s default. O copies the cursor’s row (its pending
values, else as read), except what the table gives a new row itself: the key’s columns, a
number drawn (an identity, AUTO_INCREMENT, a sequence), generated and computed columns are
left to their default. A value that is not shown whole is not copied: the clone is refused,
naming the column.
New rows appear at the end of the result, after the rows read, in the order they were added
(on a new row, o and O put the next one right after it). The cursor goes to the new row.
D on a new row removes it. A new row is tinted as a whole (theme token edit_inserted_row:
its cells, the separators and its number), and the gutter numbers it after the rows read
(4+, 5+ after three rows; rows an Apply added are numbered the same way). The number counts
the rows read so far: when the next page is read, the new rows’ numbers move after it. The
messages name a new row as the Changes tab does, by its place among the new rows (new 1).
A cell left to its column’s default shows, muted, what the server will put there:
| Column | Shown |
|---|---|
a constant default ('ACTIVE'::character varying, 0, '{}'::jsonb) | its value: ACTIVE, 0, {}; a text that reads like one of these marks (empty, NULL, <…>) in quotes |
an identity, AUTO_INCREMENT, a sequence (serial), a generated column | <generated> |
another default (now(), CURRENT_TIMESTAMP) | <default: now()>, a long one cut short |
| no default, takes NULL | NULL |
no default, NOT NULL | ! value needed (below) |
This is only shown: the Apply leaves the cell out of the INSERT, so the server applies its own
default, and the cell editor opens empty. The column widens to fit what it shows, up to the
grid’s widest column. Copies of new rows are refused, so nothing shown here is ever copied.
The Changes tab lists, and an Apply refuses, a new row that leaves a NOT NULL column without
a default to its default (“new 1 · name is NOT NULL and has no default; give it a value”), and
one of a table with such a column that the result does not show (“…is not in the result; add
it to the SELECT”).
The grid shows both where they are. A cell that needs a value reads ! value needed in the
theme token edit_invalid (its column widens to fit it, up to the grid’s widest column), and
the row’s gutter shows ! after its number instead of +. The cell editor opens
saying “Can’t edit: name has no default”, and the inspector gives the Changes tab’s reason. A
column the result does not show has no cell to mark: the row’s gutter shows !, and the Changes
tab names the column. The badge counts both with the invalid values. Typing the values clears
the marks.
Pasting a range
Cells copied from a spreadsheet, another grid or a text file paste into the grid with the
terminal’s own paste (Cmd+V, Ctrl+Shift+V): put the cursor on the top-left cell the range
goes to and paste. The terminal sends the text itself, so the app reads no clipboard: this works
over SSH and in tmux, whatever clipboard is set to. The paste lands as pending changes, like
typed values: marked in the grid, listed in the Changes tab, and nothing is sent until
Apply.
The status line says how the text was read and what it did:
Pasted as TSV · 3 rows × 4 columns · 1 invalid (id=14 · price) · 5 cells changed · 1 new row · u undo
Where the status bar is too narrow for all of it, the counts of cells and new rows go first,
then the invalid cell’s name: the format, the size, how many values are invalid and the undo key
stay (Pasted as TSV · 3 rows × 4 columns · 1 invalid · u undo); at its narrowest the format goes too (Pasted · 3×4 · 1 invalid · u undo).
A first line whose fields are exactly the names of the columns the paste lands in, in order
from the cursor’s column (a copy with its header line, from the app or a spreadsheet), is a
header, not values: it is skipped, and the status line says so first
(Pasted as TSV · Header row skipped · Space d V keeps it · …). Space d V reads the text again
keeping that line, in the paste’s place. A single line is never skipped.
Formats
The text is read as:
- TSV when it has a tab outside quotes (a spreadsheet’s copy),
- else, when it has a comma outside quotes, CSV or one column (one value for a single line): which one is asked, never guessed (below),
- else one column, a value per line, when it has several lines,
- else one value.
Text with commas and no tab can be CSV, or a column whose values hold commas (1,5, Smith, John). The status line asks:
Read as CSV (3 columns) or one column? c / 1 · any other key cancels
c pastes it as CSV, 1 as one column (… or one value? for a single line). Esc, any other
key, a click or another paste cancels (“Paste cancelled”) and does nothing else. The answer then
pastes as any paste does: its refusals, its question about many rows and the status line naming
the format. When only one of the two reads the text, only that key is offered: Not CSV (row 2 has 2 values, row 1 has 1); read as one column? 1. When neither does, the paste is refused
with the CSV reason. The question is for the grid as it was: a change, another tab or a
dialog drops it (“Paste cancelled: the rows changed”). It works the same for the
terminal’s paste and for Space d v.
Both TSV and CSV take quoted fields ("a""b", line breaks inside quotes), as spreadsheets write
cells that hold tabs, line breaks or quotes. A quote in the middle of a field that does not start
with one is part of the text. Line ends may be \n or \r\n; a carriage return alone outside
quotes is refused. Empty lines inside the block are rows of empty values. Every row must have as
many values as the first: row 3 has 4 values, row 1 has 3 is refused, never padded or shifted.
When the text was read the wrong way, Space d V reads the last paste again as the next format, TSV → CSV → one value → TSV: it undoes
the paste and pastes the same text again at the same cursor and selection. It works while that
paste is the newest change, again after undoing a later one; otherwise it says “Nothing to
re-read: the last change was not a paste”. When the new reading is refused, or its question
about many rows is cancelled, the paste stays as it was and the next Space d V tries the
format after that one. Text that can’t be read at all (a quote never closed, rows of different widths, a lone carriage
return) names Space d V and the format it tries next.
Where it goes
Columns go by the grid’s column order from the cursor’s column; rows go down the grid’s rows: the rows read, then the new rows.
| Clipboard | Selection | Result |
|---|---|---|
| one value | one cell | sets that cell |
| one value | several cells | sets every selected cell (each checked for its own column) |
| a block | the cursor | the block from the cursor |
| a block | several cells | the block from the selection’s top-left corner; the selection’s size does not matter, and the status line says pasted 3×4 at the selection's corner |
Rows past the last row become new rows at the end of the result, as o adds them: the
columns outside the pasted range are left to their default, and the key’s columns, identities
and AUTO_INCREMENT columns too unless the range sets them. A value equal to the cell’s value
as read leaves the cell unchanged; if the cell had a pending edit, that edit is dropped. The cursor goes to the range’s first cell.
When a paste is refused
A paste that does not fit is refused as a whole, saying why, and nothing changes:
- wider than the columns from the cursor to the last one: “the paste is 5 columns wide; 3 columns from the cursor to the last column”
- a column in the range that takes no value (computed in the query, generated, an identity that is always generated, a column the result has twice)
- a row in the range marked for deletion: “row 12 is marked for deletion”
- rows past the last row read while the server has more rows: “the result has more rows on the server; fetch them (n) or paste fewer rows”. Otherwise rows meant to overwrite the rows not fetched yet would become new rows. When the result’s cursor is no longer open (the next page runs the query again, or there is none), it says “the result has more rows on the server, not read here; paste fewer rows”. One value filling a selection adds no row, so it is not refused for this.
- a row in the range that can’t be found again (its values are not shown whole)
- rows past the end where no row can be added: no column of the result takes a value in a new
row, or the table has a
NOT NULLcolumn without a default that the result does not show - a result that can’t be edited, or whose changes are being applied
- more than 5,000 rows or 1 MiB of text
In the Changes tab, or with the messages, a plan or a chart shown, a paste says “Paste into the grid”. With the cell editor open, a paste goes into the editor as before (the one-line editor takes no line breaks).
A paste that changes more than 1,000 rows (rows with a value set to another one, and new
rows) asks first on the status line: Paste 2,400 rows (1,900 new)? y paste · any other key cancels. Any other key, a click or
another paste cancels it and does nothing else.
NULL and empty values
An unquoted field that is exactly \N is NULL, the form both servers’ own dumps write NULL in.
The setting edit.paste_null (:set edit.paste_null=NULL, or the settings screen) makes the
word NULL the token instead, or none for no token. A quoted "\N" is always the text.
An empty field is an empty value, never NULL. For a text column that is the empty string; a
column of another type does not take it, so it is marked invalid with “empty value (paste \N
for NULL)”. A paste never gives DEFAULT: the text DEFAULT is the text.
Invalid values
Every pasted value is checked as a typed one is (see Value checks), and NULL
against NOT NULL. Nothing is converted: a value its column does not take is kept, so you see
it, and marked:
- the row’s gutter shows
!(over~and+), the cell is in the theme tokenedit_invalid, its value still shown, also under the cursor; the gutter’s!also marks a new row an Apply is refused for (see New rows) - the badge counts them:
5 pending · 1 invalid - the Changes tab lists each under its row, with the reason: “price: not a number: ‘abc’” (elsewhere with the row named: “id=14 · price: not a number: ‘abc’”)
- the changes are not applied while any value is invalid (Apply shows the Changes tab instead)
Fix a value with e: the editor opens with the pasted text and says why it is not valid (“Not
valid: not a number: ‘abc’”). Or
drop it with Space d r, or undo the paste.
Undo
A paste is one change: u undoes all of it, the new rows too, and Ctrl+R redoes it.
Space d v and the clipboard setting
Space d v reads the system clipboard and pastes its text at the cursor, the same way. It reads
the clipboard only when you press it, and only with clipboard = "auto" (outside SSH) or
"system"; with clipboard = "osc52" (and auto over SSH) it says “Reading the clipboard needs
clipboard = “system”; paste with your terminal instead“. A clipboard that does not answer within
2 seconds pastes nothing.
A terminal without bracketed paste sends a paste as typed keys instead: they act as keys in the
grid. Use Space d v there, where a system clipboard exists.
Pasting the app’s own copies
The grid’s own TSV and CSV copies (Space r y) paste back as the same cells, with three
exceptions: a text cell that is exactly the NULL token (\N by default) pastes back as NULL; NULL is copied as an empty
field, so it pastes back as an empty value; and in a copy of one column, a last cell that is
NULL (in a TSV copy, an empty text too) is lost, since the copy does not end its last line.
To move cells within the app without these exceptions, use y and g p
(below).
Copy and paste inside the grid
y (a cell, or the selected range) and Y (the row, or the selected rows) copy to the
clipboard as before, and the app also keeps the cells themselves: each value exactly as the
grid shows it, a pending change included, NULL as NULL, and never a header line. They stay,
for this result or any other, in any tab, until you copy again or quit. No clipboard is read or
written for them, so this works over SSH and in tmux, whatever clipboard and
[editor] clipboard are set to. Two keys paste them:
p(Space d Y) adds them as new rows after the last row, wherever the cursor is. Eachpadds another copy, one stepuundoes each.- Copied from this result’s table, each value goes to its own column, never by position: a
column not copied is left to its default (one that needs a value is marked), a column the
table computes or numbers itself (
GENERATED,GENERATED ALWAYS AS IDENTITY) is left to the server (“… · 1 generated column left to the server”), and the paste is refused when a copied column is not in this result, is copied twice, or is computed or of another table (a join): it names the column. - Copied from another table, or from a result of computed columns only, the values go by position from the first column, each checked like a typed value, and a copy wider than the result is refused.
Ythenpduplicates rows with their key values as they are: a serial orBY DEFAULTidentity key then repeats, so set it to its default (Space d D) before Apply.- A copy from another connection or database goes by position.
- Copied from this result’s table, each value goes to its own column, never by position: a
column not copied is left to its default (one that needs a value is marked), a column the
table computes or numbers itself (
g p(Space d y) pastes them over the cells at the cursor, the way a pasted range does: from the cursor (one cell copied fills every selected cell), and rows past the last one are new rows.
Both are refused the way a paste is (a read-only result, more rows on the server, a table with
a required column the result does not show, …), are one step u undoes, and ask first when
more than 1,000 rows change. Nothing is read as text, so there is no question about commas, and
Space d V has nothing to read again.
Pasted the copied cells as new rows · 2 rows × 2 columns · 2 new rows · u undo
Pasted the copied cells · 2 rows × 2 columns · 2 cells changed · 1 new row · u undo
Each value is checked against the column it lands in, as a typed one: a text pasted into a number column is kept and marked invalid, and the changes are not applied until it is fixed.
What is not kept says why, and the copy to the clipboard is done (or fails) anyway:
- more than 5,000 rows, the most a paste takes (“Not kept for p: more than 5,000 rows, the most a paste takes”);
- more than 32 MiB of values (“Not kept for p: more than 32 MiB of values”);
- a row not read whole;
- a MySQL value the grid does not show as stored: a
FLOAT(the server sends it rounded), bytes or bits not in their form, text read in a character set other thanutf8mb4, aTIMESTAMPread in a time zone without a fixed offset (one with daylight saving, orSYSTEMwhen the server’s own zone is not known), and text or aTIMESTAMPof a result whose session’s character set and time zone are not known, or changed while its rows were read.
A copy the grid refuses (a pending DEFAULT, a value only a run shows, a new row) or asks about
keeps nothing either, so g p never pastes an older copy; with nothing kept, it says which keys
copy.
A MySQL TIMESTAMP is shown in the session’s time zone, without an offset, so the same text is
another instant in another zone. Its cells keep the fixed offset they were read in (SYSTEM is
the server’s own zone), and g p pastes them into a time column with a zone only when the
instant stays the same; else it says why and pastes nothing:
- a MySQL
TIMESTAMPof a result read in another offset (“Not pasted: column “created_at” was copied in time zone +09:00; this result was read in +00:00”), in a zone without a fixed offset, inSYSTEMwhen the server’s own zone is not known, in a session whose zone is not known, or one whose zone changed while its rows were read: set the session’stime_zoneto the copy’s and run the query again; - a PostgreSQL
timestamptz, which reads such text in its session’s zone.
A TIMESTAMP value pending in a result read in a zone without a fixed offset, in a zone not
known, or in a session whose zone changed while its rows were read is not kept either. Into a column without a zone (DATETIME, timestamp, text) the text pastes as
it was shown.
Undo, revert, discard
uundoes the last change of the result andCtrl+Rredoes it. A change of several selected cells, a delete of several rows, a paste and a discard are one step each.Space d rdrops the pending changes of the cell, or of the selection: a value goes back to the value as read, a row marked to be deleted is no longer marked, and a new row’s cell goes back to its default (a cloned row’s too, not to the copied value).Space d x(:discard) asks “Drop the pending changes?”:ydrops them,norEnterkeeps them.
The Changes tab
While the shown result has pending changes, the result tab strip has a Changes tab after its
result tabs: Changes 3, or Chg3 where the strip is narrow. A narrow strip keeps it while it
fits next to the shown tab and Messages. Space d p (:changes) shows it with the focus; H
and L (Space r [ and Space r ]) reach it as any result tab. It shows the changes of the
result last shown in the grid. After an Apply it stays, as Applied (Appl), to show what was
applied (see After an Apply). A plain Changes stays after the applied
changes were rolled back, and after an Apply whose result is not known once its pending changes
are dropped. It goes once nothing is pending and nothing was applied, or when the query runs
again. When another result of the tab
has pending changes too, a line says so: Result 2 also has 1 pending.
Its first line names the result and the table, and says how many updates, inserts, deletes and problems there are. Then one line per change, deletes first, then updates (a line per changed cell), then new rows, each row’s problems under it:
Changes: Result 1 · shop.users 2 updates · 2 inserts · 1 delete · 1 problem
- id=2 delete
~ id=1 email 'a@x' → 'z@x'
~ id=3 nickname 'cy' → NULL
+ new 1 email='n@x' · DEFAULT ×2
+ new 2 DEFAULT ×3
! email is NOT NULL and has no default; give it a value
- The marks are the gutter’s:
~a changed cell,+a new row,-a row to be deleted, in their colours (edit_changed,edit_inserted,edit_deleted); they read without colours too. - A row found by its key is named by the key’s values (
id=2,a=1,b=2); a row found by its values by its row in the grid,row 31 (located by its values); a new row by its place among the new rows,new 1. A row is named the same way wherever a problem is about it: in the statements, the Apply’s messages, the cell editor and the inspector (new 1 · email is NOT NULL …). - Values are literals as they were read and as they are to be (
'text',NULL,DEFAULT), a long text cut at 40 characters with…. A new row lists the values set, then how many of its columns are left to their default (DEFAULT ×2, left out when none is); a column left to a default it lacks is named in the problem under the row. A line longer than the pane is cut with…. - Control characters in values, column names and the table’s name are spelled out:
\n,\r,\t,\x07(\u{85}for one above\x7f), and so are the characters that turn the direction of text or are not seen (\u{202e},\u{200b}). A value cut short is cut before an escape, never in it. A backslash is doubled where what follows it would read as one of these ('a\\nb'is the texta\nb), and shown as it is elsewhere ('a\b'). This is only how the list shows them: the statements, and whatccopies, carry the values as they are. - Every reason the changes can’t be applied yet is marked
!, for every row (not only the first): under the row’s lines, without its name, for a new row’sNOT NULLcolumn without a default, a row that matches no row or several rows; first, for what is about no row, such as a count that failed. A row that matches several identical rows is listed too: which of them to change is asked when you apply.
j and k select a line, Ctrl+D and Ctrl+U move half a page, g g and G go to the first
and the last. Enter shows the line’s row and column in the grid (a row the grid no longer has
is said). u and Ctrl+R undo and redo, and the Space d keys work as in the grid: an edit of
a cell goes to the grid at the selected line’s cell first. Under the list the tab says how the
Apply stands to a transaction (as the Apply dialog’s footer does), and its keys where they fit.
The SQL view
s switches between the list and the statements that apply the changes, for the rest of the
run. ui.changes_view = "sql" (:set ui.changes_view=sql, or the settings screen) opens the tab
in the statements; "list" is the default. The statements are shown as the Apply dialog shows
them: in the order they run, in the editor’s syntax colours, each with the count it must give
(expect 1) at the end of its first line, a statement longer than the pane wrapped under its
first line.
On MySQL a text value is compared by its UTF-8 bytes, so the statement holds it as
_utf8mb4 X'…'. A statement with such values is followed by a dim line that starts with ↳
and reads them back in order, column = 'text' (a long text cut with …; bytes that are not
UTF-8 text say how many there are), grouped by where they are: set for the values an update
writes, where for those a row is found by, values for those an insert adds. An update can
name a column in both, its new value and the one it is found by:
UPDATE `shop`.`people` SET `note` = _utf8mb4 X'615C62' WHERE expect 1
`id` <=> 1 AND `name` = _utf8mb4 X'706C61696E206E616D65' AND
CONVERT(`name` USING utf8mb4) COLLATE utf8mb4_0900_bin <=> _utf8mb4
X'706C61696E206E616D65' AND `note` = _utf8mb4 X'6F6C64' AND
CONVERT(`note` USING utf8mb4) COLLATE utf8mb4_0900_bin <=> _utf8mb4
X'6F6C64'
↳ set note = 'a\b' · where name = 'plain name', note = 'old'
That line is not SQL: it is never sent to the server, and c copies the statements exactly as
they are sent, without it. A literal longer than a few hundred bytes is also shown shortened, its
start and end around how many characters are left out (…786400 more characters…); it is copied and sent
whole.
- Row checks (lock, no waiting) first: for every row read that changes, a
SELECT … FOR UPDATE NOWAITthat finds and locks it. A row another session holds fails at once instead of waiting. - Writes: the deletes, then the updates, then the inserts. A
DELETEand a statement that expects more than one row are marked!, their count in the warning colour.
c copies every statement exactly as it is sent, each followed by ; and a line break (through
the clipboard setting), in both views. While the statements are not final it copies nothing
and says why: “Not copied: the SQL is not final yet: 2 problems”, or “…: the rows are still
being counted”.
Rows found by their values are counted on the tab’s own session while the tab is shown, half a second after the last change, once for each state of the changes (“Counting matching rows…” until the answers come; the count sees the tab’s open transaction, as the Apply does). The list needs no count.
Apply
Space d a (:apply, or “Apply the pending changes (asked first)” in the action menu) works from the grid
and from the Changes tab. The rows found by their values are counted afresh first, every time,
whatever the Changes tab counted before; the count sees the tab’s open transaction, as the
Apply does. The status bar says Checking 3 rows… until the answers come. Meanwhile any key
(Esc too), a click, the wheel or a paste stops waiting (“Apply cancelled”) and does nothing
else, so none of them is taken by the question that follows; a dialog that opens or a run in the
tab cancels it too. Changes that move before the answers come drop the Apply: “Changes changed:
press Space d a again”.
When the changes can’t be applied yet, nothing is asked: the Changes tab takes the focus with the first problem selected, and the status bar says “Can’t apply: 2 problems (see Changes)”. The problems are those of the list above, for instance:
- “row 3 · matches no row of the table now: it was changed or deleted. Re-run the query.”
- “row 3 · matches 2 rows; add the key to the SELECT to edit it”: the rows may differ in what the result does not show.
- “row 3 · email is not shown whole, so the row can’t be found again”, and the
NOT NULLrefusals of new rows above.
Otherwise the status bar asks, on one line in the warning color:
Apply 3 changes to shop.users (2 updates · 1 delete)? y apply · any other key cancels
Only y applies. Any other key (Enter, Esc, n, any letter), a click or a paste cancels
(“Apply cancelled”) and does nothing else: it never reaches the grid. Where the window is narrow
the question leaves out the table, then the kinds but the deletes, and at its shortest is
Apply 3 changes (1 delete)? y/n, the deletes always named. With
manual commit the question also says how the Apply stands to a transaction
(“Opens a transaction; nothing is committed.”, or “Runs inside the open transaction; nothing is
committed.”); the shortest form says it too: Apply 3 changes (1 delete), nothing committed? y/n
or …, in the open transaction? y/n. The question is for the changes as they were asked about: a change, another tab,
a run, a transaction that begins or ends, or a lost connection drops it (“Apply cancelled: the
changes changed”).
When a question on the status line (this one, a paste of many rows, CSV or one column) or the
wait for the counts ends without its answer, cancelled by a key or dropped because something
moved, the keys that answer questions (y, n, c, 1, on a Korean layout too) do nothing in
the results for about a second, and say so: “Apply cancelled: the changes changed ·
y ignored”, or “Apply cancelled · y ignored”. A y pressed just as the question went never copies
a cell over the clipboard. Other keys work as usual.
The Apply dialog
A dialog, “Apply changes: shop.users”, takes the place of the question when the statements
must be seen before y:
- a row matches several identical rows: “row 3 · matches 2 identical rows”, with “[1] just one
[a] all 2”. The table has no key, the result shows every column, each comparing exactly, and
the rows are the same in all of them.
1changes one of them,aall of them (the write then expects 2). - the last Apply’s result is unknown (below): the first
ywarns, a secondyapplies again. - the profile’s environment asks before every write (
confirm = "writes"): the dialog is that question, no second one opens.
Cancel has the focus: only y applies. The dialog shows how many updates, inserts and deletes,
then the statements as the SQL view shows them; j and k scroll line by line, c copies them,
Enter, Esc or q closes. It closes by itself when its result, its rows’ connection or the
changes change. Its footer says how the Apply stands to a transaction:
- “Runs in one transaction; commits at the end.” (automatic commit)
- “Opens a transaction; nothing is committed.” (manual commit, none open)
- “Runs inside the open transaction; nothing is committed.” (your own
BEGIN, or a manual commit transaction already open)
How a row is found
A row read is found by the table’s primary key, or a NOT NULL unique key, when the result
shows all its columns and each of them compares exactly; else by the values of its columns that
compare exactly. A table others inherit from is always found by values. Floating-point numbers,
JSON, XML, geometry and types the app does not know do not compare exactly, nor (PostgreSQL)
arrays, intervals, a numeric without a scale, times with a time zone and network addresses,
nor (MySQL) text in a character set that does not convert one to one. Values are compared byte
for byte, never by the column’s collation. An update also checks that each changed column that
compares exactly still holds the value read, and a delete every column that compares exactly: a
row changed elsewhere in those columns since is not overwritten.
While it runs
y sends every statement on the tab’s own session. While it runs the strip (in the grid view) says applying…,
the Changes tab says “Applying 3 changes…” over the list (or the statements) being sent, edit keys are
refused, and the query’s cancel key (Ctrl+C) cancels it. When the result’s pages are held
open, the cursor is closed first; the rows already read stay.
Either every statement gives its count and the changes are applied (“Applied 3 changes”), or
nothing is (“Nothing was applied”, the changes stay pending and may be applied again). Why is said
on the Changes tab’s second line, and in its SQL view the statement that failed is marked ! and
scrolled to. A row is named as the Changes tab names it (id=5, row 3, new 1), and a
statement in a message is its first line, cut short when long:
- “id=5 · was changed or deleted since it was read. Nothing was applied. Re-run the query to see the current values.”
- “row 3 · now matches 2 rows, not 1. Nothing was applied.”
- “id=5 · is locked by another session. Nothing was applied.”
- “new 1 · <the server’s error>. Nothing was applied. Statement:
” - “Apply cancelled; nothing was applied.”
- “Apply result unknown: the connection was lost before the server confirmed. Re-run the query
to check.” The changes may be in the table already: the next Apply opens the dialog, whose
first
ywarns, and only a secondyapplies them again.
After an Apply
The result shows what the Apply did over the rows as read, until the query runs again: written
values in italic (theme token edit_applied), deleted rows crossed out, the rows added after the
rows read. The strip says applied · re-run to refresh (applied · not committed while its
transaction is open; applied and in open tx where the strip is narrow). These rows are not edited again: run the query first.
The Changes tab stays as Applied, until the query runs again: its list is what was applied,
its SQL view the statements that were sent (c still copies them), and its second line says
“Applied · re-run to refresh”, or “Applied · not committed” while the transaction is open.
- PostgreSQL returns each written row as stored, so a value a trigger set shows too. A value in
a form the grid shows differently (dates and times, floats, intervals, bytea, network
addresses, most arrays) shows a dim
re-run to see. - MySQL returns no rows: a new row’s
AUTO_INCREMENTcolumn shows the number it took, other defaults staydefault. - Paging on past the rows already read is refused (“Changes were applied to these rows: re-run the query to see the rest”): the rows the server would skip have moved.
With manual commit, or inside your own transaction, the Apply is not committed (“Applied 3
changes; not committed yet”): the status bar
counts it in the open transaction, and :tx lists it as “Apply (3 changes)”. A commit keeps the
values shown; a rollback drops them (“The transaction was rolled back; the applied changes are
gone”), and the Changes tab says so until the query runs again. See
Manual commit.
Leaving with changes pending
Running a statement in the tab (its results are replaced), closing the tab and quitting ask
first when changes are pending, saying how many: y drops them and goes on, n or Enter
keeps them and does nothing. Changes already applied do not ask; an open transaction asks as
usual. Deleting a connection profile or a saved query closes its tabs without another question:
its own question says how many pending changes go (“2 pending changes of the results are
dropped.”).
Copy and search
Copying (y, Y, the Space r y group, :copy), the search (/, ?), the cell viewer
(Enter) and the inspector take the values the grid shows: a pending value or what an Apply
wrote, not the value read. A copy with pending values says “(with pending changes)”, and counts
the copied rows marked to be deleted, which are copied as shown.
- A pending DEFAULT has no value yet: a copy of it is refused, except as SQL
INSERT, which writesDEFAULTthere. A cell showingre-run to seeis not copied, nor is one an Apply set to its default, showndefault(“Not copied: nickname shows “default”; run the query again to copy its value”). - New rows are not copied (“A new row is not copied: it is not a row of the result yet”) and the search does not look at them.
Export and the chart
An export to a file and the chart do not take the values the grid shows over the rows:
- An export of the rows held (the selection, the fetched rows, or every row of a result fetched to its end) is refused while the result has pending or applied changes (“Not exported: this result has pending changes; apply or discard them, or run the query again (see Changes)”; with changes applied, “Not exported: this result has pending or applied changes; apply or discard them, or run the query again”), in every format.
- An export of every row of a result the server has more of reads the rows again from the server, so it holds what an Apply wrote. With changes pending it goes on, and says “The pending changes are not in the file: every row is read again from the server”.
- The chart draws nothing while the result has pending or applied changes: it says “Not drawn: this result has pending changes. Apply or discard them, or run the query again (see Changes)”, or, once they are applied, “Not drawn: this result has pending or applied changes. Run the query again to chart the edited values.”