Elyra
Elyra The coding agent eTerm The terminal that knows where each command ends Starf An activity monitor for Apple silicon that never invents a number etrans An SSH and SFTP client for macOS Elyra VM Virtual machines for macOS, Linux and Windows on your Mac Litr A small, native web browser for macOS Notr A notebook for macOS e The native code editor Elyra Grove Native local development environment Askr The real server for Laravel & PHP Elyra Framework Rust + Svelte 5 framework for desktop apps Elyra Conductor Local project conductor Refr Local-first PDF workspace for macOS Elyra Workspace A desktop workspace for coding agents Elyra SQL Server MySQL-compatible SQL server in Rust Elyra Félagi Agents as teammates on one board Elyra SQL Client Native desktop SQL workbench Elyra SQL Anywhere Replication-ready SQL engine Elyra Sjá SEO & GEO workspace for macOS Elyra DataGrid Server-driven data grid for Laravel
Release notes
Changelog
Elyra

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 no WHERE at all.

    The count is a SELECT COUNT(*) reassembled from the parsed statement's own table and WHERE (a literal LIMIT caps 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 an AUTO_INCREMENT already consumed, and it would hold the write's locks on a production table while the user reads the dialog. UPDATE, DELETE, TRUNCATE and DROP TABLE are counted; a multi-table UPDATE … JOIN is 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 .sql output 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 --mcp is 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 one SELECT/WITH/EXPLAIN/DESCRIBE/SHOW with no INTO, 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 with readonly=2); on the MySQL-wire engines each read runs in START TRANSACTION READ ONLY and 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. 2 rather than 1 because the statement timeout and the row cap are per-query settings, which 1 would refuse. Verified against 24.7: reads, the timeout and Stop's KILL QUERY all 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 ONLY but does not enforce it: an INSERT inside one succeeds (and a ROLLBACK undoes it). The agent access relies on the rollback there; MySQL and MariaDB refuse the write outright. SET SESSION TRANSACTION READ ONLY and the transaction_read_only variable 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_id as 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, marked fk?, 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 CONSTRAINT and 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-icon 0.24 → 0.25 and muda 0.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.
  • vite 8.3.0, @sveltejs/vite-plugin-svelte 7.3.1, svelte 5.57.1, @codemirror/state 6.7.6, @codemirror/view 6.43.13, @codemirror/commands 6.11.1, sql-formatter 15.9.0, ureq 3.4.2, rust_xlsxwriter 0.99.1.
  • Dependabot security alerts are on, alongside the daily cargo-deny audit. The first alert they raised — glib < 0.20, unsound VariantStrIter — 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.toml now 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/DELETE is 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 and INSERT.

    Introspection reads system.* rather than INFORMATION_SCHEMA, which on ClickHouse labels every object BASE TABLE — a view is never reported as one — and gives no row counts.

