The data grid
One implementation serves table tabs and query results: an AppKit NSTableView with fixed row heights, server-side paging and an edit buffer that turns into SQL you read before it runs.
Pages, not endless scroll
A table tab shows one page of at most 1,000 rows. The pager in the status bar moves first, previous, next and last, and names the row range on screen. Moving between pages re-reads from the server; the grid never holds more than the current page. When a table has a single-column integer primary key and no user sort, deep pages switch from OFFSET to keyset paging, because OFFSET degrades. The status bar tooltip tells you which was used.
Query results are different: a result set is what the statement returned, so it streams in batches as rows arrive. Beyond 200,000 rows the stream pauses and a banner offers to load more, export the remainder straight to a file, or cancel.
~. A filter with an exact count, or a short final page, shows the real number. An estimate is never presented as a count.Sorting and filtering
orders, joined by OR. The SQL toggle on the right shows the generated WHERE clause.Click a column header to cycle none, ascending, descending; ⇧-click adds a secondary sort. ⌘⇧F opens the filter bar. Each condition is a column, an operator and a value; conditions combine with AND or OR. Operators cover =, ≠, <, ≤, >, ≥, contains, starts with, ends with, is null, is not null, in (…) and between. Values are always sent as parameters, never interpolated. Sort and filter both run on the server and reset to page 1, because both change what page 2 would mean. The state persists per table per connection.
Editing
Editing is enabled when the table has a primary key or a unique NOT NULL index. Otherwise the grid is read-only and the status bar says why. Double-click or ↩ starts an inline edit with an editor that fits the column type: a text field for strings and numbers, a toggle for booleans, a picker for dates and times, and the cell inspector for JSON and long text. ⌘⌫ sets NULL, Esc cancels, Tab moves right.
The grid says what it is about to do. An editable cell outlines itself under the pointer, and the editor opens with the caret at the end rather than the whole value selected. Ending an edit that changed nothing writes nothing, and emptying a cell that held a value is asked about once while auto-commit is on. Add Row and Delete Row sit at the foot of the grid beside the row count, and the delete names its own damage: “Delete 3 Rows”.
A date, time or timestamp cell opens its picker over the cell — ↩, a double-click, ⌥↓ or the context menu — with the server’s own text above the calendar for anything the picker cannot spell, and the two routes end at the same spelling. The picker shows a value in its own time zone rather than the Mac’s, and gives an empty timestamptz the Mac’s offset.
A column with a fixed set of values is picked, not typed
A value that is not one of the column’s own is refused before anything is written, and the message names the values that are — so a typo in an enum is a sentence on screen rather than a rejected transaction. A foreign-key cell has the same courtesy in reverse: Choose from referenced table opens the parent’s rows, in the grid and in the inspector’s Row pane alike.
Edits are held in a buffer and coloured: changed cells yellow, new rows green, rows marked for deletion red with strikethrough. Nothing is sent until you commit (⌘⇧S) or discard. With auto-commit on, a non-production connection writes each edit as it is made; production connections always buffer.
The commit preview
Commit generates statements and shows every one of them in a sheet, syntax-highlighted, with a count summary. Only after you press Execute do they run, inside one transaction. On success the buffer clears and the affected rows refresh in place. On any failure everything rolls back, the buffer stays intact, and the error is shown against the statement that caused it.
UPDATE orders SET status = $1 WHERE id = $2; -- only changed columns; WHERE uses the original PK values DELETE FROM orders WHERE id = $1; INSERT INTO orders (customer_id, total) VALUES ($1, $2) RETURNING *;
A written edit can be taken back
With auto-commit on, an edit reaches the server as soon as it is made — so every write carries its own way back. The status line names the last one and offers Undo, ⌘Z takes it back once nothing is pending, and a popover lists what the tab has written, each entry with Put Back. Query result grids have it too.
- The statements that restore the old values are worked out before the write runs, while the rows still hold what is about to be replaced, and they run under the same one-row check as a commit.
- A row that has changed again since — by a later edit or by someone else — is refused rather than overwritten. Writes run in order, each re-read before the next.
- A deleted row is put back whole or not offered: only when every column was loaded, with generated columns left to the server.
- Put Back is not offered inside an open transaction or while a statement is running, and a write made through one connection or database cannot be put back through another.
Selection, copy and paste
- Cell, row, column and rectangular range selection, like a spreadsheet.
⌘Ccopies TSV.⌘⌃Ccopies the selected rows as INSERT statements in the connection’s dialect. The context menu also offers CSV, JSON, a Markdown table and a WHERE-IN list.⌘Vpastes straight from a spreadsheet. Lines carrying as many values as the table has columns — or sitting under a first line that names the columns, in any order and without the auto-increment key — become new rows, each value in its own column; fewer values fill cells from the one selected. Excel’s quoted cells, line breaks inside a cell and CRLF line ends all come through, and a comma inside one value does not split it. Invalid values are flagged, never dropped.- With auto-commit on the pasted rows are written at once. On a production connection they wait for Commit like any other edit.
The cell inspector
⌘⌥I opens the inspector: column name, native type, constraints and the raw value, editable.The inspector shows the focused cell in full: JSON pretty-printed with a Copy formatted button, binary as a hex dump with Save as file, and long text without the 512-character in-cell truncation. Values that carry precision, such as decimal and timestamp, keep the server’s text end to end. They are never routed through Double or Date.
Locations on a map
A geometry column is a hex blob in every other pane, and a blob tells you nothing about where it is. Tinker reads WKB, EWKB and WKT, in a geometry, geography, MySQL spatial or plain text column, and draws what it finds on Apple Maps — points, lines and polygons, at the coordinates the server holds. No tile key, no browser, no copying a pair of numbers into a map site to see whether the row is where it should be.
A location does not have to be a geometry. A table that keeps it as two plain columns — latitude/longitude, lat/lng, lintang/bujur, or with a prefix such as pickup_lat/pickup_lng — or as one koordinat column holding “lat, lng” gets the same Map tab, whether the values are numbers or text.
- Values are read as they are typed: with spaces, with a decimal comma (
-8,5994), or in degrees (8°35'57"S,8,5 LS). Columns named the wrong way round are read correctly and marked SWAPPED. - The Map tab appears only where there is a location to draw, on a table tab and on a query result alike.
- Right-click any cell of a row with a location for Show on Map: the row on a map over the grid, with its name and coordinates, and a menu to open it in Apple Maps or Google Maps, copy the coordinates, or open the full map pane. A pin on the map has the same menu. Coordinates leave Tinker only when you click one of these.
- Click a shape to select its row in the grid. A switch above the map moves between every row on the page and the rows selected when it opened, and points close together gather into numbered markers that zoom in when clicked.
- A filter or a sort applies first, so the map shows the rows you are looking at rather than the whole table.