Changelog
All notable changes to ElyraSQL Client are documented here. The format is based on Keep a Changelog, and the project adheres to Semantic Versioning.
Unreleased
0.12.0 - 2026-10-04
Three additions — a production write that says what it will touch, a slice of a database that still holds together, and read-only access for AI agents — and a SQLite fix. A minor, by the semver line at the top of this file.
Added
-
The production confirmation says what a write will touch. It used to show the statement and ask "are you sure?", which nobody can answer about a
DELETE … WHERE created_at < '2024-01-01'without knowing whether it means forty rows or forty million. Now, before it asks, the client counts: Deletes 1,204 rows from orders, with the first five of them underneath, and in red when there is noWHEREat all.The count is a
SELECT COUNT(*)reassembled from the parsed statement's own table andWHERE(a literalLIMITcaps it), bounded to five seconds. The write is deliberately not run in a rolled-back transaction to count it: a rollback does not undo a trigger's side effects or anAUTO_INCREMENTalready consumed, and it would hold the write's locks on a production table while the user reads the dialog.UPDATE,DELETE,TRUNCATEandDROP TABLEare counted; a multi-tableUPDATE … JOINis reported as not counted, with the reason, because counting the join would give a number that looks authoritative and is not. -
Extract a slice (Premium). The rows a condition picks out of one table, plus every row their declared foreign keys point at, so the slice loads on its own — and optionally the rows that point at them: a customer, their orders, those orders' lines, and only the products on them. Keys are followed upwards from every row but downwards only from where you started, never back out through a shared parent, which is what keeps one customer from pulling in the whole database. Written to Downloads as a self-contained SQLite file (tables, keys, rows; Open as connection… adds it to the sidebar) or as
INSERTs in parent-first order for a database that has the schema. The source is only read. Binary columns are refused by name, as Data Transfer refuses them, and the slice stops at 100,000 rows rather than quietly dropping any — the reads themselves stop at the cap, so one wide step cannot pull millions of rows into memory first. The.sqloutput switches foreign key checks off while it loads on MySQL, MariaDB and SQLite, where a key cycle would otherwise have no order that loads. -
Agent access over MCP (Pro).
elyrasql-client --mcpis an MCP server: Claude Code and other agents can list, describe and read the connections you tick in Settings ▸ Agents — by name, with the password staying in the keychain. Reads only, enforced four times over: the statement is parsed and must be oneSELECT/WITH/EXPLAIN/DESCRIBE/SHOWwith noINTO, locking clause or executable comment (/*! */and MariaDB's/*M! */, which read as comments to a parser and as code to the server); functions that reach outside the database are refused by name, and on ClickHouse only table functions that stay on the server (numbers,values, …) are allowed — an allow list, because ClickHouse adds file- and network-reading ones every release; the connection is opened read-only (SQLite's file read-only, ClickHouse withreadonly=2); on the MySQL-wire engines each read runs inSTART TRANSACTION READ ONLYand is always rolled back; and every read stops after 30 seconds and returns at most the rows you allow. Every agent query is in your History, marked agent.
Changed
- A read-only ClickHouse connection is now read-only on the server too.
Requests carry
readonly=2, so ClickHouse itself refuses a write rather than only the client.2rather than1because the statement timeout and the row cap are per-query settings, which1would refuse. Verified against 24.7: reads, the timeout and Stop'sKILL QUERYall still work. - The query history records who ran a statement: the app, or an agent.
Fixed
- SQLite: a finished query could interrupt later ones. A statement timeout on SQLite is a progress handler installed on the connection, and it went back to the pool with the connection. Once its deadline had passed, every later statement on that connection — the navigator's catalogue reads, a page of the grid — failed with a bare "interrupted". The handler is now removed when the script that installed it is done.
Notes for the server
- ElyraSQL 1.12 accepts
START TRANSACTION READ ONLYbut does not enforce it: anINSERTinside one succeeds (and aROLLBACKundoes it). The agent access relies on the rollback there; MySQL and MariaDB refuse the write outright.SET SESSION TRANSACTION READ ONLYand thetransaction_read_onlyvariable are both rejected.
0.11.0 - 2026-09-26
The ER diagram, made into a real tool — and able to draw a schema with no connection at all.
Added
-
The ER diagram, rebuilt. It was a first sketch: column names only, a square grid, curves from table to table, and nothing at all to draw on a schema without declared foreign keys.
- Every column with its type and role — PK, FK, unique, nullable — from one query per engine instead of three calls that still could not say which column was the key.
- Automatic layout, left-to-right or top-to-bottom at three spacings, with referenced tables first so the diagram reads the way a schema does. Unrelated tables are packed underneath instead of stretching one row across the screen.
- Column-to-column relationships with crow's-foot ends, read from what the schema states: a nullable reference makes the parent optional, a unique one makes it one-to-one. "One or many" is never drawn, because no schema requires a parent to have children.
- Relationships inferred from naming —
user_id → users.id,parent_idas a self-reference — where none are declared, which is every ClickHouse database and many MySQL ones. Cautious by design, and never shown as a declared key: dashed, markedfk?, labelled in every export, and hidden by one switch. - Export to PNG, SVG, Mermaid, DBML and PlantUML.
- Positions are remembered per connection and database.
- Zoom around the pointer, fit to window, find a table, and highlight a table's neighbours.
- From pasted SQL, with no connection. ER diagram from SQL… reads
CREATE TABLE,ALTER TABLE … ADD CONSTRAINTand single-column unique indexes in nine dialects, or detects the dialect itself, and draws the same diagram. It is parsed on the machine and sent nowhere. One statement that cannot be read — a real dump always has some — is skipped and counted rather than failing the whole paste.
Changed
tray-icon0.24 → 0.25 andmuda0.19 → 0.20, bumped together. Dependabot offered them separately, but tray-icon depends on muda and the shell uses muda directly for the macOS menu, so each alone would have shipped two copies of the menu library. Together there is one.vite8.3.0,@sveltejs/vite-plugin-svelte7.3.1,svelte5.57.1,@codemirror/state6.7.6,@codemirror/view6.43.13,@codemirror/commands6.11.1,sql-formatter15.9.0,ureq3.4.2,rust_xlsxwriter0.99.1.- Dependabot security alerts are on, alongside the daily
cargo-denyaudit. The first alert they raised —glib< 0.20, unsoundVariantStrIter— is Linux-only, arrives through the GTK3 stack the webview depends on, has no fix short of GTK4, and is not called anywhere in the tree;deny.tomlnow records that decision rather than inheriting it from a default.
0.10.1 - 2026-09-19
Numbered as a patch at the maintainer's call; by the semver line at the top of this file this is an addition.
Added
-
Error help — when a query fails, the server's message is classified into a cause and the app offers the matching next step: the table's columns for an unknown column, user administration for a missing privilege, the timeout setting for a statement that ran long, Try again when the failure looks like timing rather than a mistake.
The case that repays it most is "this engine does not implement that". The SQL is valid elsewhere, nothing in the message says to rewrite it, and it is easy to lose ten minutes looking for a typo that is not there.
Uses Jev, TypeSafe's judgment model, with your own key — stored in the OS keychain. It returns a category, not prose, which is what lets the app turn the answer into a button instead of printing a paragraph. What is sent is the error message and the engine's name; not the statement, not the schema, not a row.
The hint sits beside the server's message and never replaces it, and appears only when the classification was confident — when the cause is genuinely ambiguous the app stays quiet. Nothing here gates anything: the write rails, the production confirmation and the read-only refusal are code, and have to work when the service does not.
0.10.0 - 2026-09-17
A fourth database engine. A minor, by the same reasoning as 0.9.0: this adds a kind of server the client could not talk to at all.
Added
-
ClickHouse, as a connection engine. Browsing, querying, the data grid, export, charts, profiles and the AI assistant all work against it; the port defaults to 8123, or 8443 with Require TLS, which is the only one ClickHouse Cloud exposes.
Driven over ClickHouse's HTTP interface rather than its MySQL-compatibility port. The shim would have cost almost no new code and was the wrong choice: it reports every column as a string, so the grid loses the types it uses to align numbers and render dates; it is absent on ClickHouse Cloud; and it offers no way to name a query so it can be cancelled. Over HTTP the grid gets real ClickHouse types, Stop works, and Statement timeout is enforced by the server.
Editing rows, the designer, structure and data synchronisation, and data generation are unavailable on ClickHouse. It has neither row-addressable updates nor transactions —
ALTER TABLE … UPDATE/DELETEis an asynchronous rewrite of data parts that returns before the change is visible and cannot be rolled back. Generating that under those names would do something other than what the button says, so it is refused rather than approximated. SQL you write yourself still runs, including DDL andINSERT.Introspection reads
system.*rather thanINFORMATION_SCHEMA, which on ClickHouse labels every objectBASE TABLE— a view is never reported as one — and gives no row counts.
Fixed
- A
Decimalcolumn could arrive rounded. ClickHouse sendsDecimalas a bare JSON number, and the JSON value type routes those through anf64: aDecimal128(10)holding1234567890.1234567890came back with its last digits silently gone, on exactly the columns a financial dataset cares about. Cells now keep the server's own digits. (64-bit integers were never affected — ClickHouse quotes those as strings for this very reason.)
0.9.0 - 2026-09-16
The client gained an assistant. A minor rather than a patch: this is the first release that adds a whole surface rather than extending one.
Added
-
AI Assistant (⌘I) — a Pro feature that writes, explains, optimises and migrates SQL against the database you have open, using your own API key from Anthropic, OpenAI or Google Gemini. Answers stream as they generate; each statement arrives in its own block with copy / insert / run, and a block that modifies data is marked
writesbefore you touch it.Each provider keeps its own keychain entry and its own model, so switching back does not ask for a key again — and one service's credential is never sent to another. The model list is fetched from the provider rather than baked in, so it cannot go stale and offer models your account does not have while hiding the one it does.
Nothing it writes runs on its own.
rungoes through exactly the path a hand-typed statement does — the production confirmation and the read-only refusal both still apply. An assistant that could execute what it wrote would be one prompt injection away from aDROP, and the schema it reads is influenced by anyone who can choose a table name.What is sent is the engine, the database, the table in focus and — unless you turn it off — table and column names. Row data never is: not the grid, not a sample, not a result. The panel shows the scope above the transcript rather than describing it somewhere else, and a very large schema is capped with the model told the listing is partial, so it asks about a table it cannot see instead of reporting that it does not exist.
The dialect differences found while building 0.8.6 and 0.8.7 are in the prompt, so it does not propose the
GROUPING SETSand aggregate-expressionORDER BYthat ElyraSQL rejects. -
Settings (⌘,), with panes for general preferences and the assistant. API keys are stored in the OS keychain alongside the database passwords — never in
meta.db, a file, or a log line.
0.8.7 - 2026-09-15
Three of the four server gaps reported when 0.8.6 was cut are closed in ElyraSQL 1.11.3, and the client now uses what they make possible. Verified by re-probing the running server rather than by reading release notes.
Added
- The aggregation strategy is visible.
EXPLAINused to reporttype=ALLwith an emptyExtraand say nothing about whether the parallel streaming path was taken, so the engine's headline capability was invisible even to its own client. It reports it now —Aggregate: parallel streaming (spills past 5000000 groups)— and sinceEXPLAINis twelve columns wide withExtralast, that line is lifted out and shown beside the result rather than left off the right edge of the grid. - Subtotals are marked rather than guessed at.
GROUPING()arrives in 1.11.3, so Summarize emits it, summed across every grouping key into a level:0a leaf row,1a subtotal,2the grand total. One key'sGROUPING()is not enough — with two keys the subtotal for a genuinelyNULLgroup and the grand total both render asNULL, NULL, …, 1, which is precisely the ambiguity the function exists to remove.
Changed
- Subtotals are offered with a single grouping column. They were restricted
to two because row position was the only clue to what a
NULLmeant; it no longer is. - CI pins ElyraSQL 1.11.3, and the pin is now a floor rather than a fixture: the Summarize query this release generates is rejected outright by 1.11.1.
ORDER BY COUNT(*)works on the server now, but the generator keeps ordering by the alias — that form works on every engine this client speaks to, and switching back would cost portability for nothing.
Fixed
- RUSTSEC-2026-0285 in
rustls0.23.41, reached throughureq: TLS 1.3 handshake messages were accepted across encryption level boundaries. That is the TLS stack the updater uses to fetch signed artifacts, so it is fixed rather than triaged — 0.23.45, a lock-only bump, with the network smoke test run against the real manifest over HTTPS to confirm the updater still works. - Subtotals are disabled on SQLite, which has neither
WITH ROLLUPnorGROUPING()— in the dialog, and again in the generator so a stale flag cannot emit SQL that engine rejects. The gap predates this release.
Notes for the server
GROUPING SETS, CUBE, CUME_DIST and PERCENT_RANK remain unsupported, so
nothing here is built on them. The first two are what a real pivot would want.
0.8.6 - 2026-09-12
Analysis moves to the server. The client used to study whichever rows it had already pulled; ElyraSQL aggregates through a parallel, streaming kernel whose memory is proportional to the number of groups rather than the number of rows, and nothing here knew that.
Numbered as a patch at the maintainer's call; by the semver line at the top of this file these are additions.
Added
- Charts can group in the database. The chart plotted whichever rows the
grid held — a query caps at 2,000 rows and the chart then took the first 500
of those — so charting a multi-million-row table drew an arbitrary prefix and
presented it as a distribution. Tick Aggregate in the database and the
GROUP BYruns server-side over the whole table, or over your query wrapped as a derived table. - Summarize, on a table's context menu: one or two grouping columns, an aggregate, optional subtotals. The SQL is shown and can be opened into the editor rather than run, because the query is the useful artefact — it can then be edited, saved or charted.
- Column profiles can scan the whole column. The profile sampled the first
200,000 rows because
GROUP BYwas assumed to be ruinous. The bound stays as the default, but the dialog now offers the whole column and reports how long either run took, which is what tells you whether it is worth asking again.
Changed
- CI pins ElyraSQL 1.11.1 rather than 1.9.4, verified by running the full live suite against it first — two minor versions of server behaviour with no change needed on our side.
dirs6 → 7,rust_decimal1.43.0,ureq3.4.1,syn3.0.5, svelte 5.57.0,@codemirror/state6.7.2,@codemirror/view6.43.10.
Notes for the server
Two things found by probing 1.11.1 rather than reading about it, both of which limit what a client can offer:
ORDER BY COUNT(*)is rejected with "ORDER BY references unknown output column"; the alias form is required. Everything generated here orders by the alias.GROUPING()is not implemented, so aWITH ROLLUProw cannot be told apart from a genuineNULL. Subtotals are therefore offered only with two grouping columns, where position makes it unambiguous.EXPLAINreportstype=ALLwith an emptyExtraand says nothing about whether the parallel aggregation was used, so no client can show when a query takes that path or defeats it.
0.8.5 - 2026-09-11
Saved queries are findable, and a class of dependency bump that quietly did nothing now fails in CI.
Numbered as a patch at the maintainer's call; by the semver line at the top of this file the sidebar section is an addition.
Added
- Saved queries are listed under the database they were written against, as
a
Queriesgroup beside that database's Tables and Views. Saved queries used to be global — nothing recorded where one came from — so a query written for one database showed up the same for every other. - Saved queries have a section in the sidebar, beneath the objects, holding the ones not tied to a database so nothing appears in two places. They were reachable only through the command palette, which is to say they were not findable at all. Three things had to be fixed for the section to work: the list was never fetched at startup, saving did not refresh it — so a query saved a moment earlier was absent until the next launch — and deleting one did not ask first.
Changed
rust_xlsxwriter0.98 → 0.99,syn3.0.4,vite8.2.2.
Fixed
- Close connection, Open connection and Delete connection did nothing. All three closed the context menu before reading the connection out of it, and the menu's data is derived from the menu being open — so each threw on its second statement and never ran. Only Close was reported; the other two were the same bug, and Delete being broken was luck rather than design.
- CI now passes
--locked. Twice, a dependency PR editedCargo.lockwhile leaving the version requirement inCargo.tomlalone. The two then disagree: cargo re-resolves back to the old version on an ordinary build, so the bump is never delivered, whilescripts/release.sh— which does pass--locked— fails outright. Both PRs passed every check, because nothing in the workflow passed--locked. The failure was invisible until release time. A dedicated step now runs first and names the problem in seconds.
0.8.4 - 2026-08-26
Fixed
- The SQL editor's caret is visible. It was a hairline that all but
disappeared against the dark background. The colour was not the cause:
drawSelection()was missing from CodeMirror's extension list, so it never rendered a caret of its own and the browser's native one stayed — which meant the theme's.cm-cursorrule had never applied, nor had.cm-selectionBackground. Both are live now, and the caret is 2px rather than 1px.
Changed
- The active line in the editor is tinted a little more strongly, and dragging text shows where it will land.
- Selection in the editor uses the theme's colour rather than the browser default, which follows from the rule above taking effect for the first time.
0.8.3 - 2026-08-23
Two things that were plainly broken on MySQL and MariaDB, and both were introduced by adding those engines in 0.8.1.
Fixed
-
Queries ran against no database.
SELECT * FROM accountablesfailed with "No database selected" while the navigator showed the database open and its tables listed. Picking a database set UI state and nothing else — noUSEwas ever sent — so the statement ran on a pooled connection that had no database selected, which is what a profile saved without a default has. The selection now happens on the connection the statement runs on, once, before the first statement of the script.Two engine differences came out of fixing it: MySQL refuses
USEin the prepared statement protocol, so it goes out unprepared; and where MySQL refuses an unqualified name with no database selected, ElyraSQL has exactly one database and resolves it anyway. -
TIMESTAMPcolumns rendered as empty cells. NotNULL, which the grid shows explicitly — blank. sqlx mapsNaiveDateTimetoDATETIMEand refuses aTIMESTAMPcolumn, so the decode failed, every later attempt failed too, and the value fell through to an empty-string fallback. A broken column looked like an empty one.TIMESTAMPnow decodes asDateTime<Utc>, and a value that genuinely cannot be decoded renders[unreadable <TYPE>]rather than nothing at all.ElyraSQL was not affected — checked rather than assumed, by running the new test against the old code.
Changed
- A new query tab opens empty instead of on
SELECT 1;, which every real use began by deleting.
0.8.2 - 2026-08-23
The navigator is a tree, and deleting a connection asks first.
Numbered as a patch at the maintainer's call, as with 0.8.1; by the semver line at the top of this file the navigator work is an addition.
Changed
- The navigator is one tree: connection → database → Tables/Views → object.
It was three disjoint sections — a connection list, a
<select>of databases, and a flat object list for whichever database that dropdown happened to be on. Expanding a connection is now what opens it, so there is no separate "connect, then go and find the databases" step, and several connections can be expanded at once. - Children load when a node is first expanded, not up front. A collapsed connection costs nothing, and a server with a hundred schemas no longer has to be enumerated before anything can be shown.
- Objects are grouped into Tables and Views. At a couple of hundred tables a single flat list is unreadable.
- The profile's default database now wins over "the first one listed", and the database selected on connect is expanded immediately.
Fixed
- Deleting a connection asks first. An
✕next to the connection name removed the saved profile and its stored password on a single click, with no confirmation and no undo. The action moved into the context menu behind a dialog that says what is removed and that the database itself is untouched;Enterdoes not confirm it, so it cannot be dismissed by reflex. - Disconnecting is visible. It was reachable only by right-clicking, which is why it read as missing. It is now a button on the connection row — the safe action in the place the destructive one used to occupy.
0.8.1 - 2026-08-23
Connect through an SSH tunnel, and to MySQL and MariaDB.
Numbered as a patch at the maintainer's call. By the semver line at the top of this file these are additions and would be 0.9.0; recorded here so the history reads honestly rather than the entry pretending to be smaller than it is.
Added
- SSH tunnelling (Pro). Forward a local port through a bastion and connect
to the database on the far side. It drives the
sshalready on the machine rather than reimplementing SSH, so~/.ssh/config,known_hosts,ssh-agent, hardware keys andProxyJumpall apply — and host key verification stays OpenSSH's job, because a tunnel that accepts any host key is a man-in-the-middle path into a production database that looks like it works. Authentication is non-interactive by design: keys or the agent, no password field, and an unknown host key is refused rather than accepted. - MySQL and MariaDB in the engine list. They share the wire protocol with
ElyraSQL, and the places they differ are handled explicitly: sqlx's handshake
preamble is kept for them and disabled only for ElyraSQL (which rejects it);
REVOKE ALLremoves SELECT there rather than flooring an account at read, so the read tier grants it back; accounts are identified by name and host; andCREATE USERgrants nothing, so even read has to be applied. The default port follows the engine.
Fixed
- Names no longer appear as byte counts on MySQL. MySQL reports
INFORMATION_SCHEMA and
SHOWcolumns with a binary collation, and the cell formatter renders anything binary as[N bytes]to avoid decoding a BLOB as text — so databases listed as "[21 bytes]" and column types as "[3 bytes]". Metadata is now read through a path that decodes valid UTF-8; user data keeps the conservative rule, and a test asserts a real BLOB is still reported as bytes. - A failed TLS handshake no longer kills the connection when the profile did
not require TLS.
Preferredis supposed to mean "encrypt if offered", but a server advertising TLS with a certificate we will not accept could not be connected to at all. It now falls back to plaintext — never when TLS is required — and the session's real encryption state is read from the server. - Cancelling a query now uses
KILL QUERYwhere the server implements it, instead of always falling back to closing the connection.
Security
- SSH tunnelling counts as a remote connection whatever the host field says.
With a tunnel,
hostis resolved on the bastion, so the usual127.0.0.1would otherwise have read as local and given the Free edition a route to any remote server. mysql-rsais deliberately not enabled. It would cover MySQL 8'scaching_sha2_passwordover an unencrypted connection, at the cost of bringing back thersacrate and RUSTSEC-2023-0071, which the sqlx 0.9 migration removed. TLS is on by default in MySQL 8 and MariaDB uses native passwords, so the gap is narrow, and the error names the two ways out.
0.8.0 - 2026-08-23
Updates are now signature-verified, and a security review is closed out at all three severities. Nothing here changes how the app is used day to day; it changes what the app refuses to do.
Upgrading: this is the first release that verifies an ed25519 signature over each downloaded artifact, not just its SHA-256. An update without a valid signature is refused outright. That is deliberate — a missing signature is exactly what an attacker who rewrote the manifest would produce — but it means the published manifest must carry signatures from here on. Both the release script and the deploy script now refuse to produce or publish one that does not.
Added
- Signed updates. The client bundles an ed25519 public key and verifies each artifact's signature before installing it. The update dialog reports what it verified rather than implying more.
elyra-sign, a small workspace tool that signs the release manifest and re-checks it before upload. Signing had no implementation at all before this, which is why the verifying half sat unused behind an empty key.scripts/release.sh, replacing the release steps that lived in one person's shell history. It signs the app and the disk image, refuses to build a manifest for an unstapled dmg, and refuses to build an unsigned one.
Changed
- The disk image itself is signed, not only the
.appinside it. Gatekeeper assesses what the user double-clicks, so 0.7.0 shipped asrejected — no usable signatureuntil it was signed by hand. - User administration now goes through the same production-confirmation prompt as
every other write. It used to pass
confirmed: trueunconditionally, on the reasoning that server configuration is not a data write — which had it exactly backwards for dropping an account or granting admin. - Table browsing without a single-column primary key now orders by every column.
LIMIT/OFFSETwith noORDER BYlet the server return rows in any order, and it need not be the same order between the two round-trips a second page needs, so rows were silently skipped or repeated. - Column-type validation in the designer is an allowlist rather than a blocklist.
- CSV export defuses cells beginning
=,+,-,@, tab or CR, which a spreadsheet would otherwise execute as a formula. - Export literals go through the shared
sqlgenquoting rather than a second, disagreeing implementation. dirs5 → 6 (which also de-duplicates it: wry already required 6),rust_xlsxwriter0.79 → 0.98,toml1.1.4.
Fixed
- Binary columns are no longer silently corrupted. Rows are read through the
same stringifying path the grid uses, which renders a BLOB as
[N bytes]; copying that wrote the literal text into the target and reported success. Transfer and data sync now refuse binary columns instead. - Transfer truncated outside its transaction. A failure after the truncate left the target empty with no way back.
- Passwords no longer reach the query history.
IDENTIFIED BYandSET PASSWORDliterals are redacted before the statement is recorded. - The licence key moved out of a cleartext file into the keychain.
open_updatewas constrained to the update staging directory rather than opening any path the frontend passed.- Keychain access and XLSX writing moved off the async runtime, where they froze every other command — including the Stop button — for their duration.
Security
- The update chain no longer accepts an empty or missing checksum, and no longer strips the download quarantine attribute.
- Downgrades are refused: an update is offered only when the manifest is newer.
0.7.0 - 2026-08-17
Stop a running query, role-based user administration, and a security pass that closes several rails which were failing open. It is also the first release verified end to end against a real ElyraSQL Server in CI.
Upgrading: TIMESTAMP values now arrive in the server's timezone rather
than UTC. sqlx used to pin the session to +00:00, but ElyraSQL Server rejects
that variable outright, and pinning it was the reason the client could not
connect to 1.9.4 at all. There is nothing to choose between here — the server
offers no way to set it.
Added
- Stop a running query —
⌘., or the Stop button that replaces Run while a query is in flight. It genuinely stops the statement rather than abandoning the results, by whichever lever the engine offers: the pending read is dropped and the connection closed (which is what a server withoutKILLresponds to — ElyraSQL 1.9.4 answers "statement not yet implemented");KILL QUERYgoes out on a second connection where the server supports it; and SQLite installs its own progress handler so the statement aborts itself. Cancelling is recorded in the history ascancelled, not as an error. - Per-connection statement timeout, set in the connection form and off by default. A statement that outruns it is stopped by the same machinery.
- A script now runs every statement on one reserved connection. That is what
gives cancellation a single session to target, and it repairs
BEGIN…COMMITtyped into the editor, which previously could land each statement on a different pooled connection.
Changed
-
The Users panel sets a role instead of ticking privileges. ElyraSQL Server doesn't implement MySQL's fine-grained privileges — it has the three coarse tiers its
--auth user:password:roleflag takes,read | write | admin, and they are a ladder: every account has at least read, and admin includes write. Three checkboxes implied combinations that don't exist, and unticking "read" did nothing at all. Each user now has one Role selector, and new users can be created directly at a tier.Privileges apply per user name on this server, so two accounts differing only by host share a role. The panel says so.
Fixed
- The client could not connect to ElyraSQL Server 1.9.4 at all. sqlx opens
every MySQL connection with a compatibility preamble aimed at MySQL and
MariaDB, and the server rejects two parts of it: the
SET sql_mode=(SELECT CONCAT(@@sql_mode, …))subquery, andSET time_zone. Neither is anything this client needs. See the upgrade note above for the one consequence. - Setting a user's privileges never worked against this server. The panel
emitted
GRANT … TO 'bob'@'%', which its parser rejects — it will not take a quoted user name before@. - Creating a database and the confirm-SQL dialog were broken by the query
cancellation work:
run_querygained an argument and two call sites were missed. Nothing caught it until the frontend was type-checked. - The table designer's ALTER preview threw a
ReferenceErrorevery time it was opened, referencing an undeclared variable. - Applying grid edits could crash when the browse tab was closed while the
production-confirm dialog was open; its sibling
previewChangeshad the guard and it did not. - A bare
BEGINno longer fails withinternal: write attempted without authorization.
Security
- The write rails no longer fail open. They gated on "is this statement
definitely a write?", so anything the classifier didn't recognise took the
read path and executed without a
WriteGuard—CALL a_procedure(),VACUUM,FLUSHandOPTIMIZE TABLEall ran on a connection marked read-only, and passed the Free edition's write block. The gate is now "is this positively known to be harmless?".CALLcounts as a write, a CTE is classified by the statement it heads (WITH … DELETEis a write), andSET GLOBALis separated from session-localSET. - The IPC surface is no longer reachable cross-origin in production. Command
and event responses carried
Access-Control-Allow-Origin: *in packaged builds. CORS is now emitted only for the dev server, and echoes its origin. - The webview is pinned to the app's own origin, popups are denied, and a
Content-Security-Policy ships with the document. A remote page loaded in the
shell would otherwise sit same-origin with
/__cmd/*. - Update artifacts can be signature-verified. The SHA-256 comes from the
same manifest as the binary, so it only proved the download arrived intact.
Ed25519 verification is wired through
elyra::updater; setUPDATE_PUBLIC_KEYonce release signing is published, and unsigned artifacts are refused from that point. The banner states which check applies. - Windows opens files and URLs without a shell.
cmd /C startre-parsed its argument, so a path or URL containing&became command injection. - sqlx 0.9 removes the
rsadependency, and with it RUSTSEC-2023-0071 (the Marvin timing sidechannel) — the one advisory with no fixed release, which had to be tolerated.cargo denynow runs with nothing ignored. - Keyset cursors for numeric primary keys are re-parsed rather than interpolated
as received, and
BACKUP TOuses the central quoting helpers.
Performance
- Reads are streamed instead of materialised.
SELECT * FROM big_tablepulled the entire table into memory and stringified every cell before discarding all but the first 2 000 rows. Results now stop at the cap on the way in, so memory stays flat and the first rows arrive sooner.
Internal
- The live suite runs in CI against a pinned
ghcr.io/kwhorne/elyrasqlcontainer. Until now nothing in CI touched the engine this client is built for — everything ElyraSQL-specific was covered only by tests someone had to remember to run. - CI also gates on
clippy --all-targets -D warnings, builds on Linux and Windows as well as macOS, and runscargo-deny. - The frontend is fully type-checked.
svelte-checkruns withcheckJsandnoImplicitAnyon, over auistore whose types reuse the generatedbindings.ts— so the frontend is checked against the Rust command signatures. Three latent bugs surfaced doing it; they are listed under Fixed. - The 1660-line state module is split into a store plus eight domain modules.
- Dependency majors landed individually rather than as one unmergeable batch: sqlx 0.9, ureq 3, Vite 8, ed25519-dalek 3, sha2 0.11, and others.
0.6.0 - 2026-07-28
A security, correctness and performance release. It closes every finding from an audit of the data paths, so upgrading is recommended — particularly if you use SQLite connections or the Tools.
Performance
- Data Transfer is ~1.8× faster and no longer scales with table size in
memory. It now pages by keyset on the primary key instead of growing
OFFSETs, and streams rows straight into one transaction rather than buffering everyINSERTfirst. Measured on 200k rows: 408 ms → 225 ms, and cost per row is flat from 20k to 600k rows (1.1 µs/row). - Data Synchronization streams its statements into a single transaction and returns only a bounded preview (500 statements) instead of one per differing row, so a large sync no longer pushes megabytes through the UI boundary.
- Column profile is bounded to a 200k-row sample (
COUNT(DISTINCT)andGROUP BYare full scans) and says so when the figures are sampled. - Charts no longer silently plot a subset: the cap is 500 rows and the chart states when it's showing a prefix of the loaded rows.
- The database-wide foreign-key map for SQLite (ER diagram) resolves in one query instead of one per table.
Security
- Unencrypted remote connections are now visible. TLS was opportunistic, so a connection could quietly fall back to clear text. The app now asks the server whether the session is actually encrypted and shows a 🔒 when it is — or a ⚠ unencrypted warning for a remote connection that isn't. Require TLS is also enabled by default for any non-local host.
- Updates no longer build a shell command. Installing an update interpolated
file paths into a
/bin/sh -cscript, so a path containing quotes or$could break the update or run arbitrary commands. The app now re-executes itself as a helper that receives its arguments as argv and performs the swap in Rust, with the previous version restored if the copy fails.
Fixed
- Values could be corrupted on SQLite. MySQL-style backslash escaping was
applied to every engine, so
a\bwas stored asa\\b. Values that merely looked numeric were also inlined unquoted, turning text like007into7. All SQL is now produced by one central, engine-aware and unit-tested quoting layer;LIKEfilters escape%/_correctly with an explicitESCAPE. - The production and read-only rails only covered the query editor. Data Transfer ignored read-only entirely, and none of the Tools asked before writing to a production connection. Authorization now happens once in the connection broker and is required by the type system, so every write path — Tools, import, inline editing and the designer — is covered, and writes to a production target prompt for confirmation.
- Data Transfer could skip or duplicate rows. Pagination used
LIMIT/OFFSETwith noORDER BY, which has no guaranteed row order between round-trips; it now orders by primary key and copies each table in a single transaction so a mid-way failure can't leave a partial copy.
0.5.0 - 2026-07-17
Added
- Data Synchronization (Tools) — make a target table's rows match a source's (insert/update, optional delete), keyed by the primary key, with a reviewable diff before applying (Premium).
- Column profile — right-click a column header → Profile column for instant stats: row/null/distinct counts, min/max, and the most common values.
- Charts — visualise any query result or browsed table as a bar / line / area / pie chart (pick an X-axis and numeric series). Toggle grid/chart from the grid toolbar or under query results; renders from the loaded rows.
- Structure Synchronization (Tools) — compare two databases' schema and generate/apply the statements that make the target match the source (new tables and missing/changed columns); non-destructive, with a reviewable script (Premium).
0.4.1 - 2026-07-12
Changed
- Updates now install automatically. Choosing Update & restart downloads the verified build and installs it in place — replacing the app bundle (macOS) or binary (Linux) and relaunching — with no drag-to-Applications step. If the install location isn't writable it falls back to a plain download.
0.4.0 - 2026-07-12
Added
- Tools menu (toolbar) gathering database-wide utilities:
- Data Transfer — copy all or selected tables (schema + rows) between databases or connections, with create tables and empty target first options and a per-table report (Premium).
- Data Generation — fill a table with plausible test data inferred from each column's type and name (names, emails, countries, dates, numbers, sequential primary keys) (Pro).
- Quick access to Data dictionary, Console, Find in database, Server monitor and History log.
- Right-click context menus in the sidebar:
- Connection — open/close, edit, duplicate, delete, new query, new database, a colour tag, and refresh.
- Database — new query, find, refresh, copy name, dump schema, new/drop database, and backup.
- Filter & Sort panel for the data grid: multiple filter criteria plus server-side multi-column sort (whole-table, not just loaded rows).
- A Navicat-style icon toolbar above the grid (refresh, filter & sort, add row, import, export, and revert / review & apply).
- The command palette now also opens with ⌘/Ctrl + P.
Fixed
- Command-palette keyboard navigation scrolls the highlighted item into view.
0.3.1 - 2026-07-11
Fixed
- Clicking a table now shows its rows. The initial browse page mutated a non-reactive tab reference, so the grid stayed empty (“0 loaded · loading…”) even though the data had loaded. It now updates through the reactive store.
0.3.0 - 2026-07-11
Added
-
SQLite support. Connections can now target a local SQLite file as well as ElyraSQL Server — pick the engine in the connection dialog. Browsing, querying, sorting, filtering, editing, import and export all work; the navigator uses
sqlite_master/PRAGMAfor schema, columns and foreign keys. ElyraSQL-only tools (server status, process list, users, backup) report that they're unavailable on SQLite. -
Automatic updates — the app checks elyracode.com on launch and shows a banner when a newer build is available; Download & install fetches the right artifact for your platform, verifies its SHA-256, and opens it. Also available from the command palette (“Check for updates…”).
-
Click a column header to sort the loaded rows (ascending → descending → off), type-aware with NULLs last — in both browse and query grids.
-
All features are now always visible. Actions above your edition stay in the menus/palette and, when used without a license, open a friendly upgrade dialog linking to https://elyracode.com/sql/client (rather than being hidden). Remote connections, editing, import, export formats, Query Builder, ER diagram, designer, backup and server tools are all discoverable.
-
About dialog with the version/edition and links (command palette or click the toolbar logo).
Changed
- Browsing a table now loads up to 1000 rows per page (was 200).
- The connection dialog shows the Test result inline (previously the result only appeared in the status bar, hidden behind the modal).
0.2.0 - 2026-07-10
Added
- Single binary + online license (elyracode.com): one build for everyone; a license key unlocks Pro/Premium at runtime. Validated online, cached with a 14-day offline grace period; revocation/seats enforced server-side. Account & license dialog in the app (click the edition badge).
- Editions (Free / Pro / Premium) enforced in the Rust core:
- Free — local, read-only: browse and run
SELECT, history, saved queries, CSV export. - Pro — remote connections, writes/DDL, inline editing, filters, FK jump, CSV import, all export formats, Query Builder.
- Premium — ER diagram, table designer, backup, and server tools (status/variables, process list, user administration). The UI shows the edition and marks locked actions.
- Free — local, read-only: browse and run
Changed
- Now a fully self-contained workspace — the desktop shell/runtime crates
are vendored under
crates/, with no external framework dependency. - Added a complete MkDocs documentation site under
docs/.
0.1.0 - 2026-07-10
First public release.
Added
- Connections — connection library with dev/staging/prod colour tags, secrets in the OS keychain, optional TLS, "test connection" diagnostics, and production safety rails (confirm-before-write, global read-only).
- Navigator — searchable object tree, context menu, metadata/DDL panel,
tabbed workspace, and a
⌘Kcommand palette. - SQL editor — CodeMirror 6 with ElyraSQL highlighting and schema-aware
autocomplete, multi-statement execution,
EXPLAINplans, formatting, comment toggle, searchable history, and saved queries. - Visual Query Builder — drag in tables, tick columns, auto-joined from foreign keys, with live SQL.
- Data grid — virtualized rendering with server-side keyset pagination, inline editing with a SQL preview committed in one transaction, add/delete rows, no-SQL column filters, and foreign-key jump to referenced rows.
- Modelling — table designer with
ALTERpreview, and an ER diagram of a schema's foreign-key relationships. - Import/export — CSV, JSON, SQL and XLSX export; CSV import; hot database backup.
- Server tools — status & variables, live process list with kill, and user administration (create/drop, grant/revoke).
Changed
- Ejected the Elyra Framework starter: the desktop shell/runtime crates are now
vendored under
crates/and the client builds standalone with no external framework dependency.