Skip to content

feat(history): fold the roadmap, lay out the sidebar, highlight what you click - #271

Merged
huyplb merged 5 commits into
mainfrom
feat/history-roadmap-sidebar-highlight
Aug 18, 2026
Merged

feat(history): fold the roadmap, lay out the sidebar, highlight what you click#271
huyplb merged 5 commits into
mainfrom
feat/history-roadmap-sidebar-highlight

Conversation

@huyplb

@huyplb huyplb commented Aug 18, 2026

Copy link
Copy Markdown
Contributor

Five changes to the Lokee history surface, plus a friendlier SQLite credential form. The last two commits carry over from the previous branch (#267 merged only the Db2 commit from it).

Roadmap folding

A database captured over months has hundreds of versions, and any one table moved in a handful of them. Printing one row per version buried the few that mattered.

roadmap.ts keeps the versions that changed the object, the head, and whatever is being viewed; each untouched run folds into one row (8 versions left it unchanged · v2–v9) that opens on click. Show all versions unfolds everything. Rows gain a column delta measured against the real previous version — not the previous visible row, so collapsing a gap cannot change the number.

Verified on a seeded 15-version SQLite history where customers moved at v1 and v10: 5 rows instead of 15; expanding v2–v9 gives 11; "Show all versions" gives 15.

Sidebar layout

Order is now Object type → Object status → Version → Date → User. Object type decides what the graph is made of, so every other filter reads as a narrowing of it; status is the legend and sits next to what it colours.

Sections drag to reorder by their grip (only the grip — the panels are full of checkboxes and date inputs, and a whole-panel drag source turns every mis-aimed click into a drag). The column drags to resize; double-click resets, arrows nudge. Both persist in localStorage.

Version and User lists carry a permanently visible thin scroll track: macOS hides overlay scrollbars, so a list that scrolled looked identical to one that had been cut off.

Click-to-highlight — and the wiring bug under it

The node renderers already drew a ring for a selected node, but the graph is fully controlled and had no onNodesChange, so React Flow's selection had nowhere to be written back and selected was never true. Clicking opened the inspector and the graph looked untouched.

highlight.ts applies selection from app state: solid ring on the clicked card, dashed outline on the same object at every other version, bright animated lineage edges, and everything else faded to 0.28 — faded, never removed, so the graph does not re-flow on a click. With no selection it returns the same array references, so the common case costs nothing.

The same missing handler was breaking edges

From React Flow's source, adoptUserNodesparseHandles:

if (!userNode.handles) {
  return !userNode.measured ? undefined : internalNode?.internals.handleBounds;
}

isNodeInitialized then refuses to place any edge touching a node with no handle bounds. This graph handed React Flow freshly built node objects (no measured) on every filter change, with no onNodesChange to report the re-measurement back — so each rebuild reset the bounds with nothing to restore them.

Fixed with useNodesState + onNodesChange, plus carryMeasurements(next, previous) so a rebuild does not drop the edges for a frame. Seven unit tests pin the contract, quoting the React Flow source as the reason.

Reviewer note, stated plainly: I first reported this as "edges intermittently vanish" from browser observation. That symptom was substantially a harness artifact — my Browser pane was hidden, so the container measured 0×0 and nothing could be measured (the NaN console spam on <circle>/<pattern> is the same zero-size layout). With the pane painted, edges draw. The wiring defect above is real and verified against the library source; the browser could not give a clean A/B while pane visibility was doing its own thing, so the evidence here is the unit tests.

Reverting leaves the object inspector

It lives in the version compare modal, which can scope a revert to ticked objects. A per-row button could only ever revert the whole schema, and two entry points with different blast radii was the trap. The Reused hash — stored once (pointer) line goes too — storage internals, not schema history.

SQLite / DuckDB credentials

Host, port, username, password, SSL and "Save password" are hidden for a file on disk; "Database Name" becomes Database File; the default credential name is the file rather than localhost/.

Browse… opens a picker. A browser's file dialog yields a File with a name and no path, so it lists the machine running the backend:

  • GET /api/files/browse — directories plus only .db .db3 .sqlite .sqlite3 .duckdb .ddb, with name/size/mtime and never contents. Dotfiles skipped, 500-entry cap, rate-limited 60/min, behind schema.browse.
  • NUL-bearing paths resolve to home rather than being normalized; relative paths resolve against home, not the service's cwd.
  • Deliberately not jailed to a root (decided with the repo owner). A signed-in user can already open any path by typing it into the form; this makes that discoverable without widening it. The extension filter is what keeps it from being a general file browser — file-browse.test.ts asserts id_rsa, secrets.env, dump.sql and .env are never listed. If you want the jail, it is one env var plus a prefix check in resolveBrowsePath.

Verified live: picking /tmp/fox-roadmap-demo.db fills the field, and POST /api/schema/list against it returns {"schemas":["main"]}.

Gates

cd apps/web && npx tsc --noEmit clean · npx vitest run 1925 pass / 2 expected-fail · eslint 0 errors. The three detect-non-literal-fs-filename warnings in file-browse.ts are the feature itself and carry inline justifications.

🤖 Generated with Claude Code


Note

Medium Risk
Adds a filesystem listing API (extension-filtered and permission-gated, but not path-jailed) and changes SQL rewriting, migration type rendering, and graph/React Flow wiring—areas that affect security posture and execution behavior if misconfigured.

Overview
Lokee history gets a folded object roadmap (changed versions, head, and current view stay visible; unchanged runs collapse into expandable gaps with column deltas vs the real prior version), a reorderable/resizable filter sidebar (persisted in localStorage), and selection highlighting on the version graph (clicked object/version, lineage edges, dimmed rest) wired through useNodesState, onNodesChange, and carryMeasurements so edges do not disappear after filter rebuilds. Per-row revert is removed from the object inspector (revert stays in the compare modal); script diffs can open full screen.

SQLite / DuckDB credentials hide server-style fields and add Browse… backed by GET /api/files/browse (schema.browse, rate-limited): directories plus database extensions only, metadata never file contents, NUL paths fall back to home.

SQL execution rewrites unaliased SELECT expressions via autoAliasSelectColumns so result grids get distinct column keys. MySQL string defaults now escape backslashes in normalizeDefault; Db2 caps migrated DECIMAL precision at 31 with a warning; BEGIN procedural blocks are treated as writes for safe-mode classification. Schema compare blueprint shows # position for columns and indexes; Monaco maps wire-compatible dialects to the right SQL grammar.

Reviewed by Cursor Bugbot for commit 50fc6b8. Bugbot is set up for automated code reviews on this repo. Configure here.

huyplb and others added 5 commits August 17, 2026 23:40
…tor syntax testing turned up

**Blueprint** now carries a `#` column in the column and index tables. "Column
id" is the ordinal in every catalog that exposes one under that name (Oracle
COLUMN_ID, SQL Server column_id), and it needs no new introspection — the
compare already builds the column list in the source table's own order.

