Skip to main content
Nothing here reaches the server on its own. Cell edits, inserts and deletes queue on the tab, Preview SQL (Cmd+Shift+P) shows the statements they will produce, and Cmd+S runs them. Modified cells stay highlighted, Cmd+Shift+P shows the statements the queue will produce, and Cmd+S runs them. The queue belongs to the tab, so switching tabs leaves it alone. A queued change also marks the value itself, so it stays legible once the row is selected and reads aloud to VoiceOver. An edited value and a new row are underlined, and a row queued for deletion is struck through.
Pending edits, inserts, and deletions highlighted in the data gridPending edits, inserts, and deletions highlighted in the data grid

Data grid with pending changes highlighted

Editing a cell

Double-click a cell, or press Return on it, and type. Return queues the edit and Escape cancels it; Tab, Shift+Tab, Up and Down queue it and edit the neighboring cell. A value that holds a line break, or runs past 256 characters, opens in a multi-line editor anchored to the cell, where Option+Enter starts a new line. Setting a value back to what it was drops it from the queue again. Delete the text of a date, number, boolean, JSON, array or vector cell to store NULL. In a NOT NULL column of those types the cell keeps its value, and an emptied text cell stores an empty string. Some types open an editor of their own:
Cell editorCell editor

Type-specific cell editor

Right-click an editable cell and open Set Value. Each item appears only where the column takes it:
  • Empty on text, SET and binary columns.
  • NULL on a nullable column.
  • Default on a column the server fills in. SQLite, libSQL, Turso, Cloudflare D1, ClickHouse, Databend, Snowflake, Trino, Cassandra and ScyllaDB take it on a new row only.
  • The database’s own date and time functions on a date or time column, such as NOW() on MySQL or CURRENT_TIMESTAMP on SQLite.
  • Generate UUID (version 4) and Generate UUIDv7 (ordered by creation time) on a uuid or uniqueidentifier column, and on a text or binary column whose display format is UUID. A foreign key column and a Cassandra timeuuid column get neither.
  • Set Cells to Value… on any cell you could type into. Type the value once and click Set. It is read the way a typed value is, so an empty value stores NULL in a date or number column, and a function such as NOW() in a date or time column runs on the server.
Select several cells first to set all of them at once; Generate UUID gives each cell its own value. Whatever the count, one Cmd+Z undoes the whole command. With whole rows selected, the clicked column changes on every selected row. Cells that cannot take the value keep what they hold: Set Cells to Value… skips a column the server owns, a binary column and a deleted row, and says how many cells it sets before you confirm. Edit > Set Value holds the same items for the focused cell or the selection, and Settings > Keyboard assigns them shortcuts. To copy one value into many cells, copy it, select the cells and press Cmd+V, or choose Paste on a cell’s right-click menu. A copied block of several cells pastes from the focused cell rightward and down instead. On MongoDB, Firestore and the Firebase Realtime Database a document can lack a field altogether, and on Bigtable a row can lack a cell; the grid shows either as No Field. Remove Field on the same right-click menu deletes the field rather than storing NULL. Bigtable stores no NULL, so NULL and Remove Field both delete the cell. See missing fields and null.

Adding and deleting rows

  • Add Row (Cmd+Shift+N), the + button, or Edit > Add Row. The row appears at the bottom with the cursor in its first writable cell. A UUID primary key the server does not fill gets a new UUID, and the cursor skips it. Later edits fold into the INSERT rather than queueing as updates.
  • Duplicate Row (Cmd+Shift+D) on the right-click menu copies a row and resets its primary key and its identity columns to DEFAULT, so the database assigns new ones. A UUID primary key the server does not fill gets a new UUID instead.
  • Delete: select rows by their row numbers (Shift-click for a range, Cmd-click for separate rows), or cells in them, and press Delete or Forward Delete. They stay visible with a strikethrough, and saving asks “Delete 5 rows from customers?” before they go.
  • Paste (Cmd+V, or Paste on the right-click menu) inserts copied rows as new rows, with a new UUID in a UUID primary key the server does not fill.
  • Fill Column on the header right-click menu writes one value, NULL, or a new UUID or UUIDv7 per row into every loaded row. It is absent on primary key columns and on columns the server owns.
A new row starts at DEFAULT in every column the server fills in. That covers a column with a default expression, and an identity or AUTO_INCREMENT column, which carries no default of its own. The INSERT leaves those columns out, so the server assigns them. Add Row and Duplicate Row stay dimmed until the table’s schema has loaded, which is what says which columns the server fills in. On a table opened for the first time that is a fraction of a second.

Rows other tables hold

When foreign keys point at the table, the delete prompt lists what the delete does to other tables before anything is sent:
  • Rows an ON DELETE CASCADE key deletes, followed up to 5 keys deep.
  • Rows an ON DELETE SET NULL or SET DEFAULT key rewrites, with the column it sets.
  • Rows a RESTRICT or NO ACTION key protects. These make the save fail, and the prompt says so.
Click Show beside a line to open those rows in a new tab, filtered. The save is cancelled and the deleted rows stay queued, so press Cmd+S again when you are done looking. The counts come from SELECT COUNT(*) queries under the connection’s query timeout. Counting stops after 5 seconds, and a table not counted by then, or whose count failed, shows as not counted rather than as a number. Only foreign keys declared in the table’s own schema are checked, and the prompt says so when the database has other schemas. When a transaction is open on the connection and the counts cannot see its uncommitted rows, the prompt says that too. SQLite and libSQL enforce foreign keys only on a connection that ran PRAGMA foreign_keys = ON, and MySQL and MariaDB skip them while foreign_key_checks is 0. With the keys off nothing cascades, and the prompt says the referencing rows stay as they are. Rows deleted through a view are counted against the table the view reads, since foreign keys point at that table and never at the view. When that table is in another database, the prompt says the delete could not be checked.

When the grid will not edit

A tab opened from the sidebar edits its table directly. Materialized views, sequences and external tables open read-only. On PostgreSQL, MySQL, MariaDB and SQL Server, a view is editable when the server writes through it without a rule or an INSTEAD OF trigger, and it shows every primary key column of the one table it reads. Edits go to the view, keyed on those columns, renamed or not. Computed columns stay read-only, and Add Row and Delete appear only when the server accepts an insert or a delete through the view. Any other view opens read-only, and hovering the grid says why: the server cannot write through it, its writes run rules or triggers, it reads more than one table, its table or key is hidden from the connection, its table has no primary key, it leaves a key column out, or the check could not finish. A query tab that reads a view follows the same rules. On other engines every view opens read-only; to change what one shows, edit the tables it reads from. A query tab edits only when the app can prove the rows came from exactly one table: To change rows a join returned, open one of its tables from the sidebar and edit there. To edit through another schema, switch the session to that schema first. Two more shapes look editable and refuse. A column renamed with AS cannot be written back, though the rest of the row still can. A query that renames or omits the primary key blocks the save outright, because nothing is left to identify the row by. Individual columns stay read-only where the server owns them: generated, computed and Oracle virtual columns, which are computed on write and left out of every INSERT and UPDATE, SQL export included; GENERATED ALWAYS AS IDENTITY columns on PostgreSQL and Oracle, and SQL Server IDENTITY columns, which the server refuses an explicit value for; SQL Server rowversion and system-versioned period columns, which the server writes itself; and columns the driver marks immutable, such as MongoDB’s _id. GENERATED BY DEFAULT AS IDENTITY and AUTO_INCREMENT columns stay editable: type a value to override the sequence, or leave the cell at DEFAULT. A connection set to the Read-Only safe mode level edits nothing at all.

Saving

Press Cmd+S, or click the toolbar checkmark. Values go out as bound parameters; DEFAULT and the functions picked from Set Value or the inspector are written into the statement as they stand. Typing one of those functions into a date or time cell does the same. Text typed into any other column is saved as typed, even when it reads like now(), and a pending function cell is drawn like a pending DEFAULT. A table without a primary key is matched on every original column value instead, with IS NULL for the nulls. On MySQL, MariaDB, TiDB and OceanBase a FLOAT, DOUBLE or JSON column is compared through CONCAT(), because the text those types read back as does not compare equal to the value it came from. On SQL Server an ntext, text, image, xml, geography, geometry or hierarchyid column is compared through CAST, because = refuses those types or the value reads back as the type’s bytes. A sql_variant column cannot be compared without losing its type, so a save that has to match one stops and names it. On an engine with transactions, the statements run inside one, so a failure rolls the whole save back and the table is left as it was. Without them, the statements that ran before the failure stand. Either way a failed save reports Save Failed with the server’s message and keeps the queue intact, so correct the value and save again. A save that succeeds clears the queue, clears undo, and reloads the grid at the same place, with its sort, value filter and selection. In a query tab the reload runs the statement behind the result again, not the one at the caret. Every queued change has to become a statement before anything is sent. When a change cannot, such as a new Redis row with no key, or it holds a value the driver cannot write, such as an edit to one leaf of an Elasticsearch nested array or to a column the server owns made before the table’s structure finished loading, or a pgvector value whose length differs from its vector(n) column, the save stops, names what it could not write, and keeps the whole queue. Undo that change, or make it with a query, then save again. The statements a driver writes for itself, as SQL Server, Oracle and ClickHouse do, are read before anything is sent. An UPDATE or DELETE with no WHERE of its own would reach every row of the table, so the save stops, names the table, and keeps the whole queue. A WHERE inside a comment, a string or a subquery does not count. Each generated SQL statement is held to the number of rows it was written for. On a table with no primary key, two identical rows cannot be told apart, so a statement meant for one of them matches both; the save stops there and reports what happened instead of rewriting the other row. Such a statement is held to the other end too. Finding fewer rows than it was written for means the row it meant to write is no longer there, so the save rolls back and keeps the edits rather than reporting a success that wrote nothing. Engines whose drivers report no real count, ClickHouse among them, are not held to it. Of the drivers that write their own statements, SQL Server and Oracle are held to it on a table without a primary key, and MongoDB and Redis are not. SQL Server bounds that match with TOP (1) and Oracle with ROWNUM = 1, so of two identical rows they write one. Where a count cannot hold a statement to its row, the row is looked up right before the statement runs instead: on ClickHouse, Snowflake and Trino for every edit and delete, and on Dameng for a table without a primary key. Finding no row, or more than the change was made on, stops the save before anything is written and keeps the queue. A ClickHouse statement is also limited to the data part its row was found in, and the row is checked after it runs. See ClickHouse. To drop the whole queue, choose File > Discard Changes… and confirm; a staged truncate or drop goes with it. Undoing the edits clears them too, and so does the Discard Unsaved Changes? prompt that appears when you refresh, sort, filter, change page, or close the tab with edits pending.

Previewing the SQL

Cmd+Shift+P, or Query > Preview SQL, lists the statements with their parameter values inlined, so the preview reads as what will run. Copy All copies every statement.

Undo and redo

Cmd+Z and Cmd+Shift+Z. Focus decides what they act on: text in the SQL editor, queued changes in the grid. Deleting several rows undoes as one step. The stacks are per tab, and a save clears them.

Restoring a save

Edit > Restore Previous Values…, or the same item under Actions in the toolbar, puts back what the rows held before the last save on the current table. It ships with no shortcut; bind one in Settings > Keyboard, under Data Grid. Pro license. The item is dimmed while no values are kept for the current table. Values are kept on this Mac for 7 days, encrypted, and never synced. Turn the capture off, or delete what is stored, in Settings > Data > Data Rewind. Every row is planned against the table as it stands now, and the sheet says what will happen to each one before anything runs: Restore writes the changes back the way any other save does: through the same safe mode prompt, in one transaction where the engine has them, and into query history. The restore is itself restorable.
Only what TablePro wrote is restored. Triggers that fired, audit rows they wrote, and rows a cascade deleted stay as they are.
A deleted row whose key the server allocates comes back under the same key. SQL Server restores it inside SET IDENTITY_INSERT, which needs ALTER permission on the table, and PostgreSQL writes it with OVERRIDING SYSTEM VALUE. Other engines have no way to write such a key, so the restore stops and says so, and so does a save that an earlier version recorded without noting which columns were identity. A save is not restorable at all when it also truncated or dropped a table. Within a save, a row is skipped when the table has no primary key, when a column was set to DEFAULT or a function such as NOW() whose stored value the app never read back, when the server chose the key of an inserted row, or when a value was larger than 1 MB. Through a view, a deleted row is skipped because the view may not show every column of its table, and so is a row the view no longer shows.

Structure changes

Column, index, foreign key, and primary key edits queue in the same place and go out with the same Save, previewed as DDL instead of DML. See Table Structure.