Fixed

  • A Decimal column could arrive rounded. ClickHouse sends Decimal as a bare JSON number, and the JSON value type routes those through an f64: a Decimal128(10) holding 1234567890.1234567890 came 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 writes before 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. run goes 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 a DROP, 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 SETS and aggregate-expression ORDER BY that 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. EXPLAIN used to report type=ALL with an empty Extra and 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 since EXPLAIN is twelve columns wide with Extra last, 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: 0 a leaf row, 1 a subtotal, 2 the grand total. One key's GROUPING() is not enough — with two keys the subtotal for a genuinely NULL group and the grand total both render as NULL, 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 NULL meant; 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 rustls 0.23.41, reached through ureq: 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 ROLLUP nor GROUPING() — 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 BY runs 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 BY was 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.
  • dirs 6 → 7, rust_decimal 1.43.0, ureq 3.4.1, syn 3.0.5, svelte 5.57.0, @codemirror/state 6.7.2, @codemirror/view 6.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 a WITH ROLLUP row cannot be told apart from a genuine NULL. Subtotals are therefore offered only with two grouping columns, where position makes it unambiguous.
  • EXPLAIN reports type=ALL with an empty Extra and 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 Queries group 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_xlsxwriter 0.98 → 0.99, syn 3.0.4, vite 8.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 edited Cargo.lock while leaving the version requirement in Cargo.toml alone. The two then disagree: cargo re-resolves back to the old version on an ordinary build, so the bump is never delivered, while scripts/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-cursor rule 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 accountables failed with "No database selected" while the navigator showed the database open and its tables listed. Picking a database set UI state and nothing else — no USE was 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 USE in 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.

  • TIMESTAMP columns rendered as empty cells. Not NULL, which the grid shows explicitly — blank. sqlx maps NaiveDateTime to DATETIME and refuses a TIMESTAMP column, 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. TIMESTAMP now decodes as DateTime<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; Enter does 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 ssh already on the machine rather than reimplementing SSH, so ~/.ssh/config, known_hosts, ssh-agent, hardware keys and ProxyJump all 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 ALL removes SELECT there rather than flooring an account at read, so the read tier grants it back; accounts are identified by name and host; and CREATE USER grants 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 SHOW columns 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. Preferred is 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 QUERY where 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, host is resolved on the bastion, so the usual 127.0.0.1 would otherwise have read as local and given the Free edition a route to any remote server.
  • mysql-rsa is deliberately not enabled. It would cover MySQL 8's caching_sha2_password over an unencrypted connection, at the cost of bringing back the rsa crate 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 .app inside it. Gatekeeper assesses what the user double-clicks, so 0.7.0 shipped as rejected — no usable signature until it was signed by hand.
  • User administration now goes through the same production-confirmation prompt as every other write. It used to pass confirmed: true unconditionally, 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/OFFSET with no ORDER BY let 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 sqlgen quoting rather than a second, disagreeing implementation.
  • dirs 5 → 6 (which also de-duplicates it: wry already required 6), rust_xlsxwriter 0.79 → 0.98, toml 1.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 BY and SET PASSWORD literals are redacted before the statement is recorded.
  • The licence key moved out of a cleartext file into the keychain.
  • open_update was 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 without KILL responds to — ElyraSQL 1.9.4 answers "statement not yet implemented"); KILL QUERY goes 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 as cancelled, 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 … COMMIT typed 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:role flag 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, and SET 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_query gained 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 ReferenceError every 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 previewChanges had the guard and it did not.
  • A bare BEGIN no longer fails with internal: 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, FLUSH and OPTIMIZE TABLE all 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?". CALL counts as a write, a CTE is classified by the statement it heads (WITH … DELETE is a write), and SET GLOBAL is separated from session-local SET.
  • 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; set UPDATE_PUBLIC_KEY once 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 start re-parsed its argument, so a path or URL containing & became command injection.
  • sqlx 0.9 removes the rsa dependency, and with it RUSTSEC-2023-0071 (the Marvin timing sidechannel) — the one advisory with no fixed release, which had to be tolerated. cargo deny now runs with nothing ignored.
  • Keyset cursors for numeric primary keys are re-parsed rather than interpolated as received, and BACKUP TO uses the central quoting helpers.

Performance

  • Reads are streamed instead of materialised. SELECT * FROM big_table pulled 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/elyrasql container. 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 runs cargo-deny.
  • The frontend is fully type-checked. svelte-check runs with checkJs and noImplicitAny on, over a ui store whose types reuse the generated bindings.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 every INSERT first. 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) and GROUP BY are 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 -c script, 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\b was stored as a\\b. Values that merely looked numeric were also inlined unquoted, turning text like 007 into 7. All SQL is now produced by one central, engine-aware and unit-tested quoting layer; LIKE filters escape %/_ correctly with an explicit ESCAPE.
  • 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/OFFSET with no ORDER 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/PRAGMA for 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.

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 ⌘K command palette.
  • SQL editor — CodeMirror 6 with ElyraSQL highlighting and schema-aware autocomplete, multi-statement execution, EXPLAIN plans, 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 ALTER preview, 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.