The number comes from the *unfiltered* diff, not the rendered row index:
numbering visible rows would renumber them the moment "show unchanged" is off,
so column 7 would read as 2 — a worse lie than showing nothing.

**Editor syntax across dialects**, tested rather than assumed
(`dialect-syntax.test.ts`). What already works: MySQL DELIMITER blocks, Oracle
`/` terminators, Postgres dollar-quoted bodies, semicolons inside bracket,
backtick and string literals, and write detection for REPLACE / COPY / LOAD
DATA / SELECT INTO / MERGE / TRUNCATE.

One real fix: an anonymous `BEGIN … END` block read as a **non-write**, so Safe
Mode ran it without a confirmation. That is the shape this repo itself
generates for Db2 and Oracle tolerant drops, and the statement inside usually
sits in an `EXECUTE IMMEDIATE '…'` literal the scanner strips on purpose — so
it fails closed now. `BEGIN;` / `BEGIN TRANSACTION` stay quiet, since a
confirmation on those would only teach people to click through. The RBAC gate
was already correct here (its allowlist is fail-closed); this was the
confirmation dialog only.

Monaco now gives the wire-compatible relatives their family's grammar —
MariaDB/TiDB get MySQL, CockroachDB/YugabyteDB/Redshift get PostgreSQL —
instead of falling through to generic `sql` and losing backtick and
dollar-quote handling.

