Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
603ef8f
feat(advisor): read-only lint pass over the connected database
broisnischal Aug 10, 2026
47f60df
fix(sql): key query cancellation by id
broisnischal Aug 10, 2026
bc17829
feat(rust): expose advisor_scan, and accept a queryId on execute/cancel
broisnischal Aug 10, 2026
a706a2b
perf(grid): size row windows by payload weight, not row count
broisnischal Aug 10, 2026
b4fd765
test(grid): cover window sizing, coverage and stable ordering
broisnischal Aug 10, 2026
2d29fe2
feat(scroll): the app's own eased scrolling
broisnischal Aug 10, 2026
725b134
test(scroll): convergence on a quantizing element, both axes
broisnischal Aug 10, 2026
0ac6d72
feat(sql): hold formatter options apart from the formatter
broisnischal Aug 10, 2026
7ed1895
feat(sql): drive the formatter from the user's options
broisnischal Aug 10, 2026
dc7f0bf
feat(advisor): shape reports in a pure module
broisnischal Aug 10, 2026
834eb94
test(advisor): severity order, grouping, filtering, export, summary
broisnischal Aug 10, 2026
d5f0ed0
feat(advisor): the Advisor page
broisnischal Aug 10, 2026
48c8e19
feat(settings): native scrolling, row spacing, zebra rows, auto-save,…
broisnischal Aug 10, 2026
56b1351
feat(api): advisorScan, and thread queryId through execute and cancel
broisnischal Aug 10, 2026
9fa6c7f
feat(tabs): advisor tab kind, and windowed-view fields on a table sna…
broisnischal Aug 10, 2026
f186822
feat(sql): saveQueryOnce, so auto-saved runs do not pile up duplicates
broisnischal Aug 10, 2026
3090033
perf(canvas): let many rounded rects share one path and one fill
broisnischal Aug 10, 2026
5c5785e
feat(grid): per-tab auto-refresh beside Refresh
broisnischal Aug 10, 2026
6b27419
perf(settings): drop nine popover selects, collapse SQL formatting
broisnischal Aug 10, 2026
90e7f6d
feat(sidebar): eased scrolling, unless native scrolling is on
broisnischal Aug 10, 2026
3bf4b12
fix(sql): Stop cancels this tab's query, not whichever ran last
broisnischal Aug 10, 2026
fc7168d
fix(panes): do not preview a windowed result the pane does not hold
broisnischal Aug 10, 2026
45d8b31
feat(palette): open the Advisor from the command palette
broisnischal Aug 10, 2026
498964d
feat(grid): eased wheel scrolling in the data grid
broisnischal Aug 10, 2026
9e27831
fix(grid): skeleton, header alignment, JSON panel stacking, zebra, ro…
broisnischal Aug 10, 2026
e97b8c0
fix(grid): a row fetch belongs to the tab that started it
broisnischal Aug 10, 2026
f0e6a69
feat(shell): advisor tab, per-tab auto-refresh, per-query cancel, aut…
broisnischal Aug 10, 2026
3911666
chore: add changeset for the advisor, windowing and scrolling work
broisnischal Aug 10, 2026
c7fc4a6
perf(grid): seek far windows by key, and say which rows are loading
broisnischal Aug 12, 2026
47ff2ce
chore: add the daily AI usage screenshot
broisnischal Aug 20, 2026
4797519
style: use hyphens instead of em dashes in prose
broisnischal Aug 26, 2026
0d0d602
feat: PIN lock, plugin API, and an ER diagram that reads
broisnischal Aug 26, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 6 additions & 6 deletions .changeset/README.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# Changesets

Add a `.md` file here in any PR that has user-visible changes.
Name it anything — `my-feature.md`, `fix-datatable.md`, etc.
Name it anything - `my-feature.md`, `fix-datatable.md`, etc.

When the PR is merged with a `release:*` label, `auto-release.yml`
folds all changeset files into `CHANGELOG.md` under the new version
Expand All @@ -15,10 +15,10 @@ Use the same headings as CHANGELOG.md:
### New Features

#### Canvas Table
- **Feature name** — What it does and why it's useful
- **Feature name** - What it does and why it's useful

#### SQL Editor
- **Another feature** — Description
- **Another feature** - Description

### Bug Fixes
- Fixed something that was broken
Expand All @@ -28,9 +28,9 @@ Use the same headings as CHANGELOG.md:
```

Supported top-level headings (case-insensitive):
- `### New Features` — shows as ✨ New Features in the app
- `### Bug Fixes` — shows as 🐛 Bug Fixes
- `### Changes` — shows as 🔧 Improvements
- `### New Features` - shows as ✨ New Features in the app
- `### Bug Fixes` - shows as 🐛 Bug Fixes
- `### Changes` - shows as 🔧 Improvements

Sub-sections (`####`) are optional but group items nicely in the release notes page.

Expand Down
47 changes: 47 additions & 0 deletions .changeset/advisor-windowing-and-scrolling.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
### Changes

- **The Advisor tells you what is wrong with your database.** A new tab runs a read-only lint pass over the connected database and reports what it finds, grouped by check and ranked worst-first: tables without row-level security, foreign keys with no index, indexes nothing has scanned, tables bloated with dead rows, tables whose rows are so wide that reading a page of them moves megabytes, and tables with no primary key. Every check is a catalog query - it never reads a row of your data, never writes, and depends on no extension being installed, so it is safe to point at production. Findings carry the SQL that would fix them, to copy and read before you run it; the Advisor never executes DDL. Severity is decided per finding rather than per check, so an unused 8 GB index and an unused 2 MB one do not shout equally loudly, and a check that cannot run - a role without permission on a catalog - says so instead of quietly counting as a pass. Export the report as CSV, JSON or Markdown. Postgres for now; other engines get an honest "no checks yet" rather than a clean bill of health they have not earned.

- **Large tables no longer arrive in one enormous response.** Opening a table on a large page size used to be a single request for every row in it - a million-row table meant roughly 130 MB of JSON decoded on the main thread, per tab, which froze the whole app for as long as it took. A large fetch now reads a 200-row probe, measures how heavy the rows actually are, and either loads the rest normally or streams the table in windows sized to keep each request near 2 MB. A 143,000-row table opens in **185 ms** where it used to sit there for tens of seconds. This applies to any large limit, not just "All rows" - page size 1M is an ordinary option in the toolbar and taking it literally was the whole problem.

- **Window size is measured in bytes, not rows.** What stalls the main thread is payload, and rows differ by three orders of magnitude: five thousand narrow rows are half a megabyte, five thousand rows of a table with an embedding column are ninety. Notably, `pg_stats` cannot see this - a TOASTed embedding is reported as the 18-byte pointer stored in the main tuple - so the measurement uses table size over row count and catches exactly the tables that hurt.

- **A windowed table keeps its rows when you switch away and back.** It used to re-run the fetch and the count every time, so returning to a big table meant waiting again. Its window bookkeeping and its resident rows are preserved, and the scroll position with them.

- **Every window of a view slices one ordering.** A windowed view is assembled from many separate `LIMIT/OFFSET` queries, and SQL guarantees nothing about the order of rows between two queries that do not ask for one - Postgres ships with `synchronize_seqscans = on`, so a second scan of the same table can legitimately begin at a different page. Assembled from unordered windows, one view could show a row twice, miss others, and change which row sat at a given position between fetches. Windowed views are now ordered by primary key (a user's sort keeps the lead, with the key appended as a tiebreaker), and a table with no key is never windowed - a single query is internally consistent, many unordered ones are not.

- **A far window is an index seek, not a walk over everything before it.** `LIMIT n OFFSET 995000` makes Postgres walk 995,000 index entries before it can return a row - **345 ms** on a million-row table, where the same page fetched by key (`WHERE id > $last ORDER BY id LIMIT n`) takes **1.5 ms**. Every window slices one total order, so the last row of the window before it is exactly the anchor that page needs, and a window next to one already resident is now read that way. The conditions are deliberately narrow, because being wrong here means missing rows rather than slow ones: Postgres, a single ordering column that is the whole primary key (a non-unique key would skip every row sharing the anchor's value), and a type the backend will accept. Anything else, or the first error on the seek path, and the view stays on offsets. The check the backend runs before each keyset page is memoised for ten seconds, so the fast path does not put a catalog round-trip in front of every window it just saved a scan on.

- **The grid says which rows it is loading.** Skeleton bars only say that rows are coming, never which ones - on a million-row table a jump can leave a screenful of them up for a second with nothing to read. A pill now names the row span being fetched, appearing only after a window has been in flight long enough to be worth mentioning, so a window that lands quickly does not flash it on the way past. When a window has exhausted its retries the pill says so and offers Retry.

- **Smooth scrolling, and a switch to turn it off.** The grid and the sidebar ease each wheel tick to its destination, which reads as smoother on mice that scroll in big jumps. Settings → Appearance → **Native scrolling** hands both back to the OS, momentum and all, with nothing of ours in front of the wheel. Off by default. Someone who has asked their system for reduced motion gets the jump, not the sweep.

- **Row spacing, and alternating row colours.** Settings → Appearance offers Compact, Standard and Relaxed row heights - compact fits about a third more rows on screen - and a shading toggle for every other row, independent of the grid-style preset.

- **Auto-refresh, per tab.** The caret beside Refresh sets an interval of 5 seconds to 5 minutes for the table you are looking at, so a table you are watching re-polls while one you are editing stays still. It skips a tick while a load is in flight or a cell editor is open, and refreshes in place rather than jumping you back to the top.

- **SQL formatting is configurable.** Settings → Database → SQL formatting exposes keyword, data type, function and identifier casing, tab width, tabs versus spaces, whether `AND`/`OR` lead or trail the line break, expression width and blank lines between statements. Identifier casing defaults to preserve, because a quoted identifier is case-sensitive and rewriting it can change what a statement means.

- **Executed queries can file themselves under Saved Queries.** Off by default; when on, a successful run is saved as well as logged, deduplicated by its SQL so re-running the statement you are iterating on adds one entry rather than fifty.

### Bug Fixes

#### The data grid
- **A row fetch now belongs to the tab that started it.** Loads wrote into shell-level state guarded by a single global token, so switching tabs mid-fetch dropped a million-row result into whichever table happened to be on screen, and coming back to the tab you left restarted its work. Every load is owned by its tab: results land in that tab's state, the visible grid is only touched while that tab is in front, and a load left running finishes into the tab that asked for it.
- **Opening several large tables at once no longer locks the app.** A background tab was prefetching its full page size, so three tables opened on a large limit fired three enormous requests at the same time. A limit past the windowing bar waits for the tab to be activated.
- **The skeleton no longer looks like the layout shifted when rows arrive.** Placeholder bars sat hard left at a flat half-width in every column, so the moment real rows landed every right-aligned value jumped to the other side of its cell. Bars now take the alignment their column will use, vary in width like text of differing length, sit on the band the glyphs will occupy, and shimmer while the window is in flight.
- **Column headers line up with each other and with their own values.** The red `NOT NULL` asterisk led the column name, which indented the name by its own width on required columns only - so those headers sat a few pixels right of every other header. It now trails the name.
- **An expanded JSON row no longer paints over the column header.** The panel has to sit above the canvas, which paints an opaque background, so scrolling its row up behind the sticky header drew the panel over the header. It is clipped to the band below it.
- **Alternating row shading is visible.** It was painted at 7% of a muted tone, which against the panel background in a dark theme is nothing at all - the Striped and Dots presets looked like they did nothing either.
- **A window whose fetch failed no longer shimmers forever.** A failed window was only retried by the next visible-range emit, so stopping the scroll over it left those rows as skeletons with nothing coming. Each window now retries on its own up to three times before it gives up, and giving up is visible and retryable rather than silent.
- **One slow page no longer turns read-ahead off for the rest of the view.** A single measurement past the slow threshold latched the fetcher down to one window in flight - and one deep offset page on a big table crosses that threshold on its own merits, so the tables that need read-ahead most were the ones that lost it. The decision reads a rolling mean now.
- **A reload that keeps your scroll position refills the rows under it.** Reloading in place hands the grid a fresh rows array holding nothing but its probe, but the viewport had not moved, so the range it re-emitted matched the one the shell had already heard and was dropped as a duplicate - leaving every row under the viewport a skeleton until the user happened to scroll.
- **The skeleton shimmer stopped blinking once a period.** The sweep restarted with its highlight band still half on screen, so the gradient vanished mid-grid on the beat. It now travels a full band clear of both edges, and the whole frame's bars share one path and one gradient instead of a fill each - per-bar fills were the most expensive thing on screen in exactly the state that has to feel smooth.
- **Deep exports chunk by weight.** Fifty thousand rows of a table with an embedding column is most of a gigabyte in a single response.

#### SQL editor
- **Two editor tabs can run at once, and Stop hits the right query.** Cancellation was keyed by a single slot, so the second run overwrote the first's handle and Stop cancelled whichever query registered last rather than the one whose button was pressed. Each run carries its own id.
- **A query that finishes while you are on another tab no longer leaves the editor looking busy.** The shared loading flag was only cleared when the finishing run's own tab was still in front, so the next snapshot of any editor tab could capture that stale state as its own.

#### Settings
- **The Settings pane stopped being slow to open.** The SQL formatting section mounted nine popover selects - each a floating layer with a focus trap and a search box - merely to show the Database tab. Those are inline choice controls now, and the section starts collapsed; search still expands it. Formatter options are also read through a validator, so settings written by an older version cannot throw the whole pane into the error boundary.
Loading
Loading