Two gaps are recorded as `it.fails` rather than quietly left: T-SQL `GO` is not
a terminator the splitter knows (a GO script arrives as one statement and the
server rejects it), and a bare `BEGIN … END;` block is chopped into fragments.
Both need the dialect, which `splitSqlStatements` does not take; `BEGIN` cannot
simply become an opener because it starts a transaction in Postgres and MySQL.
They will turn red when fixed, which is the signal to drop the `.fails`.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…ull screen

**One control per concept.** The sidebar carried four panels for two ideas: a
"Legend" that only showed the object-type colours, an "Object status" list that
only showed the status colours, and separately an "Object type" checkbox list
and a "Status filter" button row that did the actual filtering. The colour key
was never where the click was.

Now there are two: **Object type** and **Object status**, each row carrying its
own dot and doing the filtering when clicked. Verified in the browser —
clicking Deleted takes the graph from 14 nodes to 11 and back.

The type rows stay real checkboxes on purpose: `schema-history.test.ts` asserts
`isChecked()` on six of those testids, and turning them into buttons would have
broken the suite for a cosmetic reason.

**Script pane gets a maximize button.** The inline diff is capped at `max-h-56`
because it sits in a detail column beside everything else about the object,
which is fine for a short table and useless for a view or a routine body. The
header now has a maximize button that reopens the same diff full screen (Esc or
the backdrop closes it). It lives in `GithubScriptDiff`, so the object inspector
and the version compare modal both get it without either growing its own copy —
and the line renderer is shared between the two sizes rather than duplicated.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…mits

Identifiers have had plenty of attention this week; column DEFAULTs and type
specs had none. Both go into the DDL almost verbatim, so a sweep of hostile
defaults (quotes, semicolons, comment markers, backslashes, unicode) and edge
type specs ran against Postgres, MySQL, SQL Server, Db2 and Oracle.

The good news first: escaping holds. Every engine round-tripped
`x'; DROP TABLE victim; --` as data, including apostrophes, `--`, `/* */` and
unicode. Nothing executed out of a literal.

**MySQL lost backslashes.** `information_schema` reports a string default as a
bare value, so the provider builds the literal — and it doubled only the
apostrophe. MySQL treats `\` as an escape inside a string, so a default of
`C:\path\name` was re-emitted as `'C:\path\name'` and stored as `C:path` plus a
newline. Nothing failed; the migrated column just quietly had a different
default from the source. Proven both ways through the provider path: without the
fix `"C:\path\name"` became `"C:path\name"`, with it the copy matches the source
exactly.

**Db2 accepted a DECIMAL precision it cannot store.** Postgres, Oracle and SQL
Server all allow 38 digits; Db2 stops at 31 and answers SQL0604N from 32 up
(verified on 11.5). The source precision passed straight through, so a
cross-dialect migration into Db2 failed at CREATE TABLE — after the plan had
been reviewed and accepted. It is clamped now, with a warning naming the
narrowing rather than doing it silently.

Two results were **my harness, not the product**, and are worth writing down so
they are not rediscovered as bugs:

* SQL Server appeared to mangle `café ☕` into `café ?`. It does — for a plain
  `'…'` literal. But the provider carries `sys.default_constraints.definition`
  through verbatim, so a source default created as `N'…'` keeps its prefix. The
  sweep had built the literal by hand.
* A "LEAK" flag fired on every Db2 case. The probe was a bare `SELECT 1`, which
  Db2 rejects outright (SQL0104N) — it needs `FROM SYSIBM.SYSDUMMY1`.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…losing columns

`SELECT 1, 2` showed **one** column. The grid derives its columns from the keys
of each row object, so two result columns arriving under the same key collapse
into one and the other disappears — silently, with no error anywhere.

Measured against the live engines, this is a per-dialect problem:

| Dialect    | unaliased expression is called | effect |
|------------|--------------------------------|--------|
| Postgres   | `?column?` — every one         | all collapse into one |
| SQL Server | `''` (unnamed)                 | all collapse, blank header |
| MySQL      | the expression text            | collides when two match |
| Db2        | positional `1`, `2`            | fine |
| Oracle     | de-duplicated `1+1`, `1+1_1`   | fine |

`autoAliasSelectColumns` names them in the SQL before it runs, so every engine
returns distinct keys and the header reads like the query: `count(*)` becomes
`count`, a second one `count_2`, a lone literal `literal`, anything else
`col_N`. Generated names never shadow an alias the user already wrote.

It is deliberately conservative and hands the statement back untouched whenever
it is not a plain SELECT, when the select list will not parse, or when nothing
needs naming — plain and qualified columns, `*`, and existing explicit or
implicit aliases are all left exactly as typed. Half-written SQL is normal in an
editor, and mangling somebody's query is far worse than a missing grid column.

Verified through `runStatements`, the path the editor actually uses: without
the change Postgres reported `["?column?"]` for three of four cases; with it all
five engines return every column.

The parser is scanned rather than regexed — the first version had three
patterns eslint's security plugin flagged as ReDoS-prone, which matters when
the input is whatever the user has typed so far.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…you click

Five changes to the Lokee history surface, plus a friendly SQLite file picker.

Roadmap folding. A database captured over months has hundreds of versions and
any one table moved in a handful of them; printing one row per version buried
the few that mattered. `roadmap.ts` keeps the versions that changed the object,
the head, and whatever is being viewed, and folds each untouched run into one
row that opens on click. Rows gain a column delta measured against the real
previous version — not the previous visible row, so collapsing a gap cannot
change the number.

Sidebar. Object type leads (it decides what the graph is made of; every other
filter reads as a narrowing of it), then status, version, date, user. Sections
drag to reorder by their grip, the column drags to resize, and both persist per
browser. Version and User lists carry a permanently visible scroll track —
macOS hides overlay scrollbars, so a list that scrolled looked identical to one
that had been cut off.

Click-to-highlight, and the wiring bug under it. The node renderers already drew
a ring for a `selected` node, but the graph is fully controlled with no
`onNodesChange`, so React Flow's selection had nowhere to be written back and
`selected` was never true. `highlight.ts` applies selection from app state, and
lights the clicked object's whole lineage — one object owns one column for its
life, which is what the graph is for — fading the rest rather than hiding it.

The same missing `onNodesChange` was silently breaking edges. React Flow's
`parseHandles` drops a node's handle bounds when handed a replacement object
without `measured`, and `isNodeInitialized` then refuses to place any edge
touching it. Rebuilding on every filter change reset those bounds with no
channel for the re-measurement to return. Fixed with `useNodesState` +
`onNodesChange`, and `carryMeasurements` so a rebuild does not drop the edges
for a frame.

Reverting leaves the object inspector. It lives in the version compare modal,
which can scope a revert to ticked objects; a per-row button could only ever do
the whole schema, and two entry points with different blast radii was the trap.
The "Reused hash — stored once (pointer)" line goes too — storage internals.

SQLite/DuckDB credentials. Host, port, username, password and SSL are hidden for
a file on disk, "Database Name" becomes "Database File", and Browse… opens a
picker. A browser's file dialog yields a name and no path, so the picker lists
the machine running the backend: `GET /api/files/browse` returns directories and
only *.db/*.sqlite/*.sqlite3/*.duckdb, with name, size and mtime — never
contents — behind `schema.browse` and rate-limited. Deliberately not jailed to a
root: a signed-in user can already open any path by typing it, and the extension
filter is what keeps this from being a general file browser.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@cursor

cursor Bot commented Aug 18, 2026

Copy link
Copy Markdown
Contributor

Bugbot couldn't run - usage limit reached

Bugbot is counted against Cursor usage for this user or team, and this run hit a usage or spend limit.

A user or team admin can review and increase usage limits in the Cursor dashboard.

(requestId: serverGenReqId_59db89e7-9fba-4299-9680-8900b3611a57)

@huyplb
huyplb merged commit 21fe22b into main Aug 18, 2026
11 checks passed
@huyplb
huyplb deleted the feat/history-roadmap-sidebar-highlight branch August 18, 2026 21:12
huyplb added a commit that referenced this pull request Aug 18, 2026
…o longer has

Fallout from #271, already on main. Hiding host / port / username / password for
file dialects was right for the product and broke every e2e that saves a SQLite
credential: `addSqliteCredential` fed `conn-host-input` a value, and Playwright
sat waiting 30s for a field that is not rendered. `schema-revert.test.ts` failed
in `beforeAll` and skipped all four cases.

The helper now passes only what a file dialect shows — a path — and
`addCredential` fills host, port, username and password only when the caller
supplies them, so live-server dialects are unaffected.

This gets the suite past setup and into the tests. It is not green yet: the
target picker does not end up with the new credential selected, so
`snapshotTarget` finds its button still disabled. Root cause not yet found —
recorded here rather than left as a silent red.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
huyplb added a commit that referenced this pull request Aug 18, 2026
… and say it in the reader's words (#273)

* fix(lokee): a revert only runs against the current database, and snapshots first

Three changes to the revert path, one of them a correctness bug.

**What you review is now what runs.** The diff came from
`compareLokeeVersions(original, target)` while the plan and the execute call
went to `planLokeeRevert(original)` — which reverses from the *live head*,
ignoring Target entirely. With an older version on Target the reader reviewed
one script and Execute applied a different, usually larger one. Reverting
restores the live database, so it is only coherent when Target is the newest
version; anything else is refused, with a one-click "Use current database" to
fix it. An open dialog now follows the picker instead of holding the pair it
opened with, so that button visibly does something.

**Snapshot before touching anything.** The revert route captures the live schema
first. That leaves a version to come back to, but the reason that matters is
correctness: `planRevert` reverses from the newest *captured* version, not from
what is in the database, so a schema edited by hand since the last capture was
being reversed against a picture that no longer existed. When the snapshot finds
drift the request is refused (`schema_drifted`) rather than applied — the caller
reviewed a plan built on the old head, and running a different one silently is
the surprise this exists to stop. Verified live: a hand-added column is captured
as its own version and the revert leaves the database untouched.

**The version menus say which choices cannot be reverted.** The newest version
on Original reads "current, nothing to restore"; an older version on Target
reads "compare only". Both stay selectable — they are legitimate comparisons —
but they no longer look identical to a choice that can run and then dead-end at
a greyed-out button with the reason hidden in a tooltip.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* feat(history): say it in the reader's words, and stop showing filters that do nothing

A launch-readiness pass over the schema history module, reading it as someone
seeing it for the first time.

**One real bug, found by reading a card.** A deleted column came back from the
graph DTO typed as `table` — `objectType: row.object_type ?? 'table'`, and the
delta row carries no type. So a deleted column was drawn with a table's icon and
colour, and slipped through the default table filter as a phantom table. The key
already says what it is (`column:ORDERS.NOTE`), so the fallback now reads the
kind from the key.

**Names the database actually uses.** Children were labelled with their compare
key — `ORDERS.NOTE` — which CLAUDE.md is explicit is an uppercased match key and
never an identifier. Cards now read `NOTE` with `deleted from orders` beneath.

**Filters that can change what you see.** The object-type list was the union of
every dialect, so a SQLite user got MQT — a term only Db2 uses — beside
Procedure and Function boxes that could never match. It now offers the types the
history contains; a type the user ticked themselves stays. On the SQLite demo
that is 8 checkboxes down to 2.

**Words, not internals.** "reused" was the store's vocabulary for one object
pointed at by many versions; a reader is asking what moved. Summary reads
"12 unchanged", the legend "Unchanged", the edge key "Unchanged from previous
version". "Content-addressed schema history (Lokee)" — an internal codename and
an implementation detail — is now what the feature does. The header says Schema
history everywhere.

**A first run you can act on.** The empty state was a paragraph pointing at a
button on another bar: the one screen where a newcomer has nothing to act on was
the one screen with no action on it. It now holds the database picker and a
"Take first snapshot" button.

Also `[sqlite] /tmp/app.db.main` → `sqlite · /tmp/app.db · main`; the dotted
schema suffix read as a file extension.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix(lokee): stop crying data loss over a catch-up, and name where a run goes

The last launch blockers in schema history, all found by walking the flow the
user described — a database that has fallen behind, brought back up to date.

**The loudest warning was on the safest operation.** `ReversalRisk` defines
`lossy` as "succeeds but destroys or truncates data". Re-creating a dropped
column does neither: it adds one back, empty, because the rows went when it was
dropped. Classifying it lossy meant a plan of nothing but ADD COLUMN came back
`risk: lossy`, showed "This revert destroys data", and demanded a data-loss
acknowledgement — on the single most common thing anyone would do with version
history. It is now `safe`, and keeps the note explaining the column comes back
empty, because that is worth knowing while the gate is not.

Verified against a live database three versions behind: the plan went from
`risk: lossy, lossyCount: 2` to `risk: safe, lossyCount: 0`, and executes
without a confirmation, restoring both columns.

**The button says where it goes.** "Execute migration (3)" gave a count and no
destination, and read as an undo even when the plan only adds. Direction cannot
come from version numbers — the plan always runs from the head, so the target is
always the lower number — so it comes from what the plan does: a plan that
destroys nothing is catching a database up ("Update to v2"), one that destroys
something is rolling it back ("Revert to v2").

**The author line earns its place.** It answered "who did this?" only when the
versions disagreed; on a single-user install it was the same address repeated
down the whole column, crowding out the date and change count that do differ.
Shown only when the versions actually have different authors.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* chore(licensing): hide the React Flow badge, and ship the notices that are actually required

No Pro subscription, so the on-canvas "React Flow" badge is gone. That is
permitted: @xyflow/react is plain MIT (node_modules/@xyflow/react/LICENSE), and
its only condition is that the copyright and permission notice accompany the
software — displaying a badge in the UI is not a licence term. xyflow asks that
you subscribe when you hide it; that is a request, and this project has chosen
not to.

Which leaves the condition that *is* real, and was not being met. The client
bundles 38 third-party packages — React Flow among them — under MIT, BSD and
ISC, all of which require their notice to travel with the code. `NOTICE` only
carried Fox Schema's own Apache-2.0 terms, so shipping a build distributed their
code without their notices.

- `THIRD-PARTY-NOTICES.md` reproduces each package's own licence text,
  generated from the installed tree by `scripts/generate-third-party-notices.mjs`
  and regenerable when dependencies change.
- `.gitignore` blanket-ignores `*.md`, which would have kept the file out of the
  repo entirely; added to the tracked exceptions beside README and SECURITY.
- The Dockerfile copies LICENSE, NOTICE and the new file into the image, so the
  notices reach whoever runs the container rather than stopping at the repo.

Only gap: @google-cloud/secret-manager (Apache-2.0) ships no licence file of its
own; its entry names the licence and links upstream.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix(e2e): the credential helper still filled a host box that SQLite no longer has

Fallout from #271, already on main. Hiding host / port / username / password for
file dialects was right for the product and broke every e2e that saves a SQLite
credential: `addSqliteCredential` fed `conn-host-input` a value, and Playwright
sat waiting 30s for a field that is not rendered. `schema-revert.test.ts` failed
in `beforeAll` and skipped all four cases.

The helper now passes only what a file dialect shows — a path — and
`addCredential` fills host, port, username and password only when the caller
supplies them, so live-server dialects are unaffected.

This gets the suite past setup and into the tests. It is not green yet: the
target picker does not end up with the new credential selected, so
`snapshotTarget` finds its button still disabled. Root cause not yet found —
recorded here rather than left as a silent red.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* chore(lokee): drop an unused historyCompare import

Flagged by the code-quality bot on #273. Pre-existing rather than new — the
symbol had no reference before this branch either; the file being in the diff is
what surfaced it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
huyplb added a commit that referenced this pull request Aug 19, 2026
…wn family, and SQLite credentials selectable again (#274)

* fix(sync): selecting a saved SQLite connection asked for a password it cannot have

A real break, found by running the revert e2e before releasing rather than
after. `selectSavedConnection` treats "no stored password" as "prompt the user",
which was never reached for SQLite while the credential form still offered a
Save-password box. #271 hid that box for file dialects — correctly, a file has no
password — so SQLite credentials now save with `hasPassword: false`, and picking
one as Source or Target opened a password prompt, snapped the picker back to
"— Saved —", and left no connection selected. Snapshot, Compare and Migrate all
stayed disabled with nothing on screen explaining why.

Reproduced by hand in the browser, not inferred from the test: pick a saved
SQLite credential, watch the select revert.

`isFileDialect` / `dialectUsesPassword` now live in `provider-settings` beside
the rest of the per-dialect truth, and both the picker and the connection modal
read them instead of each carrying their own copy of the list.

Also updates the last e2e that asserted the old filter behaviour: it checked
`isChecked()` on Function and Procedure boxes, which this seed's schema does not
have and which are therefore no longer rendered — an assertion on an absent
control hangs rather than fails.

E2E after this, against the running app: schema-revert 4/4,
schema-version-revert-edges 10/10, schema-history 6/6.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix(utilities): index fragmentation named the wrong Postgres function, and MariaDB is not MySQL

Two reported failures, both reproduced against the live engines rather than
reasoned about.

**Index fragmentation on PostgreSQL could never have worked.** The probe read
`SELECT leaf_fragmentation FROM pgstattuple(ci.oid)`. Against PostgreSQL 17:

    -- extension absent (the default, and the reported error)
    ERROR:  function pgstattuple(oid) does not exist
    -- extension installed — the obvious fix
    ERROR:  column "leaf_fragmentation" does not exist

`pgstattuple` reports *table* statistics. `leaf_fragmentation` belongs to
`pgstatindex`, which returns 0 for a real index. So installing the extension
swapped one error for another; the feature was broken either way. Now uses
`pgstatindex(ci.oid::regclass)`, verified returning values on that server.

Two follow-ons. `pgstatindex` yields NaN for an index with no leaf pages yet —
"nothing measured", not a number, and `NaN%` in a column reads as a bug — so it
is nulled out. And a missing extension now explains itself: the panel says the
server needs `CREATE EXTENSION pgstattuple;` and that a superuser can add it,
instead of relaying the driver's sentence.

**MariaDB was aliased to the MySQL family and two probes were wrong on it.**
Against MariaDB 11.8:

    SELECT @@innodb_buffer_pool_instances;   -- ERROR 1193: Unknown system variable
    SELECT ... FROM performance_schema.global_status ...   -- (empty)
    SELECT ... FROM information_schema.GLOBAL_STATUS ...   -- 245412

The removed variable killed System info outright, which is the reported bug. The
empty one was quieter and worse: performance_schema is off by default on
MariaDB, so the pool panel showed blank connection counts with no error at all.
MariaDB is now its own family, reading status from `information_schema` and
dropping the variable it no longer has; sessions and sizes share the MySQL
queries, which do work on it.

Verified live, all four utilities: pool 151 max / 1 connected, sessions listed,
system up 245536s on 11.8.8-MariaDB, sizes reported per table.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant