<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom">
  <channel>
    <title>Tabularis Blog</title>
    <link>https://tabularis.dev/blog</link>
    <atom:link href="https://tabularis.dev/feed.xml" rel="self" type="application/rss+xml" />
    <description>Releases, guides and product notes from Tabularis — the open-source desktop database client.</description>
    <language>en</language>
    <lastBuildDate>Fri, 05 Jun 2026 11:00:00 GMT</lastBuildDate>
    <item>
      <title>v0.13.1: Signed macOS Builds, a Postgres Explain That Finally Runs, and Pagination That Honors Your OFFSET</title>
      <link>https://tabularis.dev/blog/v0131-signed-macos-postgres-explain-offset-pagination</link>
      <guid isPermaLink="true">https://tabularis.dev/blog/v0131-signed-macos-postgres-explain-offset-pagination</guid>
      <pubDate>Fri, 05 Jun 2026 11:00:00 GMT</pubDate>
      <description>v0.13.1 is a correctness pass on v0.13.0: macOS builds are now code-signed and notarized, the Postgres Explain Plan that never worked now runs, paginated queries stop dropping your OFFSET, postgresql:// and mariadb:// connection strings are accepted, the MCP read-only gate stops misreading a parenthesized SELECT as a write, the grid no longer freezes on giant JSON cells, and the macOS Keychain stops prompting on every AI-tab open.</description>
      <content:encoded><![CDATA[<h1>v0.13.1: Signed macOS Builds, a Postgres Explain That Finally Runs, and Pagination That Honors Your OFFSET</h1>
<p><strong>v0.13.1</strong> is a short follow-up to <a href="https://tabularis.dev/blog/v0130-kubernetes-tunnels-quick-navigator-dml-tabs">v0.13.0</a>. Where the last release was about <em>reach</em> — Kubernetes tunnels, a Quick Navigator, MCP into plugin drivers — this one is about <em>correctness</em>: a sweep of features that shipped but quietly didn&#39;t work, plus the distribution-level fix Mac users have been asking for since the first DMG.</p>
<p>No new surface area. Several things that were broken, fixed. Four external contributors land in this tag.</p>
<hr>
<h2>Signed and Notarized on macOS</h2>
<p>Until now, opening Tabularis on macOS meant a trip through Gatekeeper: the &quot;tabularis cannot be opened because the developer cannot be verified&quot; dialog, or an <code>xattr -c</code> incantation copied from the install docs. The app was never signed, so every Mac treated it as quarantined.</p>
<p>PR <a href="https://github.com/TabularisDB/tabularis/pull/289">#289</a> wires the release workflow to sign and notarize the macOS build. The <code>.app</code> and <code>.dmg</code> are now signed with a Developer ID Application certificate and submitted to Apple&#39;s notarization service during the release build — the App Store Connect API key is decoded from a secret into a temp file on the macOS runners only, and the Apple env vars are inert on the Linux and Windows jobs since Tauri only consumes them when bundling for macOS.</p>
<p>What this means for you: a notarized <code>.dmg</code> opens with the normal &quot;downloaded from the internet&quot; confirmation and nothing more. No <code>xattr</code>, no Privacy &amp; Security override, no &quot;unverified developer&quot; wall. If you&#39;ve been keeping the workaround command in a note, you can delete it.</p>
<hr>
<h2>Postgres Explain Plan, Now Actually Running</h2>
<p>The <a href="https://tabularis.dev/wiki/visual-explain">Visual Explain</a> feature has worked on MySQL, MariaDB, and SQLite since it shipped. On PostgreSQL it failed every single time with <code>error deserializing column 0</code> — and it turns out it never worked on any Postgres version.</p>
<p><a href="https://github.com/NewtTheWolf">@NewtTheWolf</a> (Dominik Spitzli) and the maintainer track it down in PR <a href="https://github.com/TabularisDB/tabularis/pull/279">#279</a> (closes <a href="https://github.com/TabularisDB/tabularis/issues/276">#276</a>). <code>EXPLAIN (FORMAT JSON)</code> returns the plan in a single column, and on Postgres that column is typed as <code>json</code> (OID 114), not <code>text</code>. The code read it straight into a <code>String</code>, and <code>tokio-postgres</code> refuses to deserialize a <code>json</code> column into a <code>String</code> — so the call errored out before the plan was ever parsed. This is server-side behavior that&#39;s been stable since the <code>FORMAT</code> option landed in PG 9.0, which is why the report reproduced on both PG 16 and PG 18.</p>
<p>The fix reads the column as a <code>serde_json::Value</code> and re-serializes it for the existing parser, with a <code>String</code> fallback for Postgres-compatible engines that hand the plan back as plain text. If you&#39;ve ever clicked &quot;Explain Plan&quot; on a Postgres connection and gotten an error, that was this.</p>
<p><video src="https://tabularis.dev/videos/posts/tabularis-explain-postgres.mp4" poster="/videos/posts/tabularis-explain-postgres.jpg" autoplay loop muted playsinline style="width:100%;border-radius:8px;margin:1rem 0"></video></p>
<hr>
<h2>Pagination That Honors Your OFFSET</h2>
<p>When the grid paginates a <code>SELECT</code>, it rewrites the query: it strips your trailing <code>LIMIT</code>/<code>OFFSET</code>, then re-appends <code>LIMIT &lt;fetch&gt; OFFSET &lt;page offset&gt;</code>. The rewriter only read back your <code>LIMIT</code> — the <code>OFFSET</code> was dropped on the floor. On page 1 the per-page offset is <code>0</code>, so <code>LIMIT 1 OFFSET 1</code> quietly became <code>LIMIT 1 OFFSET 0</code>, and your OFFSET was ignored.</p>
<p>PR <a href="https://github.com/TabularisDB/tabularis/pull/275">#275</a> (fixes <a href="https://github.com/TabularisDB/tabularis/issues/273">#273</a>) adds <code>extract_user_offset</code>, mirroring the existing token-aware <code>extract_user_limit</code> so an <code>OFFSET</code> inside an identifier or string literal isn&#39;t misread, and <code>build_paginated_query</code> now adds your OFFSET to the per-page offset. Pagination walks the rows you actually asked for. Because all three SQL drivers share <code>build_paginated_query</code>, the fix lands on Postgres, MySQL, and SQLite at once — with regression tests including the exact case from the issue and OFFSET-without-LIMIT on both page 1 and page 2.</p>
<p>There was a folk remedy floating around: appending a trailing <code>--</code> &quot;fixed&quot; it. The reason is grimly funny — the comment broke the stripper&#39;s pattern match, so the appended pagination clause landed on the same line as the <code>--</code> and got swallowed as a comment, and the database ran your original query verbatim. Correct result, entirely by accident. You don&#39;t need the <code>--</code> anymore.</p>
<hr>
<h2>Connection Strings Stop Silently Failing</h2>
<p>Pasting <code>postgresql://user@host/db</code> into the New Connection modal did nothing: no fields populated, no error, and the green success indicator still rendered. The connection-string protocol registry was built only from each driver&#39;s id and example, so for PostgreSQL only <code>postgres</code> was ever registered — <code>postgresql://</code> matched nothing and was silently skipped.</p>
<p>PR <a href="https://github.com/TabularisDB/tabularis/pull/277">#277</a> (fixes <a href="https://github.com/TabularisDB/tabularis/issues/260">#260</a>) registers well-known scheme aliases — <code>postgresql</code> ↔ <code>postgres</code>, <code>mariadb</code> ↔ <code>mysql</code>, <code>sqlite3</code> ↔ <code>sqlite</code> — in a second pass that never overrides a protocol a driver registered explicitly, so a dedicated <code>mariadb</code> plugin would still win over the alias. The modal&#39;s <code>looksLikeConnectionString</code> pre-filter is gone: input always runs through the parser now, an unrecognized scheme produces a real error (<code>Unsupported database driver: oracle. Supported: mariadb, mysql, postgres, postgresql, sqlite, sqlite3</code>), and the green check only appears when the string actually parsed and populated the form.</p>
<p><img src="https://tabularis.dev/img/tabularis-connection-string-aliases.png" alt="The New Connection modal parsing a connection string into host, port, username, and password fields, with the green check confirming a successful parse"></p>
<hr>
<h2>MCP: A Parenthesized SELECT Is a Read Again</h2>
<p>v0.13.0 <a href="https://tabularis.dev/blog/v0130-kubernetes-tunnels-quick-navigator-dml-tabs">rebuilt the MCP safety gates</a> to fail closed on multi-statement payloads. v0.13.1 fixes a false positive in the same classifier, reported and fixed by <a href="https://github.com/ymadd">@ymadd</a> in PR <a href="https://github.com/TabularisDB/tabularis/pull/272">#272</a>.</p>
<p><code>(SELECT ...) UNION ALL (SELECT ...)</code> — the shape you get when each UNION branch needs its own <code>ORDER BY</code>/<code>LIMIT</code> — starts with a <code>(</code>, so <code>first_keyword</code> returned empty and the query was classified as &quot;unknown&quot;. That tripped both the <a href="https://tabularis.dev/wiki/mcp-readonly-mode">read-only mode</a> and the <a href="https://tabularis.dev/wiki/mcp-approval-gates">write-approval gate</a>: a pure read raised a write prompt. The classifier now peels leading <code>(</code> and whitespace before reading the first keyword, so the inner <code>SELECT</code> is detected — while multi-statement detection still runs first (so <code>(SELECT 1); DROP ...</code> stays &quot;unknown&quot;), and the greedy peel never downgrades a parenthesized write or DDL to &quot;select&quot;. Regression tests cover parenthesized UNION, nested and whitespace-padded parens, empty parens (which fail closed), parenthesized DDL/DML, and a writing CTE.</p>
<hr>
<h2>The Grid Stops Freezing on Large JSON</h2>
<p>Open a table with a fat <code>JSON</code> column — say a MySQL <code>JSON</code> field holding a megabyte of nested data — and the grid would lock up. Each visible cell tokenized and rendered its <strong>full</strong> stringified value into thousands of DOM nodes, even though the cell is clipped to about 300px on screen and the full value is already one click away.</p>
<p><a href="https://github.com/NewtTheWolf">@NewtTheWolf</a> (Dominik Spitzli) fixes it in PR <a href="https://github.com/TabularisDB/tabularis/pull/285">#285</a> (closes <a href="https://github.com/TabularisDB/tabularis/issues/283">#283</a>): a new <code>truncateCellPreview</code> caps the inline preview at 300 characters <em>before</em> tokenization and render in both <code>JsonCell</code> and <code>TextCell</code>, and the native <code>&lt;td&gt;</code> tooltip is capped too. The cap is lossless — the inline expander and the JSON viewer both read the raw row value, not the truncated <code>displayText</code>, so the full content is always reachable. The MySQL and Postgres JSON demos gained a ~1 MB big-JSON row to reproduce the freeze and keep it fixed.</p>
<hr>
<h2>Editor: Theme Isolation and Focus on Open</h2>
<p>Two Monaco fixes from the maintainer.</p>
<p>PR <a href="https://github.com/TabularisDB/tabularis/pull/282">#282</a> (closes <a href="https://github.com/TabularisDB/tabularis/issues/281">#281</a>) stops the editor theme from leaking across instances. Monaco themes are global to the page, so every editor has to agree on the active theme — but each component resolved its own: most used the UI theme, the SQL editor and save-query modal honored the <code>editorTheme</code> override, and the AI explain modal passed a theme id it never registered. Whichever editor mounted last won, so opening the AI explanation modal re-colored every other editor in the app. A new <code>useEditorTheme</code> hook now resolves the effective theme once (the <code>editorTheme</code> override when set, otherwise the UI theme, with a fallback if the override points at a deleted theme), and all eleven Monaco usages route through it.</p>
<p>PR <a href="https://github.com/TabularisDB/tabularis/pull/280">#280</a> (closes <a href="https://github.com/TabularisDB/tabularis/issues/274">#274</a>) focuses the editor when you open a new console tab — via <code>Ctrl</code>/<code>Cmd+T</code>, the <code>+</code> button, or a Quick Navigator action — so you can start typing immediately. Each tab mounts its own editor instance once, keyed by tab id, so a single <code>editor.focus()</code> covers every creation path. The type check on <code>console</code> tabs avoids stealing focus when a table or query-builder tab opens.</p>
<p><video src="https://tabularis.dev/videos/posts/tabularis-ctrl-t.mp4" poster="/videos/posts/tabularis-ctrl-t.jpg" autoplay loop muted playsinline style="width:100%;border-radius:8px;margin:1rem 0"></video></p>
<hr>
<h2>Quieter Keychain, Correct TLS Pools</h2>
<p>Two connection-layer fixes that you feel as fewer interruptions and fewer surprises.</p>
<p>Opening the <strong>AI</strong> settings tab on macOS used to fire the Keychain authorization prompt repeatedly — a single tab open issued eight or more keychain reads, one per provider across <code>SettingsProvider</code>, <code>AiTab</code>, and <code>get_ai_models</code>. <a href="https://github.com/ymadd">@ymadd</a>&#39;s PR <a href="https://github.com/TabularisDB/tabularis/pull/269">#269</a> routes the AI key reads through the <code>CredentialCache</code> that already backs DB and SSH credentials, so the keychain is read at most once per provider per session. As a bonus, <code>get_ai_key</code> now distinguishes a definitive &quot;no entry&quot; from a denied or timed-out prompt, so a transient denial no longer caches a configured key as permanently missing until you restart.</p>
<p><a href="https://github.com/arsis-dev">@arsis-dev</a> (Julien Barbe) follows the v0.13.0 MySQL SSL work with the Postgres equivalent in PR <a href="https://github.com/TabularisDB/tabularis/pull/278">#278</a>: the connection pool key now includes PostgreSQL TLS settings, so editing a connection from one SSL mode to another can no longer silently reuse a pool created under the old mode.</p>
<hr>
<h2>Smaller Things</h2>
<ul>
<li><strong>MiniMax-M3 is the new default</strong> (<a href="https://github.com/octo-patch">@octo-patch</a>, PR <a href="https://github.com/TabularisDB/tabularis/pull/270">#270</a>) — <code>MiniMax-M3</code>, the new flagship model, is added to <code>ai_models.yaml</code> and placed first, so it&#39;s auto-selected when only the MiniMax key is configured. <code>MiniMax-M2.7</code> and <code>MiniMax-M2.7-highspeed</code> stay available for anyone who prefers the previous generation.</li>
</ul>
<p><img src="https://tabularis.dev/img/tabularis-minimax-m3-default.png" alt="Settings → AI with the MiniMax provider selected and the Default Model dropdown open, showing MiniMax-M3 at the top followed by MiniMax-M2.7 and MiniMax-M2.7-highspeed"></p>
<ul>
<li><strong>Big-JSON demo rows</strong> (part of PR <a href="https://github.com/TabularisDB/tabularis/pull/285">#285</a>) — the MySQL and Postgres demo seeds gained ~1 MB JSON rows so the grid freeze stays reproducible and regression-tested.</li>
</ul>
<hr>
<h2>Thanks</h2>
<p>Four external contributors land in v0.13.1 — three returning, one new.</p>
<p><strong><a href="https://github.com/ymadd">@ymadd</a></strong> keeps refining the seams from v0.13.0: the parenthesized-SELECT classifier fix (<a href="https://github.com/TabularisDB/tabularis/pull/272">#272</a>) that removes a false write-prompt without weakening the fail-closed gate, and the AI keychain caching (<a href="https://github.com/TabularisDB/tabularis/pull/269">#269</a>) that finishes wiring the credential cache through the last read path that wasn&#39;t using it.</p>
<p><strong><a href="https://github.com/NewtTheWolf">@NewtTheWolf</a> (Dominik Spitzli)</strong> returns with two correctness fixes: the grid freeze on large JSON cells (<a href="https://github.com/TabularisDB/tabularis/pull/285">#285</a>) and co-authoring the Postgres Explain Plan fix (<a href="https://github.com/TabularisDB/tabularis/pull/279">#279</a>) — the feature that errored on every Postgres connection it ever ran against.</p>
<p><strong><a href="https://github.com/arsis-dev">@arsis-dev</a> (Julien Barbe)</strong> follows his v0.13.0 MySQL SSL and Codex work with the PostgreSQL TLS pool-key fix (<a href="https://github.com/TabularisDB/tabularis/pull/278">#278</a>) — closing the same class of &quot;the pool ignored your TLS setting&quot; bug on the other major engine.</p>
<p><strong><a href="https://github.com/octo-patch">@octo-patch</a></strong> is new to the contributor list and brings the MiniMax-M3 default upgrade (<a href="https://github.com/TabularisDB/tabularis/pull/270">#270</a>). Welcome.</p>
<p>If you run Tabularis on macOS and were tired of the Gatekeeper dance, ever clicked &quot;Explain Plan&quot; on Postgres and got an error, lost rows to a paginated query that ignored your OFFSET, pasted a <code>postgresql://</code> string into a void, watched the grid freeze on a fat JSON column, or got Keychain-prompted on every AI-tab open — this is the upgrade.</p>
<hr>
<p><em>v0.13.1 is available now. Update via the in-app updater, or download from the <a href="https://github.com/TabularisDB/tabularis/releases/tag/v0.13.1">releases page</a>.</em></p>
]]></content:encoded>
      <enclosure url="https://tabularis.dev/blog/v0131-signed-macos-postgres-explain-offset-pagination/opengraph-image.png" type="image/png" />
      <category>release</category>
      <category>bugfix</category>
      <category>macos</category>
      <category>postgres</category>
      <category>mcp</category>
      <category>data-grid</category>
      <category>editor</category>
      <category>ai</category>
      <category>community</category>
    </item>
    <item>
      <title>Tabularis Wins a SourceForge Rising Star Award</title>
      <link>https://tabularis.dev/blog/sourceforge-rising-star-award</link>
      <guid isPermaLink="true">https://tabularis.dev/blog/sourceforge-rising-star-award</guid>
      <pubDate>Fri, 05 Jun 2026 09:51:00 GMT</pubDate>
      <description>SourceForge has recognized Tabularis with a Rising Star award — given to a select group of projects out of more than 500,000 for the downloads and engagement they&apos;ve earned from the community. We&apos;re honored, and it belongs to all of you.</description>
      <content:encoded><![CDATA[<h1>Tabularis Wins a SourceForge Rising Star Award</h1>
<p style="text-align:center;margin:1.5rem 0 2rem;"><img class="no-lightbox" src="https://tabularis.dev/img/posts/sourceforge-rising-star.svg" alt="SourceForge Rising Star award badge for Tabularis" style="width:100%;max-width:320px;height:auto;display:block;margin:0 auto;" /></p>

<p>We&#39;re honored to share that <strong>Tabularis has been recognized with a <a href="https://sourceforge.net/projects/tabularis/">Rising Star award</a> by SourceForge</strong>.</p>
<p>It&#39;s a recognition reserved for a select group of projects out of the more than 500,000 hosted on SourceForge — a platform that sees close to 20 million people a month looking for and building open source software. The award is given for reaching real milestones in downloads and user engagement, which is to say: it&#39;s a reflection of you actually using Tabularis.</p>
<h2>What it means to us</h2>
<p>We don&#39;t take this lightly. For a project that&#39;s only a few months old, being singled out from half a million others is humbling — and it&#39;s not the kind of thing a roadmap or a feature list earns on its own. It&#39;s earned by people who downloaded the app, kept it open, filed an issue, wrote a plugin, translated a string, or simply told a colleague it was worth a look.</p>
<p>So the honest version of &quot;we won an award&quot; is: <strong>you won it for us.</strong> Every star, every release downloaded, every Discord question — that&#39;s the engagement SourceForge measured. We just got to put our name on it.</p>
<h2>Onward</h2>
<p>The badge now lives on our <a href="https://sourceforge.net/projects/tabularis/">SourceForge project page</a>, and you&#39;ll see it pop up across our channels. But the part that matters isn&#39;t the badge — it&#39;s the trajectory it marks. A database client that respects your machine, your credentials, and your time is resonating with people, and that&#39;s exactly the signal we needed to keep building.</p>
<p>Thank you to SourceForge, and thank you to everyone who got us here.</p>
<hr>
<p><em>The Tabularis Team</em></p>
]]></content:encoded>
      <enclosure url="https://tabularis.dev/blog/sourceforge-rising-star-award/opengraph-image.png" type="image/png" />
      <category>community</category>
      <category>award</category>
      <category>open-source</category>
      <category>milestone</category>
    </item>
    <item>
      <title>Your Database GUI Shouldn&apos;t Need an Account</title>
      <link>https://tabularis.dev/blog/your-database-gui-shouldnt-need-an-account</link>
      <guid isPermaLink="true">https://tabularis.dev/blog/your-database-gui-shouldnt-need-an-account</guid>
      <pubDate>Wed, 03 Jun 2026 15:34:00 GMT</pubDate>
      <description>A database client is the most credential-dense application on a developer&apos;s machine. Routing any part of it through a vendor account inverts the trust model — and you usually find out why that matters on the day the vendor pivots.</description>
      <content:encoded><![CDATA[<h1>Your Database GUI Shouldn&#39;t Need an Account</h1>
<p>You download a SQL client because you need to look at a table. You open it, and the first screen is a signup form. The tool that is about to hold your production credentials wants an email address before it will show you a query editor.</p>
<p>This has somehow become normal, and I think it&#39;s worth saying plainly: it shouldn&#39;t be.</p>
<p>A database client is the most credential-dense application on a developer&#39;s machine. It holds connection strings to production, SSH keys, tunnel configs, sometimes the only working path into a customer&#39;s VPC. Editors hold code, which is usually in a repo anyway. Browsers hold sessions you can revoke. A database client holds the keys to the data itself. Of all the tools on your machine, it is the one with the strongest case for never talking to anyone but you and your database.</p>
<p>The trend is going the other way. Accounts, cloud workspaces, query history synced to someone else&#39;s backend, AI features that route your schema through the vendor&#39;s proxy so usage can be metered.</p>
<p>None of this happens because vendors are malicious. It happens because of how these tools are funded. A VC-backed dev tool needs monthly active users, and you can&#39;t count users you can&#39;t see, so you add an account. Collaboration features need a backend, so queries move to the cloud. AI is the monetization story, so it goes through the vendor&#39;s keys instead of yours. Each decision is individually reasonable. The sum is a desktop app whose useful life is coupled to the runway of the company behind it.</p>
<p>And you find out what that coupling costs on the day the company changes course. Arctype was a genuinely good client. It got acquired, and the product was sunset — along with the workspaces where people&#39;s queries lived. That&#39;s not an edge case. That&#39;s the expected lifecycle of an account-based tool: the account is the leash, and eventually somebody pulls it.</p>
<p>Tabularis is built on the opposite bet, so let me be concrete about what &quot;local-first&quot; actually means here, because the term gets thrown around a lot:</p>
<p><strong>No account.</strong> There is no signup, no license activation, no &quot;continue with Google&quot;. You download a binary and connect to a database. That&#39;s the whole onboarding.</p>
<p><video src="https://tabularis.dev/videos/overview.mp4" controls muted playsinline loop autoplay controlsList="nodownload noremoteplayback noplaybackrate" disablePictureInPicture></video></p>
<p><strong>Secrets live in your OS keychain, not in our anything.</strong> Passwords, SSH passphrases, API keys — they go into macOS Keychain, Windows Credential Manager, or libsecret on Linux, under the service name <code>tabularis</code>. You can inspect every entry with <code>security find-generic-password</code> or <code>secret-tool</code> and verify it yourself. If you&#39;d rather a password never persist at all, untick one checkbox and it lives in process memory for the session and dies with it. The details are in the <a href="https://tabularis.dev/wiki/security-credentials">security docs</a>.</p>
<p><strong>Everything non-secret is a plain file.</strong> Connection profiles, SSH configs, preferences, saved queries: JSON on your disk, in a directory you can grep, diff, back up, and put in your dotfiles. The <a href="https://tabularis.dev/wiki/ai-audit-log">AI audit log</a> is one JSON line per query, locally. There is no export feature because there is nothing to export from — you already have the files.</p>
<p><strong>AI is bring-your-own-key, and optional.</strong> If you turn the assistant on, your key goes in the keychain and calls go directly to the provider you chose. Point it at Ollama and nothing leaves your machine at all. We never see your schema, your queries, or your prompts, because there is no &quot;we&quot; in the request path.</p>
<p>In the interest of honesty, here is the complete list of network calls Tabularis makes on its own: the updater checks GitHub releases for a new version. That&#39;s it. No telemetry SDK, no crash reporter phoning home, no anonymous usage pings. You can confirm this the boring way — <a href="https://github.com/TabularisDB/tabularis">the code is open</a>, grep it.</p>
<p>This costs us real things, and I&#39;d rather name them than pretend otherwise. We don&#39;t have sync, so your connections don&#39;t follow you between machines unless you copy the config files yourself. We don&#39;t have shared team workspaces. And because there is no telemetry, I genuinely don&#39;t know how many people use Tabularis or which features they touch — I find out when someone opens an issue, which makes every bug report worth more and every silent user invisible. Those are the terms of the trade, and I think they&#39;re good terms, but they are a trade.</p>
<p>The deeper reason I think this matters goes beyond privacy. A database client is plumbing. Plumbing should be boring, durable, and indifferent to the fortunes of whoever installed it. When your queries are files and your secrets are in the OS keychain, switching away from Tabularis costs you nothing — and that&#39;s exactly the point. A tool you can leave at any moment has to earn its place every day. A tool that holds your account, your history, and your team&#39;s saved queries only has to be too annoying to migrate from.</p>
<p>I know which kind of pressure produces better software.</p>
<p>&quot;Sign in to continue&quot; was a fine default for a SaaS dashboard. For the tool holding your production credentials, it never was one.</p>
]]></content:encoded>
      <enclosure url="https://tabularis.dev/blog/your-database-gui-shouldnt-need-an-account/opengraph-image.png" type="image/png" />
      <category>opinion</category>
      <category>local-first</category>
      <category>security</category>
      <category>privacy</category>
      <category>open-source</category>
    </item>
    <item>
      <title>v0.13.0: Kubernetes Tunnels, a Quick Navigator, and MCP That Reaches Your Plugins</title>
      <link>https://tabularis.dev/blog/v0130-kubernetes-tunnels-quick-navigator-dml-tabs</link>
      <guid isPermaLink="true">https://tabularis.dev/blog/v0130-kubernetes-tunnels-quick-navigator-dml-tabs</guid>
      <pubDate>Wed, 03 Jun 2026 10:00:00 GMT</pubDate>
      <description>v0.13.0 adds first-class Kubernetes port-forward tunnels alongside SSH, a Cmd+P Quick Navigator for jumping to any table in any database, MCP access to plugin-driven connections, a closed multi-statement bypass in the MCP safety gates, Codex as an MCP install target, DML tabs in Generate SQL, a configurable display timezone, self-healing query history, and MySQL SSL modes that are actually honored.</description>
      <content:encoded><![CDATA[<h1>v0.13.0: Kubernetes Tunnels, a Quick Navigator, and MCP That Reaches Your Plugins</h1>
<p><strong>v0.13.0</strong> follows <a href="https://tabularis.dev/blog/v0120-per-connection-appearance-related-records-sql-splitter">v0.12.0</a> with a cycle about <em>reach</em>: reaching a database that lives inside a Kubernetes cluster without <code>kubectl port-forward</code> in a forgotten terminal tab, reaching any table in any schema with two keystrokes, and letting MCP agents reach the connections that run through plugin drivers — while closing the one hole that let a stacked query reach further than it should have.</p>
<p>Six external contributors land in this tag — three of them new — plus a first-time co-author.</p>
<hr>
<h2>Kubernetes Port-Forward Tunnels</h2>
<p>If your database lives inside a Kubernetes cluster, the ritual is familiar: <code>kubectl port-forward svc/postgres 5433:5432</code> in a terminal you must remember to keep open, then a connection in your database client pointed at <code>127.0.0.1:5433</code> that silently breaks the moment that terminal dies.</p>
<p><a href="https://github.com/metalgrid">@metalgrid</a> (Iskren Hadzhinedev) — new to the contributor list — ships PR <a href="https://github.com/TabularisDB/tabularis/pull/246">#246</a>, which makes Kubernetes a <strong>first-class transport option alongside SSH tunnels</strong>. The connection modal grows a <strong>Kubernetes</strong> tab: pick a kubectl context, a namespace, a resource (service or pod), and a container port — each dropdown discovered live from your kubeconfig and cascading into the next.</p>
<p><img src="https://tabularis.dev/img/tabularis-kubernetes-tunnel.png" alt="The Kubernetes tab in the connection modal with cascading dropdowns for context, namespace, resource, and port"></p>
<p>How it works:</p>
<ul>
<li>Tabularis runs <code>kubectl port-forward</code> as a <strong>managed child process</strong>, binds a local ephemeral port, and points the database driver at it — same pattern as the SSH tunnel, no port to pick manually.</li>
<li>Tunnels are <strong>reused</strong> across connections to the same resource (keyed by context/namespace/resource/port), with health checks and lifecycle management.</li>
<li>Saved K8s configurations persist as reusable profiles in <code>k8s_connections.json</code> — the same pattern as SSH profiles — and round-trip through connection Export / Import.</li>
<li>Connections with a tunnel get a blue <strong>K8s badge</strong> on the Connections page and in the sidebar.</li>
<li>K8s and SSH are mutually exclusive on a connection — enabling one disables the other.</li>
</ul>
<p>The only requirements are <code>kubectl</code> in your <code>$PATH</code> and a valid kubeconfig. The PR lands with 18 new Rust tests and 24 new TypeScript tests, and the tunnel expansion is wired through every database command path — including MCP, so an agent can query a cluster-resident database through the same tunnel you use.</p>
<p>Full reference in the wiki: <a href="https://tabularis.dev/wiki/kubernetes-tunneling">Kubernetes Tunneling</a>.</p>
<p>If you&#39;ve been keeping a <code>kubectl port-forward</code> alive in tmux just to browse a staging database, this is the upgrade.</p>
<hr>
<h2>Quick Navigator: <code>Cmd+P</code> for Your Schema</h2>
<p>Every editor since Sublime has had a &quot;jump to anything&quot; key. Your database client now does too. PR <a href="https://github.com/TabularisDB/tabularis/pull/252">#252</a> — co-authored with <strong>lecndu</strong>, taking inspiration from Beekeeper Studio&#39;s Quick Search — adds a <strong>Quick Navigator</strong> overlay on <code>Cmd+P</code> / <code>Ctrl+P</code>:</p>
<p><video src="https://tabularis.dev/videos/wiki/19-quick-navigator.mp4" poster="/videos/wiki/19-quick-navigator.jpg" controls muted playsinline loop autoplay controlsList="nodownload noremoteplayback noplaybackrate" disablePictureInPicture></video></p>
<ul>
<li>Type to filter <strong>tables, views, routines, and triggers</strong> of the active connection.</li>
<li>All databases and schemas configured on the connection are indexed <strong>in the background</strong> when the overlay opens — a multi-database MySQL connection or a multi-schema Postgres one is searched whole, with results grouped under per-database/schema headers.</li>
<li>Hover any result for <strong>quick actions</strong>: Inspect Structure, New Console, Generate SQL, Count Rows, Run Query, and Copy Name — scoped to what makes sense for each object type.</li>
<li>Pick a result and the sidebar <strong>expands and scrolls to the table</strong> — including databases that were collapsed and hadn&#39;t loaded their table list yet.</li>
<li>The shortcut is customizable in <strong>Settings → Keyboard Shortcuts</strong> under Navigation.</li>
</ul>
<p>The follow-up commits are where it got interesting: on a connection with <em>hundreds</em> of tables, selecting a result used to freeze the UI, because every sidebar table item re-rendered on every render. <code>SidebarTableItem</code> is now memoized with a comparator that only re-renders the two items whose active-state actually changed, collapsed databases auto-expand and lazy-load when they become active, and the scroll-into-view retries across animation frames until the asynchronously-loaded item actually exists in the DOM. Large-schema sidebars get faster even if you never press <code>Cmd+P</code>.</p>
<hr>
<h2>MCP: Plugin Drivers, a Closed Bypass, and Codex</h2>
<p>Three PRs this cycle touch the MCP server — two from <a href="https://github.com/ymadd">@ymadd</a>, who keeps pulling on threads until the whole seam is rebuilt.</p>
<h3>Plugin-driven connections now work over MCP</h3>
<p>The MCP server hardcoded dispatch for mysql/postgres/sqlite, so every connection running through a plugin driver — Hacker News, Redis, anything from the registry — failed with <code>Unsupported driver</code>. PR <a href="https://github.com/TabularisDB/tabularis/pull/256">#256</a> (closes <a href="https://github.com/TabularisDB/tabularis/issues/255">#255</a>) routes the schema resource, <code>list_tables</code>, <code>describe_table</code>, <code>run_query</code>, and the pre-flight <code>EXPLAIN</code> through the <strong>shared driver registry</strong> — the same path the GUI uses — and registers built-in plus installed plugin drivers when the <code>--mcp</code> subprocess starts.</p>
<p>Reaching plugins from a headless subprocess surfaced a pile of hardening, all shipped in the same PR: plugin RPC calls are bounded by timeouts so a wedged plugin can&#39;t block the request loop forever, plugin children are killed instead of orphaned when the subprocess exits, plugins claiming a built-in driver id are refused, <code>resources/read</code> resolves through the keychain/SSH-aware path, and the <code>--mcp</code> mode finally gets a logger (stderr only) so plugin-load errors are visible.</p>
<h3>The approval/read-only bypass, closed</h3>
<p>PR <a href="https://github.com/TabularisDB/tabularis/pull/261">#261</a> fixes the kind of bug a safety feature exists to not have. The <a href="https://tabularis.dev/wiki/mcp-readonly-mode">read-only</a> and <a href="https://tabularis.dev/wiki/mcp-approval-gates">approval</a> gates classified a query by its <strong>leading keyword</strong> — so a stacked payload like <code>SELECT 1; DROP TABLE users</code> was tagged as a clean read and sailed past both gates. Separately, an approver could edit an approved <code>SELECT</code> into a <code>DELETE</code> in the approval modal, and the edit was executed without being re-classified.</p>
<p>The classifier now <strong>fails closed on multi-statement payloads</strong>: string literals are stripped under both the SQL-standard (<code>&#39;&#39;</code>) and MySQL backslash-escape (<code>\&#39;</code>) readings, and a <code>;</code> followed by more SQL under <em>either</em> reading trips the gate — so a payload can&#39;t hide a separator by exploiting whichever dialect the classifier doesn&#39;t assume. And the approver-edited query is <strong>re-classified and re-checked against read-only</strong> before execution, with the audit record updated to the effective query.</p>
<p>The execution layer&#39;s prepared-statement protocol already rejected most stacked queries, but the classifier is the documented fail-closed contract — this restores it. If you point agents at anything you care about, update.</p>
<h3>Codex joins the client list</h3>
<p><a href="https://github.com/arsis-dev">@arsis-dev</a> (Julien Barbe) — new to the contributor list — adds <strong>Codex</strong> as an MCP install target in PR <a href="https://github.com/TabularisDB/tabularis/pull/264">#264</a>. The MCP integration page already auto-detects Claude Desktop, Claude Code, Cursor, Windsurf, and Antigravity; Codex now appears alongside them, wired through <code>codex mcp add tabularis -- &lt;tabularis&gt; --mcp</code>, with the Codex-specific manual command shown in the setup UI.</p>
<hr>
<h2>Generate SQL Grows DML Tabs</h2>
<p>The <strong>Generate SQL</strong> tool in the table context menu used to do one thing: show the <code>CREATE TABLE</code> statement. <a href="https://github.com/capvalen">@capvalen</a> (Infocat) — new to the contributor list — extends it in PR <a href="https://github.com/TabularisDB/tabularis/pull/259">#259</a> with a tab per statement kind:</p>
<p><img src="https://tabularis.dev/img/tabularis-generate-sql-dml-tabs.png" alt="The Generate SQL modal with tabs for CREATE TABLE, SELECT *, SELECT fields, UPDATE, and DELETE"></p>
<ul>
<li><strong>SELECT *</strong> and <strong>SELECT [fields]</strong> — ready-made queries against the table.</li>
<li><strong>UPDATE</strong> and <strong>DELETE</strong> — templates with every column laid out.</li>
<li>A <strong>Run in Console</strong> button that opens the generated statement in a new editor tab.</li>
</ul>
<p>The generated <code>UPDATE</code> uses <code>:named</code> bind parameters derived from the column names instead of bare <code>?</code> placeholders, so the statement binds correctly the moment it lands in the query editor. Translations ship for all eight locales in the same PR.</p>
<hr>
<h2>Display Timezone: Timestamps Where You Are</h2>
<p><a href="https://github.com/ymadd">@ymadd</a>&#39;s third PR of the cycle, <a href="https://github.com/TabularisDB/tabularis/pull/251">#251</a> (closes <a href="https://github.com/TabularisDB/tabularis/issues/249">#249</a> and <a href="https://github.com/TabularisDB/tabularis/issues/250">#250</a>), starts as a bug fix and ends as a setting.</p>
<p>The bug: the AI activity log rendered raw UTC timestamps — events table, sessions list, detail modal, and the CSV / JSON / notebook exports all showed a time that wasn&#39;t yours.</p>
<p>The setting: <strong>Settings → Localization → Timezone</strong>, a searchable picker of IANA zones with current UTC-offset labels, defaulting to <strong>Auto</strong> (your OS zone). The selected zone drives every UI timestamp — activity log, query history, favorites — and the backend exports via <code>chrono-tz</code>. Query-history date grouping classifies the today/yesterday/older boundaries in the same zone, so the group headers and the per-row times can never disagree. Even the default export filename derives its date from the configured zone.</p>
<hr>
<h2>Query History That Heals Itself</h2>
<p>PR <a href="https://github.com/TabularisDB/tabularis/pull/253">#253</a> chases a &quot;history doesn&#39;t work anymore&quot; report on macOS down to a file ending in valid JSON followed by 46 bytes of leftover tail — concurrent <code>addEntry</code> calls (one per statement in multi-statement scripts) raced on the file write, and a shorter write over a longer one left trailing garbage. Once corrupt, the parse failure silently short-circuited both reads <em>and</em> writes: the History panel went permanently empty and nothing new was ever recorded.</p>
<p>Three fixes ship together:</p>
<ul>
<li><strong>Self-healing reads</strong> — a corrupt file is renamed aside as <code>&lt;id&gt;.json.corrupt-&lt;timestamp&gt;</code> and history starts fresh, instead of staying mute forever.</li>
<li><strong>Atomic writes</strong> — write to a temp file, then rename onto the target, so a concurrent or crashed write can never leave a half-written file.</li>
<li><strong>Per-connection serialization</strong> — an async mutex orders read-modify-write sequences so concurrent entries can&#39;t lose updates.</li>
</ul>
<p>If a corrupt file was recovered, the History sidebar shows a dismissible banner with the backup path — so you know what happened and where your data went.</p>
<hr>
<h2>Failed Schema Loads Now Say So</h2>
<p>When <code>get_schemas</code> failed — bad search path, permissions, a flaky tunnel — the sidebar rendered <code>TABLES (0)</code> / <code>VIEWS (0)</code> and nothing else. The connection looked open (the connection test runs on a separate code path), so it <em>appeared</em> connected while showing nothing, with no hint anything went wrong.</p>
<p><a href="https://github.com/verbaux">@verbaux</a> (Nikolay Zhuravlev) fixes the silence in PR <a href="https://github.com/TabularisDB/tabularis/pull/242">#242</a>: the sidebar now shows a <strong>&quot;Failed to load schemas&quot;</strong> message with a <strong>Retry</strong> button, a two-line brief of the actual error, and a collapsible <strong>Details</strong> section holding the full raw error with a copy button. For Postgres the brief is the human-readable part of the driver error, with the debug dump tucked into the expanded box. The new strings ship in all eight locales.</p>
<hr>
<h2>MySQL SSL Mode, Actually Honored</h2>
<p><a href="https://github.com/arsis-dev">@arsis-dev</a>&#39;s second PR of the cycle, <a href="https://github.com/TabularisDB/tabularis/pull/263">#263</a>, follows up the original MySQL SSL support from <a href="https://github.com/TabularisDB/tabularis/pull/133">#133</a>. Two related gaps: the test-connection path built its URL without passing the selected <code>ssl_mode</code> to the driver — so a connection configured with <code>ssl_mode=disabled</code> still attempted TLS in the test path — and the connection pool key ignored TLS settings entirely, so editing a connection from one SSL mode to another could silently reuse a cached pool created under the old mode. Both are fixed, with regression tests on the URL builder and the pool key.</p>
<hr>
<h2>Smaller Things</h2>
<ul>
<li><strong>PostgreSQL FK metadata via <code>pg_catalog</code></strong> (<a href="https://github.com/m-tonon">@m-tonon</a>, PR <a href="https://github.com/TabularisDB/tabularis/pull/245">#245</a>) — the <code>information_schema</code> query that loaded foreign-key metadata sometimes missed constraints, and when it did the FK buttons just disappeared from the grid. The query now reads <code>pg_constraint</code> / <code>pg_attribute</code> / <code>pg_class</code> / <code>pg_namespace</code> directly, correctly handling composite keys, cross-schema references, and update/delete rules — with an integration test covering all of it.</li>
<li><strong>Mouse scroll no longer re-renders the selection</strong> (commit <code>12f45865</code>) — scrolling the results with the wheel was churning <code>selectedIndex</code> re-renders for no reason.</li>
<li><strong>Plugin data folder paths corrected</strong> (commit <code>358514ed</code>) — plugin data directories now resolve to the right place.</li>
</ul>
<hr>
<h2>Thanks</h2>
<p>Six external contributors land in v0.13.0 — three new, three returning — plus a first-time co-author.</p>
<p><strong><a href="https://github.com/ymadd">@ymadd</a></strong> ships three PRs this cycle, and they form an arc: the registry dispatch that lets MCP reach plugin drivers (<a href="https://github.com/TabularisDB/tabularis/pull/256">#256</a>), the fail-closed classifier that makes sure that expanded reach stays gated (<a href="https://github.com/TabularisDB/tabularis/pull/261">#261</a>), and the display-timezone work (<a href="https://github.com/TabularisDB/tabularis/pull/251">#251</a>) that turns a timestamp bug into a proper Localization setting. After the SQL splitter in v0.12.0, ymadd has now rebuilt both of the places where Tabularis decides what a piece of SQL <em>is</em> — in the editor and in the safety gates.</p>
<p><strong><a href="https://github.com/metalgrid">@metalgrid</a> (Iskren Hadzhinedev)</strong> is new to the contributor list and lands the Kubernetes tunnel feature (<a href="https://github.com/TabularisDB/tabularis/pull/246">#246</a>) — a genuinely vertical piece of work spanning a new Rust tunnel module, Tauri commands, the connection modal with cascading kubectl discovery, badges, export/import, and 42 new tests. First PR, first-class transport. Welcome.</p>
<p><strong><a href="https://github.com/arsis-dev">@arsis-dev</a> (Julien Barbe)</strong> is also new and ships two precise PRs: the Codex MCP integration (<a href="https://github.com/TabularisDB/tabularis/pull/264">#264</a>) and the MySQL SSL mode fix (<a href="https://github.com/TabularisDB/tabularis/pull/263">#263</a>) — the latter with exactly the kind of issue-archaeology (checking #122, #133, #166, #167, #190 for overlap) that makes a fix easy to merge. Welcome.</p>
<p><strong><a href="https://github.com/capvalen">@capvalen</a> (Infocat)</strong> is new as well, and extends the Generate SQL tool with DML tabs and Run in Console (<a href="https://github.com/TabularisDB/tabularis/pull/259">#259</a>) — including translations for all eight locales. Welcome.</p>
<p><strong><a href="https://github.com/verbaux">@verbaux</a> (Nikolay Zhuravlev)</strong> follows the Russian locale from v0.12.0 with the schema-load error surfacing (<a href="https://github.com/TabularisDB/tabularis/pull/242">#242</a>) — taking a &quot;shows nothing, says nothing&quot; failure and giving it a message, a Retry button, and copyable details, in every locale.</p>
<p><strong><a href="https://github.com/m-tonon">@m-tonon</a> (Matheus Tonon)</strong> returns with the <code>pg_catalog</code> FK metadata fix (<a href="https://github.com/TabularisDB/tabularis/pull/245">#245</a>) — following the Related Records panel from v0.12.0 with the fix that keeps the FK buttons it depends on from vanishing.</p>
<p>And <strong>lecndu</strong> co-authors the Quick Navigator (<a href="https://github.com/TabularisDB/tabularis/pull/252">#252</a>) — a first contribution from inside a pair, which counts.</p>
<p>If you want to reach a database inside a Kubernetes cluster without babysitting a port-forward, jump to any table in any schema with <code>Cmd+P</code>, point an MCP agent at a plugin-driven connection and trust the gates it passes through, generate an UPDATE with named bind params in two clicks, read timestamps in your own timezone, or never again watch your query history go silently mute — this is the upgrade.</p>
<hr>
<p><em>v0.13.0 is available now. Update via the in-app updater, or download from the <a href="https://github.com/TabularisDB/tabularis/releases/tag/v0.13.0">releases page</a>.</em></p>
]]></content:encoded>
      <enclosure url="https://tabularis.dev/blog/v0130-kubernetes-tunnels-quick-navigator-dml-tabs/opengraph-image.png" type="image/png" />
      <category>release</category>
      <category>feature</category>
      <category>security</category>
      <category>mcp</category>
      <category>plugin</category>
      <category>kubernetes</category>
      <category>ui</category>
      <category>ux</category>
      <category>sql</category>
      <category>mysql</category>
      <category>postgres</category>
      <category>community</category>
    </item>
    <item>
      <title>v0.12.0: Per-Connection Appearance, Related Records, and a SQL Splitter We Own</title>
      <link>https://tabularis.dev/blog/v0120-per-connection-appearance-related-records-sql-splitter</link>
      <guid isPermaLink="true">https://tabularis.dev/blog/v0120-per-connection-appearance-related-records-sql-splitter</guid>
      <pubDate>Mon, 25 May 2026 10:00:00 GMT</pubDate>
      <description>v0.12.0 lets you paint each connection with its own accent color and icon, peek at the row behind any foreign key without leaving the grid, ship a first-party SQL splitter with per-driver dialects, make queries feel snappier, fix BIGINT precision on large IDs, align PostgreSQL TLS with libpq, add Russian, and clean up a long tail of editor and grid papercuts.</description>
      <content:encoded><![CDATA[<h1>v0.12.0: Per-Connection Appearance, Related Records, and a SQL Splitter We Own</h1>
<p><strong>v0.12.0</strong> is a broad follow-up to <a href="https://tabularis.dev/blog/v0110-json-viewer-text-diff-triggers-japanese">v0.11.0</a>. Three new external contributors land in this tag — alongside three returning ones — and the cycle leans further into the parts of a database client that you feel every day: telling two MySQL connections apart at a glance, looking at the row a foreign key points at without losing the one you&#39;re on, and a SQL splitter that actually understands what <code>DELIMITER //</code> means in MySQL and what <code>$body$</code> means in PostgreSQL.</p>
<p>If v0.11.0 was about what happens <em>inside</em> a cell — JSON, long text, triggers — v0.12.0 is about what happens <em>around</em> it: the connection, the relationship, the grid, the driver, the language.</p>
<hr>
<h2>Per-Connection Accent Color and Icon</h2>
<p>Issue <a href="https://github.com/TabularisDB/tabularis/issues/189">#189</a> was simple to state: &quot;I have a <code>MySQL local</code> and a <code>MySQL prod</code> sitting one above the other in the sidebar, they share the dolphin and they share the orange, and I have hit Enter on the wrong one.&quot; Up to v0.11.0 every connection rendered with its driver&#39;s default icon and its driver&#39;s default color — Postgres elephant blue, MySQL dolphin orange, SQLite cylinder grey — and there was no way to override either.</p>
<p><a href="https://github.com/NewtTheWolf">@NewtTheWolf</a> shipped the fix in PR <a href="https://github.com/TabularisDB/tabularis/pull/241">#241</a>.</p>
<p><img src="https://tabularis.dev/img/tabularis-per-connection-appearance.png" alt="Two MySQL connections in the Tabularis Connections page side by side — one painted green with a leaf icon ("MySQL local"), one painted red with a shield icon ("MySQL prod") — clearly distinguishable at a glance"></p>
<p>The New Connection modal grows an <strong>Appearance</strong> section at the bottom of the General tab with two pickers:</p>
<ul>
<li><strong>Accent color</strong> — a 12-swatch curated palette plus a custom hex input.</li>
<li><strong>Icon</strong> — four mutually-exclusive tabs:<ul>
<li><strong>Default</strong> keeps the driver&#39;s manifest icon.</li>
<li><strong>Pack</strong> is a curated set of 30 icons (cubes, clouds, layers, shields, leaves, branches…).</li>
<li><strong>Emoji</strong> takes a single emoji of your choice.</li>
<li><strong>Image</strong> uploads a PNG / JPG / WebP / SVG (max 512 KB). Uploads are scanned for the usual SVG nasties (<code>&lt;script&gt;</code>, <code>javascript:</code> URLs, <code>on*=</code> event handlers).</li>
</ul>
</li>
</ul>
<p><video src="https://tabularis.dev/videos/wiki/16-per-connection-appearance.mp4" poster="/videos/wiki/16-per-connection-appearance.jpg" controls muted playsinline loop autoplay controlsList="nodownload noremoteplayback noplaybackrate" disablePictureInPicture></video></p>
<p>When no override is set, the resolvers fall back to the driver&#39;s default — so existing connections keep behaving exactly as before, and you never see a &quot;blank&quot; connection. The override is persisted alongside the rest of the profile in <code>connections.json</code> and round-trips through Export / Import like every other field.</p>
<p>The custom accent and icon are wired into every place a connection appears today: the connection list on the Connections page, the sidebar entry once the connection is open, and the Visual Explain modal&#39;s connection chip. Tab headers and the status bar will pick up the same accent automatically once those components exist.</p>
<p>Full reference in the wiki: <a href="https://tabularis.dev/wiki/connections#per-connection-appearance">Connections → Per-Connection Appearance</a>.</p>
<hr>
<h2>Foreign Keys: Now You Can Peek Without Leaving</h2>
<p>v0.11.0 made foreign keys <em>navigable</em> — hover an FK cell, click the ↗, the referenced row opens in a tab pre-filtered with <code>WHERE ref_col = value</code>. That&#39;s the right pattern when you want to <em>go</em> to the related record. It&#39;s the wrong pattern when you just want to <em>check</em> what <code>user_id = 123</code> resolves to before continuing to edit the row you&#39;re already on.</p>
<p><a href="https://github.com/m-tonon">@m-tonon</a> — new to the contributor list — closes <a href="https://github.com/TabularisDB/tabularis/issues/228">#228</a> with PR <a href="https://github.com/TabularisDB/tabularis/pull/230">#230</a> by adding the second affordance: an inline <strong>Related Records Panel</strong> that slides up from the bottom of the data grid.</p>
<p><video src="https://tabularis.dev/videos/wiki/17-related-records-panel.mp4" poster="/videos/wiki/17-related-records-panel.jpg" controls muted playsinline loop autoplay controlsList="nodownload noremoteplayback noplaybackrate" disablePictureInPicture></video></p>
<p>Click any FK value (or pick <strong>Show related record</strong> from the cell context menu) and the panel opens <em>below</em> the current grid, keeping the parent table visible and interactive above it. Clicking a different FK in the parent grid swaps the panel content in place — no close-then-reopen. The panel is <strong>drag-resizable</strong>, so a wide referenced row can claim the height it needs.</p>
<p>If you decide you do want to navigate after all, the panel has an <strong>Open in tab</strong> button that hands off to the existing FK navigation path — same WHERE clause, same tab-reuse behavior.</p>
<p>V1 keeps the same scope boundary as the navigation pattern: single-column foreign keys only. Composite constraints and cross-schema navigation are noted as follow-ups.</p>
<p>If you live in tables with FK-heavy schemas — orders → users → addresses, line items → products → categories — and you&#39;ve been tab-hopping just to <em>look</em> at something, this is the upgrade.</p>
<p>Full reference in the wiki: <a href="https://tabularis.dev/wiki/data-grid#related-records-panel">Data Grid → Related Records Panel</a>.</p>
<hr>
<h2>A SQL Splitter We Actually Own</h2>
<p>The statement splitter is the bit of plumbing between &quot;what&#39;s in the editor&quot; and &quot;what gets sent to the database&quot;. Up to v0.11.0 we delegated it to an external library, which has served well enough — but the cycle landed two reports that pointed at the same root cause. <a href="https://github.com/TabularisDB/tabularis/issues/223">#223</a> was the visible one: a SELECT preceded by a <code>-- header comment</code> block showed up in the run-selection dropdown as <em>multiple</em> entries — the comment lines and the SELECT each got their own row, even though only the SELECT was executable.</p>
<p><a href="https://github.com/ymadd">@ymadd</a> replaces the lot with a first-party splitter in PR <a href="https://github.com/TabularisDB/tabularis/pull/225">#225</a>. What it handles natively:</p>
<ul>
<li><strong>String literals</strong> — <code>&#39;...&#39;</code>, <code>\&#39;</code> escapes, <code>E&#39;...&#39;</code> extended, dollar-quoted PostgreSQL strings (<code>$tag$...$tag$</code>), MySQL backticks, MSSQL bracket identifiers.</li>
<li><strong>Comments</strong> — <code>--</code> line, <code>/* */</code> block (with PostgreSQL nested-comment support), and the MySQL <strong>conditional <code>/*! ... */</code></strong> form — which is emitted as <em>its own statement</em> so <code>mysqldump</code> output (with version-gated <code>SET</code> statements wrapped in conditional comments) executes correctly when pasted into the editor.</li>
<li><strong>Delimiters</strong> — <code>;</code>, the <code>DELIMITER</code> directive for MySQL stored routines, and <code>GO</code> for MSSQL.</li>
</ul>
<p>A new per-driver dialect field flows from the connection straight into every run-query, explain, and dropdown path in the editor, so MySQL backticks, MSSQL <code>[...]</code>, and PostgreSQL dollar-quoting are each parsed by the rules the driver actually uses.</p>
<p>The comment-fold behavior also changes what you see in the dropdown: a header <code>-- block</code> followed by a <code>SELECT</code> is now a <em>single</em> entry, and a trailing comment after a statement folds back into that statement. The dropdown only shows runnable statements.</p>
<p>Oracle&#39;s <code>/</code> block terminator, Firebird PSQL <code>BEGIN...END</code>, and MSSQL&#39;s adaptive <code>GO</code> split are explicitly out of scope for v1 and noted as follow-ups.</p>
<p>This is the kind of work — invisible until you look at the dropdown — that you only realize was sitting under everything else once it&#39;s gone.</p>
<hr>
<h2>Snappier Queries, Right Out of the Gate</h2>
<p><a href="https://github.com/thomaswasle">@thomaswasle</a> ships PR <a href="https://github.com/TabularisDB/tabularis/pull/216">#216</a>, which is the kind of fix that doesn&#39;t show up in a screenshot but shows up in your hands.</p>
<p>Two independent issues were adding latency to every single query execution. First, every query was re-reading the connections file from disk to look up which database it was talking to — fast, but it adds up over a working day, and over a remote keychain on a laptop you can feel it. The new connection cache reads the file once on first use and serves every subsequent lookup from memory. Any time you save or edit a connection, the cache is dropped so the next read picks up the change. There&#39;s no behavior to learn — queries just feel less laggy.</p>
<p>Second, the result grid was waiting on column and foreign-key metadata before showing the rows. After each query returned, the grid stayed blank until two extra metadata round-trips finished — sometimes adding 100–500 ms of perceived latency on top of a query that was actually already done. The result now renders the moment the data arrives; metadata loads in the background and the FK indicators light up a beat later.</p>
<p>If you&#39;ve ever run a fast SELECT and watched the grid sit blank for half a second, this is the upgrade.</p>
<p>The same cycle also ships PR <a href="https://github.com/TabularisDB/tabularis/pull/239">#239</a> — the sidebar now refreshes after <code>CREATE TABLE</code>, so a freshly created table actually shows up where you expect it without a manual refresh.</p>
<hr>
<h2>BIGINT Precision: Stop Losing Snowflake IDs</h2>
<p><a href="https://github.com/NewtTheWolf">@NewtTheWolf</a> closes <a href="https://github.com/TabularisDB/tabularis/issues/210">#210</a> with PR <a href="https://github.com/TabularisDB/tabularis/pull/220">#220</a>, and it&#39;s the kind of fix that&#39;s only obvious in retrospect.</p>
<p>BIGINT values bigger than about 9 quadrillion — which includes every snowflake ID, every Discord ID, and most Twitter/X IDs — were silently losing their last digits on read. So <code>844197938335842304</code> came back from the database as <code>844197938335842300</code>. The grid then sent that rounded value back on UPDATE / DELETE, which either matched the wrong row or matched nothing at all.</p>
<p>The fix preserves the exact value end-to-end on read <em>and</em> on write-back. Wired through:</p>
<ul>
<li><strong>MySQL</strong> — <code>BIGINT</code> and <code>BIGINT UNSIGNED</code>.</li>
<li><strong>PostgreSQL</strong> — <code>BIGINT</code> (<code>INT8</code>), <code>XID8</code>, and <code>MONEY</code>.</li>
<li><strong>SQLite</strong> — <code>INTEGER</code>.</li>
</ul>
<p>Sort and filter are unaffected because both run server-side against the native BIGINT column. Small IDs (anything that fits in JavaScript&#39;s safe range) are handled exactly as before — no change in the common case, no impact on row counts.</p>
<p>The same PR adds a <code>bigint_demo</code> seed table to the MySQL and Postgres init scripts in the Docker Compose demo, mirroring the <code>text_demo</code> / <code>json_demo</code> pattern from v0.11.0. Point Tabularis at the demo and you have something to reproduce the original bug against (before this fix) and confirm it&#39;s gone (after).</p>
<p>If you&#39;ve been editing rows in a Discord-style table and watching the wrong ones change, this is the upgrade.</p>
<hr>
<h2>PostgreSQL TLS Modes, Aligned with libpq</h2>
<p><a href="https://github.com/TabularisDB/tabularis/issues/209">#209</a> was a precise report: Tabularis&#39; PostgreSQL SSL modes (<code>disable</code>, <code>allow</code>, <code>prefer</code>, <code>require</code>) all behaved like they demanded a valid CA — which broke connection to AWS RDS instances with self-signed certs that work fine in <code>psql</code> and DBeaver with <code>sslmode=require</code>. The expected behavior, the one every other Postgres client ships, is:</p>
<ul>
<li><code>require</code> — force encryption, but <strong>do not</strong> require certificate validation.</li>
<li><code>verify-ca</code> — encryption plus validate the CA.</li>
<li><code>verify-full</code> — encryption plus validate the CA plus verify the hostname.</li>
</ul>
<p><a href="https://github.com/VincentZhangy">@VincentZhangy</a> — new to the contributor list — lands the alignment in PR <a href="https://github.com/TabularisDB/tabularis/pull/211">#211</a>.</p>
<p><img src="https://tabularis.dev/img/tabularis-postgresql-ssl-modes.png" alt="The SSL Mode dropdown in the PostgreSQL connection modal expanded, showing the full progression: disable / allow / prefer / require / verify-ca / verify-full"></p>
<p><code>require</code> no longer demands a CA. The clear security progression — <code>require</code> → <code>verify-ca</code> → <code>verify-full</code> — that other PostgreSQL clients ship is now what Tabularis ships too.</p>
<p>If you&#39;ve been pointing Tabularis at RDS with a self-signed cert and bouncing off &quot;needs CA certificate&quot;, this is the upgrade.</p>
<p>Full reference in the wiki: <a href="https://tabularis.dev/wiki/connections#tls--ca-certificates-postgresql">Connections → TLS &amp; CA Certificates</a>.</p>
<hr>
<h2>Delete Rows with the Delete or Backspace Key</h2>
<p><a href="https://github.com/thomaswasle">@thomaswasle</a> closes <a href="https://github.com/TabularisDB/tabularis/issues/218">#218</a> with PR <a href="https://github.com/TabularisDB/tabularis/pull/221">#221</a>. Pressing <code>Delete</code> or <code>Backspace</code> with one or more rows selected now marks them for deletion — the same behavior already available from the right-click context menu, just reachable from the keyboard.</p>
<p><video src="https://tabularis.dev/videos/wiki/18-delete-row-shortcut.mp4" poster="/videos/wiki/18-delete-row-shortcut.jpg" controls muted playsinline loop autoplay controlsList="nodownload noremoteplayback noplaybackrate" disablePictureInPicture></video></p>
<p>The shortcut fires only when rows are selected, no cell is being edited, and the grid is not read-only — so <code>Backspace</code> inside an active cell editor still does what <code>Backspace</code> should do.</p>
<hr>
<h2>Русский</h2>
<p>PR <a href="https://github.com/TabularisDB/tabularis/pull/229">#229</a> from <a href="https://github.com/verbaux">@verbaux</a> — new to the contributor list — adds <strong>Russian</strong> as the eighth supported UI language. <strong>Русский</strong> is registered in the language picker and surfaces in <strong>Settings → Appearance → Language</strong>.</p>
<p>The locale list is now <strong>English, Italian, Spanish, French, German, Chinese, Japanese, and Russian</strong>.</p>
<p>The same PR also fixes a long-standing pluralization bug in the tab switcher. The English UI used to render &quot;1 tabs&quot; for a single tab because the count was concatenated outside the translation call. Counts are now passed <em>into</em> the translation, with proper plural forms per language — including Russian&#39;s four CLDR forms (1 → вкладка, 2–4 → вкладки, 5–20 → вкладок).</p>
<p>A handful of UI surfaces remain not-yet-wired-to-i18n and render in English: the Visual Query Builder canvas, the AI Query modal, and the mini result grid. All noted in the PR; an opportunity for a follow-up contribution.</p>
<hr>
<h2>A New macOS Dock Icon</h2>
<img src="https://tabularis.dev/img/tabularis-macos-dock-icon.png" alt="The new Tabularis macOS dock icon — an Apple squircle with a light glass background, subtle teal and violet auroras, and the isometric cube logo centered" style="width: 160px; float: right; margin: 0.25rem 0 1rem 1.5rem; border: none; box-shadow: none; border-radius: 0;" />

<p>The old macOS dock icon was a bare isometric cube on a transparent background. On modern macOS (Tahoe and friends) that looked out of place next to system apps that all sit inside a proper squircle — the cube floated, had no glass treatment, and on light wallpapers the dark edges fought the dock.</p>
<p>PR <a href="https://github.com/TabularisDB/tabularis/pull/217">#217</a> replaces <code>icon.icns</code> with a Tahoe-style design: a proper Apple squircle, a light glass background with a top sheen, very subtle teal and violet auroras in opposite corners picking up the cube&#39;s own gradient colors, and the cube logo centered with a soft drop shadow.</p>
<p>Windows <code>.ico</code> and Linux PNGs are untouched; iOS / Android folders unchanged.</p>
<div style="clear: both;"></div>

<hr>
<h2>Smaller Things</h2>
<p>A long tail of papercuts gets cleaned up in this cycle:</p>
<ul>
<li><strong><code>Ctrl+Enter</code> runs the active tab, not the last opened one</strong> (<a href="https://github.com/thomaswasle">@thomaswasle</a>, PR <a href="https://github.com/TabularisDB/tabularis/pull/240">#240</a>) — with multiple Console tabs open, <code>Ctrl+Enter</code> used to always execute the query in whichever tab was opened <em>most recently</em>, regardless of which one was actually focused. It now fires the query in the active tab, as it always should have.</li>
<li><strong>Pagination works on SELECTs with leading SQL comments</strong> (<a href="https://github.com/ymadd">@ymadd</a>, PR <a href="https://github.com/TabularisDB/tabularis/pull/213">#213</a>) — a SELECT preceded by a <code>-- header</code> block silently lost its pagination bar, so 500-row results looked like a fixed slice with no way to advance.</li>
<li><strong>PostgreSQL filters honor case-sensitive column names</strong> (<a href="https://github.com/m-tonon">@m-tonon</a>, PR <a href="https://github.com/TabularisDB/tabularis/pull/232">#232</a>) — a column named <code>userId</code> was getting lowercased to <code>userid</code> by the filter bar, and the filter silently matched nothing. PostgreSQL columns now get properly quoted; MySQL/SQLite paths are unchanged.</li>
<li><strong>Save Query modal doesn&#39;t override the editor theme</strong> (<a href="https://github.com/debba">@debba</a>, PR <a href="https://github.com/TabularisDB/tabularis/pull/248">#248</a>) — opening the Save Query modal silently swapped the theme of <em>every</em> SQL editor in the app to the UI theme. Only visible if you&#39;d picked a different editor theme in Settings → Appearance, which is exactly the configuration that setting exists for.</li>
<li><strong>Settings toggle knob centered</strong> (<a href="https://github.com/verbaux">@verbaux</a>, PR <a href="https://github.com/TabularisDB/tabularis/pull/219">#219</a>) — the knob in the Settings toggles was landing slightly below the center of the track on macOS. Same size, colors, and keyboard behavior, just visually correct now.</li>
<li><strong>CI: manual prereleases from fork PRs</strong> (<a href="https://github.com/NewtTheWolf">@NewtTheWolf</a>, PR <a href="https://github.com/TabularisDB/tabularis/pull/206">#206</a>) — the release workflow now accepts a PR number, tag name, and prerelease flag as manual inputs, so a maintainer can build a prerelease directly from a fork PR without merging it. Tags containing a <code>-</code> automatically flag the release as prerelease, and AUR / Snap / Winget downstream workflows skip prereleases so beta channels stay out of system package managers.</li>
</ul>
<hr>
<h2>Thanks</h2>
<p>Six external contributors land in v0.12.0. Three of them are new — Matheus, Nikolay, and vlor — and three continue the streak from v0.11.0.</p>
<p><strong><a href="https://github.com/NewtTheWolf">@NewtTheWolf</a> (Dominik Spitzli)</strong> ships three PRs this cycle: the per-connection appearance feature (<a href="https://github.com/TabularisDB/tabularis/pull/241">#241</a>) — a real piece of vertical work spanning the modal, the four-tab icon picker, the upload pipeline with SVG sanitization, and the wiring into every place a connection appears — the BIGINT precision fix (<a href="https://github.com/TabularisDB/tabularis/pull/220">#220</a>) that catches a class of silent corruption nobody had named yet but everyone had hit, and the CI workflow change (<a href="https://github.com/TabularisDB/tabularis/pull/206">#206</a>) that makes building prereleases from fork PRs a one-click maintainer action. Three substantial PRs in one tag, all merged without changes.</p>
<p><strong><a href="https://github.com/thomaswasle">@thomaswasle</a> (Thomas Müller-Wasle)</strong> also ships three: the per-query latency fix (<a href="https://github.com/TabularisDB/tabularis/pull/216">#216</a>) which lifts a real ms-level cost out of every command and unblocks the grid from waiting on metadata, the <code>Delete</code> / <code>Backspace</code> keyboard shortcut for row deletion (<a href="https://github.com/TabularisDB/tabularis/pull/221">#221</a>), and the <code>Ctrl+Enter</code> active-tab fix (<a href="https://github.com/TabularisDB/tabularis/pull/240">#240</a>) — a precisely-traced bug through how editor commands get bound. Thomas has now shipped triggers in v0.11.0, SQL INSERT export + cell selection in v0.10.2, and three more in v0.12.0; the diagnoses keep getting sharper.</p>
<p><strong><a href="https://github.com/ymadd">@ymadd</a></strong> lands the first-party SQL splitter (<a href="https://github.com/TabularisDB/tabularis/pull/225">#225</a>) — a properly substantial replacement of the parsing layer with per-driver dialect support — and the leading-comment pagination fix (<a href="https://github.com/TabularisDB/tabularis/pull/213">#213</a>). Together with the multi-statement batch fix in v0.11.0, ymadd has now rewritten large parts of how Tabularis decides &quot;is this one statement or many&quot; — twice over.</p>
<p><strong><a href="https://github.com/m-tonon">@m-tonon</a> (Matheus Tonon)</strong> is new to the contributor list and lands two PRs the same cycle. The Related Records Panel (<a href="https://github.com/TabularisDB/tabularis/pull/230">#230</a>) is the kind of feature you only build if you&#39;ve used the tool enough to know that &quot;navigate to it&quot; and &quot;look at it&quot; are different verbs; the Postgres case-sensitive filter fix (<a href="https://github.com/TabularisDB/tabularis/pull/232">#232</a>) is the kind of bug you only diagnose if you&#39;ve actually been hit by it on your own database. Welcome.</p>
<p><strong><a href="https://github.com/verbaux">@verbaux</a> (Nikolay Zhuravlev)</strong> is also new, and ships the Russian translation (<a href="https://github.com/TabularisDB/tabularis/pull/229">#229</a>) — full parity with the English locale, a Russian README, and a properly thorough fix to the tab-counter pluralization that was rendering &quot;1 tabs&quot; in English. The same PR also notices the SettingToggle knob being off-center on macOS and fixes it in <a href="https://github.com/TabularisDB/tabularis/pull/219">#219</a> — exactly the kind of cross-cutting &quot;while I&#39;m here&quot; attention that turns a translation PR into something more.</p>
<p><strong><a href="https://github.com/VincentZhangy">@VincentZhangy</a> (vlor)</strong> rounds out the contributor list with PR <a href="https://github.com/TabularisDB/tabularis/pull/211">#211</a>, aligning the PostgreSQL SSL modes with the behavior <code>psql</code> and DBeaver users already expect.</p>
<p>If you want to tell two MySQL connections apart at a glance, peek at the row behind a foreign key without leaving the one you&#39;re on, paste a <code>mysqldump</code> output and see one statement per <code>/*! ... */</code> block, run a query and see the grid the instant the data arrives, edit by a snowflake ID without losing the last three digits, connect to RDS with <code>sslmode=require</code> and have it work, hit <code>Delete</code> on a selected row, or read the UI in Русский — this is the upgrade.</p>
<hr>
<p><em>v0.12.0 is available now. Update via the in-app updater, or download from the <a href="https://github.com/TabularisDB/tabularis/releases/tag/v0.12.0">releases page</a>.</em></p>
]]></content:encoded>
      <enclosure url="https://tabularis.dev/blog/v0120-per-connection-appearance-related-records-sql-splitter/opengraph-image.png" type="image/png" />
      <category>release</category>
      <category>feature</category>
      <category>ui</category>
      <category>ux</category>
      <category>data-grid</category>
      <category>sql</category>
      <category>drivers</category>
      <category>postgres</category>
      <category>mysql</category>
      <category>i18n</category>
      <category>community</category>
    </item>
    <item>
      <title>Tabularis Is Now Backed by DigitalOcean</title>
      <link>https://tabularis.dev/blog/digitalocean-opensource-sponsorship</link>
      <guid isPermaLink="true">https://tabularis.dev/blog/digitalocean-opensource-sponsorship</guid>
      <pubDate>Wed, 20 May 2026 11:00:00 GMT</pubDate>
      <description>DigitalOcean has welcomed Tabularis into its Open Source Credits Program. A milestone for the project, a vote of confidence from one of the cloud providers that built its name on supporting developers, and a real boost for what comes next.</description>
      <content:encoded><![CDATA[<h1>Tabularis Is Now Backed by DigitalOcean</h1>
<p style="text-align:center;margin:1.5rem 0 2rem;"><img class="no-lightbox" src="https://tabularis.dev/img/posts/digitalocean-partnership.png" alt="Tabularis is now part of the DigitalOcean Open Source Credits Program" style="width:100%;max-width:800px;height:auto;display:block;margin:0 auto;" /></p>

<p>We have some news we&#39;re genuinely excited to share: <strong><a href="https://m.do.co/c/f6ab3d158275">DigitalOcean</a> has welcomed Tabularis into its Open Source Credits Program</strong>.</p>
<p>For a project that started four months ago as one person&#39;s late-night frustration with database tooling, having a cloud provider of DigitalOcean&#39;s stature put their name behind us is more than a sponsorship line on a website. It&#39;s a signal — to the contributors who&#39;ve shown up, to the users who&#39;ve trusted Tabularis with their workflows, and to anyone still figuring out whether this project is worth their time — that what we&#39;re building here matters.</p>
<h2>Why this partnership feels right</h2>
<p>DigitalOcean built its reputation by betting on developers before it was obvious. Simple pricing, documentation people actually wanted to read, a community-first posture in a market that mostly didn&#39;t have one. The kind of company that, if you&#39;ve ever shipped a side project to a server you paid for yourself, you&#39;ve probably already met.</p>
<p>Tabularis was built in the same spirit. A tool by developers, for developers. Free, open, and stubbornly independent. The fit isn&#39;t an accident.</p>
<p>What the Open Source Credits Program does, in plain terms: DigitalOcean provides yearly cloud credits to open source projects whose work they want to see continue. No equity, no contracts, no pressure to pivot the roadmap. Just resources, in the form of the same infrastructure their paying customers use, given to maintainers who would otherwise be funding it out of pocket.</p>
<p>For a community-driven project still funded out of pocket, that math changes things.</p>
<h2>What this unlocks for Tabularis</h2>
<p>The credits are earmarked for the infrastructure behind the <strong>upcoming Tabularis plugin registry</strong> — the next step for the ecosystem the community has been building one plugin at a time.</p>
<p>Without going into the technical weeds (there&#39;s a <a href="https://tabularis.dev/roadmap/plugin-registry">full roadmap page</a> for anyone who wants the details), the registry is the piece that lets plugin authors publish on their own, lets users see what&#39;s actually being used, and lets the ecosystem grow without a maintainer sitting in the middle of every release.</p>
<p>It&#39;s the difference between &quot;a list of plugins I review by hand&quot; and &quot;an actual platform other people can build on.&quot; That&#39;s a step Tabularis needs to take, and the DigitalOcean credits make it possible to take it properly, not as a side project squeezed in between bug fixes.</p>
<h2>A thank you, and what comes next</h2>
<p>To the team at DigitalOcean and the people running the Open Source Credits Program: thank you. Genuinely. Backing a four-month-old open source project takes a kind of long-term thinking that&#39;s rare, and we don&#39;t take it lightly.</p>
<p>To the Tabularis community: this partnership belongs to you too. Every star, every PR, every plugin, every translation, every Discord question answered — all of it is what made Tabularis the kind of project a program like this wanted to back. We&#39;re not where we are by accident.</p>
<p>We&#39;ll be telling this story across our channels in the coming days — if you want to help amplify it, you can find us tagging <strong>@digitalocean</strong> and using <strong>#DOforOpenSource</strong>.</p>
<p>Four months ago, Tabularis was a single binary one person pushed to GitHub at midnight. Today it&#39;s a community, a growing plugin ecosystem, and a project DigitalOcean wants to put their name behind. None of that draws a straight line on a chart — every star, every PR, every Discord thread, every link shared in a Slack channel pulled the curve upward. This partnership is one of the moments where that work becomes visible.</p>
<p>The road ahead is the longest part. We&#39;re glad you&#39;re walking it with us.</p>
<hr>
<p><em>The Tabularis Team</em></p>
]]></content:encoded>
      <enclosure url="https://tabularis.dev/blog/digitalocean-opensource-sponsorship/opengraph-image.png" type="image/png" />
      <category>community</category>
      <category>sponsors</category>
      <category>partnership</category>
      <category>open-source</category>
    </item>
    <item>
      <title>v0.11.0: A Real Editor Inside Every Cell, Triggers, and 日本語</title>
      <link>https://tabularis.dev/blog/v0110-json-viewer-text-diff-triggers-japanese</link>
      <guid isPermaLink="true">https://tabularis.dev/blog/v0110-json-viewer-text-diff-triggers-japanese</guid>
      <pubDate>Mon, 18 May 2026 10:00:00 GMT</pubDate>
      <description>v0.11.0 puts a Monaco-grade editor with diff inside JSON, JSONB and long text cells (in a dedicated Tauri window, even), adds a full trigger manager for PostgreSQL/MySQL/SQLite, makes foreign keys click-to-navigate, ships a Japanese translation, and lets multi-statement scripts share a real database session.</description>
      <content:encoded><![CDATA[<h1>v0.11.0: A Real Editor Inside Every Cell, Triggers, and 日本語</h1>
<p><strong>v0.11.0</strong> is the fattest tag since <a href="https://tabularis.dev/blog/v0100-ai-safety-audit-approval">v0.10.0</a> — and it&#39;s almost entirely external work. Four community contributors land in this cycle (two of them new), shipping the JSON/JSONB viewer that has been a top request since #24, a trigger manager that lights up the third major database object after tables and routines, a Japanese translation, and a reliability fix to the driver layer that you only notice the day it isn&#39;t there.</p>
<p>If v0.10.x was about getting connections to behave, v0.11.0 is about what happens once you&#39;re inside.</p>
<hr>
<h2>JSON / JSONB Cells, Now With a Real Editor</h2>
<p>The most-requested data-grid issue since the project started (<a href="https://github.com/TabularisDB/tabularis/issues/24">#24</a>) was simple to state and unpleasant to live without: &quot;Let me look at this JSONB column.&quot; Up to v0.10.3 a <code>jsonb</code> cell rendered as one long string of escaped braces, and editing it meant typing valid JSON into a textarea that did nothing to help.</p>
<p><a href="https://github.com/NewtTheWolf">@NewtTheWolf</a> shipped the fix in PR <a href="https://github.com/TabularisDB/tabularis/pull/181">#181</a> — and it&#39;s the kind of feature you can tell was reverse-engineered from how DBeaver&#39;s Value Panel and DataGrip&#39;s Value Editor actually feel to use, not just what they look like.</p>
<p><video src="https://tabularis.dev/videos/wiki/13-json-viewer.mp4" controls muted playsinline loop autoplay controlsList="nodownload noremoteplayback noplaybackrate" disablePictureInPicture></video></p>
<p>A JSON / JSONB cell now gets three affordances:</p>
<ul>
<li>A <strong>chevron</strong> on the row that expands an inline editor pane below it — Monaco running in JSON mode with syntax highlighting, bracket matching, and a manual <strong>Diff toggle</strong> that compares the original cell value against the pending edit (unified by default, with a one-click switch to side-by-side).</li>
<li>A <strong>braces icon</strong> that opens the cell in a <strong>standalone Tauri window</strong> dedicated to the value. Multiple cells can have their viewers open at the same time — each window keeps its own session and remembers its bounds — so comparing two rows is now &quot;click, click&quot; instead of &quot;copy, paste into a different tab, come back, repeat&quot;.</li>
<li>A <strong>double-click</strong> that opens the viewer window directly in edit mode, for when the chevron isn&#39;t where your hand wants to go.</li>
</ul>
<p>Edits round-trip through the grid&#39;s pending-changes machinery rather than going straight to the database — you can review the unified diff, decide you don&#39;t like it, close the viewer, hit Submit on the row when you do. Two viewer windows open on the same connection don&#39;t interfere with each other; session state lives in a Rust <code>Mutex&lt;HashMap&gt;</code> keyed by ULID, and saves flow back to the grid via a Tauri event.</p>
<p>On the PostgreSQL side the driver finally <strong>binds <code>json</code> / <code>jsonb</code> natively</strong> through <code>tokio-postgres</code>&#39; <code>with-serde_json-1</code> impl. INSERTs and UPDATEs of object/array values that used to round-trip through a string cast now go straight through as typed parameters; the same code path is used by inline edits, the viewer save, and the row editor sidebar. Scalar JSON values (a bare <code>42</code>, <code>&quot;hello&quot;</code>, <code>true</code>) are JSON-encoded before binding, so you can&#39;t accidentally feed Postgres a malformed payload from the grid.</p>
<p>The same PR also lands a <strong>per-connection toggle</strong> that scans plain text columns for JSON-shaped content and routes them through the same cell renderer. It&#39;s per-connection on purpose — you almost always want it on for your audit-log database and off for the one where TEXT means &quot;free-form prose&quot;.</p>
<p><video src="https://tabularis.dev/videos/wiki/14-long-text-cells.mp4" controls muted playsinline loop autoplay controlsList="nodownload noremoteplayback noplaybackrate" disablePictureInPicture></video></p>
<p>Full reference in the wiki: <a href="https://tabularis.dev/wiki/data-grid#json--long-text-cells">Data Grid → JSON &amp; long text cells</a>.</p>
<hr>
<h2>Long Text and <code>LONGTEXT</code>, Same Treatment</h2>
<p>The week after #181 merged, <a href="https://github.com/NewtTheWolf">@NewtTheWolf</a> extended the same chevron + Monaco + diff pattern to plain string columns in PR <a href="https://github.com/TabularisDB/tabularis/pull/208">#208</a>, closing <a href="https://github.com/TabularisDB/tabularis/issues/207">#207</a>.</p>
<p>Any text-like column whose value is longer than 80 characters or contains a newline — <code>TEXT</code>, <code>LONGTEXT</code>, <code>VARCHAR(500)</code>, <code>VARCHAR(MAX)</code> — now renders with the same chevron. Expand the row and you get Monaco in <code>plaintext</code> mode, with the same Diff and Side-by-side toggles. Markdown articles, code snippets, SQL queries you stored as text, multi-paragraph notes: all of it readable inline, all of it diffable before commit. The row-editor sidebar in the right-hand panel got the same upgrade — a long field there now opens in a Monaco-backed input instead of a textarea, with the diff toggle right there, and the editor pane itself is <strong>drag-resizable</strong> so you can give a long markdown article the height it deserves.</p>
<p>There is no separate viewer window for plain text — by design. Text cells aren&#39;t compared as often as JSON, and the chevron + inline expansion was the entry point that mattered. JSON cells keep both the chevron and the dedicated-window path.</p>
<hr>
<h2>Trigger Management</h2>
<p>Stored procedures and functions have been browsable for a while. Triggers — the third major database object you reach for in real schemas — were the visible gap. PR <a href="https://github.com/TabularisDB/tabularis/pull/183">#183</a> from <a href="https://github.com/thomaswasle">@thomaswasle</a> closes it across all three built-in drivers.</p>
<p>The Explorer sidebar grows a <strong>Triggers</strong> accordion under every schema (PostgreSQL), database (MySQL multi-db), and in the flat layout for MySQL single-db / SQLite. Each entry shows the trigger name, a timing/event badge (<code>BEFORE INSERT</code>, <code>AFTER UPDATE</code>, <code>INSTEAD OF DELETE</code>…), and a tooltip with the target table. There&#39;s a filter field at the top of the accordion, matching the existing table-filter pattern.</p>
<p><img src="https://tabularis.dev/img/tabularis-triggers-sidebar.png" alt="Triggers accordion in the Tabularis Explorer sidebar listing eight MySQL triggers with BEFORE / AFTER and INSERT / UPDATE / DELETE badges, alongside a read-only View Definition tab on the right"></p>
<p>Right-click a trigger for the actions you&#39;d expect:</p>
<ul>
<li><strong>View Definition</strong> — opens the trigger SQL in a read-only editor tab (Run and Explain Plan are hidden, since you&#39;re looking at DDL).</li>
<li><strong>Edit</strong> — opens the <strong>Trigger Editor Modal</strong> in <em>Guided mode</em>: name, table, timing (BEFORE / AFTER / INSTEAD OF), event checkboxes (INSERT / UPDATE / DELETE / TRUNCATE), a body editor, and a live SQL preview. A <em>Raw SQL</em> tab is always available for hand-edits. Editing a trigger warns that it&#39;s executed as drop + recreate and runs the two statements atomically.</li>
</ul>
<p><img src="https://tabularis.dev/img/tabularis-trigger-editor-modal.png" alt="Create Trigger modal in Guided mode with name and table fields, BEFORE / AFTER / INSTEAD OF timing pills, INSERT / UPDATE / DELETE event buttons, a Monaco body editor, and a generated SQL preview"></p>
<ul>
<li><strong>Create Trigger</strong> from the table or accordion header — same modal, blank slate.</li>
<li><strong>Drop Trigger</strong> — with the standard confirmation.</li>
</ul>
<p>The Rust side is the part you can tell required a database driver author to write. Each engine has its own quirks:</p>
<ul>
<li><strong>PostgreSQL</strong> aggregates multi-event triggers via <code>string_agg</code> on <code>information_schema.triggers</code> (a single trigger can fire on <code>INSERT OR UPDATE</code>, and the catalog stores those as separate rows). Definitions come from <code>pg_get_triggerdef</code>.</li>
<li><strong>MySQL</strong> uses <code>sqlx::raw_sql</code> for trigger DDL to bypass server error 1295 (the prepared-statement protocol doesn&#39;t accept <code>CREATE TRIGGER</code> / <code>DROP TRIGGER</code>). The connection pool is also switched to the correct database before <code>CREATE TRIGGER</code>, which is the kind of detail that only shows up in a multi-database setup.</li>
<li><strong>SQLite</strong> parses <code>sqlite_master.sql</code> to extract the timing and event metadata that the catalog itself doesn&#39;t decompose.</li>
</ul>
<p>Plugin drivers that don&#39;t implement triggers degrade gracefully — <code>get_triggers</code> returns an empty list rather than failing the whole schema load.</p>
<p>The new wiki page covers the lot: <a href="https://tabularis.dev/wiki/triggers">Triggers</a>.</p>
<hr>
<h2>Foreign Keys: Click to Navigate</h2>
<p>Hover an FK cell in the result grid; a small ↗ icon appears on the right. Click it and the referenced table opens in a tab, already filtered to the row you came from. Right-click the same cell and the context menu&#39;s first entry is &quot;Open referenced row in <code>&lt;table&gt;</code>&quot;. Same pattern TablePlus and Postico use, finally in Tabularis (PR <a href="https://github.com/TabularisDB/tabularis/pull/197">#197</a>).</p>
<p><code>fetchPkColumn</code> now fetches columns and foreign keys in parallel when a tab opens, and the FK list lives on the tab so subsequent clicks don&#39;t re-query. The WHERE fragment is built with the existing <code>quoteIdentifier(driver)</code> helper — backticks for MySQL/MariaDB, double-quotes elsewhere — with number / bigint / boolean / string formatting matching what the row-copy SQL INSERT format does. Numeric-looking strings are quoted <em>unless</em> the source column reports a numeric type, which guards against bigints that some drivers ship as JS strings.</p>
<p>If the referenced table is already open as a tab, that tab is reused — the WHERE filter is overwritten and the query re-runs.</p>
<p>V1 sticks to single-column FKs. Composite constraints and cross-schema navigation are noted in the PR and on the roadmap.</p>
<hr>
<h2>Enter Accepts Autocomplete Suggestions</h2>
<p>A small but breaking default change (<a href="https://github.com/TabularisDB/tabularis/issues/186">#186</a>, PR <a href="https://github.com/TabularisDB/tabularis/pull/194">#194</a>): when the autocomplete dropdown is open in the SQL editor, <strong>Enter now accepts the highlighted suggestion</strong> instead of inserting a newline. The behavior every other Monaco-based editor ships by default, finally lined up.</p>
<p>If you preferred the previous behavior, <strong>Settings → Editor → Accept suggestion on Enter</strong> turns it back off. The setting is honored across every editor surface — main SQL tabs, notebook cells, the trigger editor&#39;s Raw SQL tab.</p>
<p>The bump from <code>0.10.x</code> to <code>0.11.0</code> comes from this change — it&#39;s the kind of default switch that warrants a minor bump rather than slipping it into a patch.</p>
<hr>
<h2>Multi-Statement Scripts Now Share a Connection</h2>
<p><a href="https://github.com/ymadd">@ymadd</a> — who landed the Notebook database-selector portal fix in v0.10.3 — shipped a much deeper driver fix in PR <a href="https://github.com/TabularisDB/tabularis/pull/200">#200</a>, closing <a href="https://github.com/TabularisDB/tabularis/issues/199">#199</a>.</p>
<p>Up to v0.10.3, when you ran a multi-statement script through <strong>Run All</strong> the editor fanned out via <code>Promise.allSettled</code> — each statement acquired its own pooled connection. The result was that cross-statement session state silently broke: <code>SET @var := …</code> in statement 1 was invisible to statement 2, <code>LAST_INSERT_ID()</code> / <code>LASTVAL()</code> returned 0 against the wrong session, explicit <code>BEGIN</code> / <code>COMMIT</code> blocks didn&#39;t form a transaction, temporary tables couldn&#39;t be read, and <code>PREPARE</code> / <code>EXECUTE</code> pairs failed. The behavior was &quot;execute everything fast&quot; instead of &quot;execute everything like <code>mysql</code> CLI / <code>psql</code> / DBeaver does&quot;.</p>
<p>The fix is a new <code>execute_batch</code> method on the <code>DatabaseDriver</code> trait. The three built-in drivers override it to acquire <strong>one</strong> pooled connection and run every statement on it in order. The frontend&#39;s <code>runMultipleQueries</code> is replaced with a single <code>invoke(&quot;execute_query_batch&quot;)</code>; the whole batch shares one cancellation handle. Plugin drivers fall back to a default impl that preserves ordering without session-state continuity — the explicit trade-off so plugins don&#39;t break.</p>
<p>The same PR fixes a long-standing reporting bug: the three built-in drivers were hardcoding <code>affected_rows: 0</code>. INSERTs, UPDATEs, and DELETEs now report the real count <code>execute()</code> returned. There are seven new integration tests pinning the behavior down.</p>
<p>This is the kind of work that&#39;s invisible until the moment it isn&#39;t.</p>
<hr>
<h2>A Bigger Cancellation Fix, Too</h2>
<p><a href="https://github.com/ymadd">@ymadd</a> also lands PR <a href="https://github.com/TabularisDB/tabularis/pull/203">#203</a>, closing <a href="https://github.com/TabularisDB/tabularis/issues/201">#201</a>.</p>
<p><code>QueryCancellationState</code> stored <em>one</em> <code>AbortHandle</code> per <code>connection_id</code>. So the second <code>execute_query</code> / <code>execute_query_batch</code> / <code>explain_query_plan</code> against the same connection overwrote the previous handle, and <code>cancel_query</code> could only stop the most recently registered one. The earlier Tokio task kept running on its pooled connection until completion.</p>
<p>The fix switches the slot to a <code>Vec&lt;Arc&lt;AbortHandle&gt;&gt;</code>, prunes finished handles on register, removes specific handles by <code>Arc</code> identity on unregister, and drains the whole slot on cancel. Five new unit tests and a fresh integration test (two <code>SELECT SLEEP(5)</code> on the same connection, single cancel, both report <code>JoinError::is_cancelled()</code>) lock the behavior in. The same fix landed in the export / dump / import path in the follow-up commit, so the cancellation story is consistent across every long-running operation.</p>
<p>If you&#39;ve ever hit Cancel on a heavy query and watched the dot keep spinning on the connection, this is the upgrade.</p>
<hr>
<h2>日本語</h2>
<p>PR <a href="https://github.com/TabularisDB/tabularis/pull/202">#202</a>, also from <a href="https://github.com/ymadd">@ymadd</a>, adds a full Japanese translation. Every key in <code>en.json</code> has a counterpart in <code>ja.json</code>, including the strings that landed <em>this cycle</em> — the trigger management UI (<code>sidebar.*</code>, <code>triggers.*</code>), the connection export/import flow, the Discord callout, and the new &quot;Accept suggestion on Enter&quot; setting. Pick <strong>日本語</strong> from <strong>Settings → Appearance → Language</strong> to switch.</p>
<p>This brings the locale list to <strong>English, Italian, Spanish, French, German, Chinese, and Japanese</strong>.</p>
<p>To make this kind of contribution easier going forward, this release also lands a built-in <strong>Import / Export translations</strong> flow in the Locales settings. Open a single JSON file, edit it offline (or in the AI tool of your choice), import it back in. The export warning makes clear that any unsaved app-level setting will be merged with whatever your file contains, so accidental key loss is avoidable.</p>
<hr>
<h2>Smaller Things</h2>
<ul>
<li><strong>MCP config path on Windows</strong> (<a href="https://github.com/kennelken">@kennelken</a>, PR <a href="https://github.com/TabularisDB/tabularis/pull/204">#204</a>) — the <code>directories</code> crate&#39;s <code>config_dir()</code> resolves to <code>%AppData%\Roaming\tabularis\config</code> on Windows, but the app stores <code>connections.json</code> one directory up at <code>%AppData%\Roaming\tabularis</code>. The MCP server was looking in the nested folder, finding nothing, and shipping an empty connections list to the client. The fix uses the parent of the default <code>config_dir()</code> on Windows, so MCP discovery matches the rest of the app. If you&#39;re on Windows and the MCP server reported zero connections, this is the upgrade.</li>
<li><strong>SQL string color across themes</strong> (<a href="https://github.com/thomaswasle">@thomaswasle</a>, PR <a href="https://github.com/TabularisDB/tabularis/pull/192">#192</a>) — Monaco&#39;s SQL tokenizer sets <code>tokenPostfix: &quot;.sql&quot;</code>, so single-quoted SQL strings tokenize as <code>string.sql</code>, <em>not</em> <code>string</code>. Monaco 0.55 doesn&#39;t reliably fall back, which left SQL strings rendering as a barely-readable dark red on every dark theme (Dracula was the worst offender). Every bundled theme JSON now has an explicit <code>string.sql</code> rule using the same color as the generic <code>string</code> rule; the three built-in themes get the rule injected by <code>generateMonacoTheme()</code>.</li>
<li><strong>&quot;New Console&quot; in sidebar context menus</strong> (<a href="https://github.com/TabularisDB/tabularis/issues/187">#187</a>, PR <a href="https://github.com/TabularisDB/tabularis/pull/188">#188</a>) — right-click a database for <strong>New Console</strong> to open a blank SQL editor scoped to that database; right-click a table for <strong>New Console</strong> to open one pre-filled with <code>SELECT * FROM table_name</code>. Faster than opening the editor and navigating the schema selector when you know exactly what you want to query.</li>
<li><strong>Export through SSH tunnels</strong> (PR <a href="https://github.com/TabularisDB/tabularis/pull/185">#185</a>) — query export over an SSH-tunneled connection was using the connection&#39;s raw host/port instead of the tunnel&#39;s ephemeral local port, so exports against tunneled databases would fail or — worse — hit the wrong server. Export now expands SSH params through the same helper the editor uses.</li>
<li><strong>Docker Compose demo environment</strong> — <code>demo/docker-compose.yml</code> brings up a PostgreSQL with <code>tabularis_demo</code> and <code>analytics_demo</code> databases, plus a MySQL with <code>tabularis_demo</code>, all seeded with <code>customers</code>, <code>departments</code>, <code>employees</code>, <code>orders</code>, <code>order_items</code>, <code>products</code>, plus the new <code>json_demo</code> and <code>text_demo</code> tables that exercise the JSON and long-text cells covered above. One <code>docker compose up -d</code> and you have something to point Tabularis at.</li>
</ul>
<hr>
<h2>Thanks</h2>
<p>Four external contributors land in v0.11.0. This is the largest contributor-driven release Tabularis has shipped to date.</p>
<p><strong><a href="https://github.com/NewtTheWolf">@NewtTheWolf</a></strong> lands the headline feature in two PRs and ships the seed data that makes it testable. PR <a href="https://github.com/TabularisDB/tabularis/pull/181">#181</a> is a properly substantial piece of work — a new Rust module for the viewer windows, ULID-keyed sessions, the Postgres native binding path, the per-connection JSON-detect flag, every locale string, the test plan written out — and it lands ahead of the request queue rather than chasing it. PR <a href="https://github.com/TabularisDB/tabularis/pull/208">#208</a> extends the same pattern to text cells the week after, with the side-by-side diff toggle as the right generalization across both. Dominik has now shipped the JSON viewer, the long-text viewer, the Firestore plugin in v0.10.3, the Discord release template, and the seed data behind half of this changelog — and is still finding obvious-in-hindsight gaps in the UX faster than I am.</p>
<p><strong><a href="https://github.com/thomaswasle">@thomaswasle</a></strong> ships the trigger manager in PR <a href="https://github.com/TabularisDB/tabularis/pull/183">#183</a> — a full vertical slice from the Driver trait down through three driver-specific implementations, a Tauri command surface, a sidebar accordion, a guided modal, and the read-only definition view in the editor — and quietly fixes the SQL string color across every Monaco theme in PR <a href="https://github.com/TabularisDB/tabularis/pull/192">#192</a>. The trigger PR is the kind of contribution that requires you to have read enough of the codebase to know where the Driver trait lives, what the sidebar item conventions are, and how the editor&#39;s read-only-tab flag interacts with the existing run-button rendering. Thomas has now shipped triggers in 0.11.0 and SQL INSERT export + cell selection in 0.10.2 — pattern-matching the work to &quot;the parts of Tabularis that are obviously a database tool&quot;.</p>
<p><strong><a href="https://github.com/ymadd">@ymadd</a></strong> turns into a regular contributor in this cycle. PR <a href="https://github.com/TabularisDB/tabularis/pull/200">#200</a> is the kind of fix that requires you to have <em>used</em> the editor enough to notice that <code>SET @var :=</code> doesn&#39;t survive across statements, and then to track it through <code>Promise.allSettled</code> into the Rust driver trait. The PR ships with seven new integration tests, the MySQL error 1295 workaround for DDL through prepared statements, and a real-vs-zero <code>affected_rows</code> fix folded into the same diff. PR <a href="https://github.com/TabularisDB/tabularis/pull/203">#203</a> takes the cancellation slot from &quot;one handle per connection, last write wins&quot; to a properly per-task <code>Vec&lt;Arc&lt;AbortHandle&gt;&gt;</code>. PR <a href="https://github.com/TabularisDB/tabularis/pull/202">#202</a> is the entire Japanese translation, with every recently-added string already covered. Three PRs in one cycle, every one of them is the kind you accept without changes.</p>
<p><strong><a href="https://github.com/kennelken">@kennelken</a> (Sergey Tarasenko)</strong> is new to the contributor list, and lands a fix in PR <a href="https://github.com/TabularisDB/tabularis/pull/204">#204</a> that the Windows MCP users have been hitting silently — empty connections list, no error, just a server that acts like nothing&#39;s configured. The diagnosis took working backwards from &quot;MCP returns nothing&quot; through the <code>directories</code> crate&#39;s platform-specific behavior; the fix is one branch in <code>get_app_config_dir</code>. Welcome.</p>
<p>If you live in JSONB columns, work across long text fields, want triggers managed without leaving the app, prefer your autocomplete to accept on Enter, run multi-statement scripts that need to share a session, are on Windows and connecting through MCP, or have been waiting for 日本語 — this is the upgrade.</p>
<hr>
<p><em>v0.11.0 is available now. Update via the in-app updater, or download from the <a href="https://github.com/TabularisDB/tabularis/releases/tag/v0.11.0">releases page</a>.</em></p>
]]></content:encoded>
      <enclosure url="https://tabularis.dev/blog/v0110-json-viewer-text-diff-triggers-japanese/opengraph-image.png" type="image/png" />
      <category>release</category>
      <category>feature</category>
      <category>json</category>
      <category>data-grid</category>
      <category>triggers</category>
      <category>i18n</category>
      <category>community</category>
    </item>
    <item>
      <title>The Database Has to Defend Itself Again</title>
      <link>https://tabularis.dev/blog/database-has-to-defend-itself-again</link>
      <guid isPermaLink="true">https://tabularis.dev/blog/database-has-to-defend-itself-again</guid>
      <pubDate>Wed, 13 May 2026 11:00:00 GMT</pubDate>
      <description>For two decades the database could outsource trust to the application layer. Once an LLM with tool access holds a live connection to production, that proxy is gone — the database has to defend itself again.</description>
      <content:encoded><![CDATA[<h1>The Database Has to Defend Itself Again</h1>
<p><em><a href="https://arpitbhayani.me/blogs/defensive-databases" target="_blank" rel="noopener noreferrer">Arpit Bhayani&rsquo;s &ldquo;Defensive Databases for Agentic AI Systems&rdquo;</a> makes the same argument very well, and extends it from a more technical angle — broken assumptions about deterministic callers, intentional writes and brief connections, with concrete patterns like idempotency keys, role-per-agent connection pools, soft deletes and query tagging.</em></p>

<p>For two decades the database has been able to outsource trust to the application layer. The app authenticated users, sanitized inputs, enforced business rules, and the DB just executed whatever came through the connection pool. That worked because the caller was almost always software written by someone, reviewed by someone, and shipped on a release train.</p>
<p>Agents don&#39;t fit that picture.</p>
<p>Once an LLM with tool access holds a live connection to your production database, the assumptions behind the application-as-perimeter model stop being true:</p>
<ul>
<li>Connections aren&#39;t short-lived anymore. A tool-using agent can keep a session open across a long reasoning loop, with the SQL emerging one token at a time.</li>
<li>The caller isn&#39;t deterministic. Two runs of the same prompt can produce different queries. Sometimes very different ones.</li>
<li>Writes aren&#39;t intentional in the way a human commit is. An agent will issue an <code>UPDATE</code> without a <code>WHERE</code> clause if its plan says so.</li>
<li>Failures don&#39;t surface loudly. An exception that would have woken up a developer can be absorbed by the model and rationalized into the next step.</li>
</ul>
<p>Short version: the application layer used to be the boundary. With agents in the loop, it isn&#39;t. The database has to defend itself again.</p>
<p>That&#39;s most of the reason the MCP safety work in Tabularis looks the way it does. The <a href="https://tabularis.dev/wiki/mcp-server">MCP server</a> is the actual surface where an agent and a real database meet, and that surface needs guarantees the model can&#39;t talk its way around.</p>
<p>A few of the pieces we shipped:</p>
<p><strong><a href="https://tabularis.dev/wiki/mcp-readonly-mode">Read-only connections</a>.</strong> Not &quot;the agent promises not to write&quot; — the connection itself rejects writes. If the agent&#39;s plan calls for an <code>UPDATE</code> on a read-only connection, it fails at the boundary, before the row is gone. The classifier strips strings, comments and quoted identifiers before scanning the keyword, and treats anything ambiguous as a write. Fail-closed is the safer default when the alternative is a corrupted production table.</p>
<p><strong><a href="https://tabularis.dev/wiki/mcp-approval-gates">Approval gates</a> with pre-flight <code>EXPLAIN</code>.</strong> Before a write (or a heavy read) actually runs, we surface the statement together with the planner&#39;s view of it for human approval. <code>EXPLAIN</code> turns out to be the right unit here: it shows the model&#39;s intent translated into what the database will really do, and that&#39;s often where the divergence between &quot;what the agent said&quot; and &quot;what would have happened&quot; shows up. You can fix the WHERE clause inside the modal, then approve. Both the original and the edited query are kept, linked by the same approval id.</p>
<p><video src="https://tabularis.dev/videos/wiki/12-ai-approval-gate.mp4" controls muted playsinline loop autoplay controlsList="nodownload noremoteplayback noplaybackrate" disablePictureInPicture></video></p>
<p><strong><a href="https://tabularis.dev/wiki/ai-audit-log">Query audit logs</a>.</strong> Every statement an agent issues is stored locally — one line of JSON per call — with its prompt context, the connection it used, the rows it touched, and the outcome. When something goes wrong (and with agents, something goes wrong) the audit log is how you reconstruct what actually happened, not what the model claims it did.</p>
<p><img src="https://tabularis.dev/img/tabularis-ai-audit-log-sessions.png" alt="Tabularis MCP Activity panel grouped into sessions, with an Export as Notebook button on each session"></p>
<p><strong>Full MCP activity tracing.</strong> Tool calls, results, errors, timing: the whole exchange between the agent and Tabularis is observable. Events can be flat-filtered or auto-grouped into sessions by inactivity gaps, and any session can be exported as a SQL notebook you can replay, diff against another run, or attach to a PR. When a model starts improvising, you can usually pinpoint the exact tool call where it happened.</p>
<p><img src="https://tabularis.dev/img/tabularis-ai-audit-log-event-details.png" alt="Detail view of a single MCP audit event, showing the query, classifier kind, connection, status and the row of context surrounding it"></p>
<p>None of these ideas are new. DBAs have wanted half of them for years. We could get away without them because the application layer was a decent proxy for &quot;someone reasoned about this before it ran.&quot;</p>
<p>That proxy is gone. Putting the guarantees back inside the database itself is cheap compared to finding out, after the fact, that an agent dropped a column at 3am because its context window was full of stale documentation.</p>
<p>&quot;Trust the application layer&quot; was a fine default. With agents in the loop, it stops being one.</p>
]]></content:encoded>
      <enclosure url="https://tabularis.dev/blog/database-has-to-defend-itself-again/opengraph-image.png" type="image/png" />
      <category>ai</category>
      <category>mcp</category>
      <category>safety</category>
      <category>audit</category>
      <category>opinion</category>
      <category>architecture</category>
    </item>
    <item>
      <title>v0.10.3: Portable Connections, an Editor Error Boundary, and Firestore</title>
      <link>https://tabularis.dev/blog/v0103-connection-import-export-editor-error-boundary-firestore</link>
      <guid isPermaLink="true">https://tabularis.dev/blog/v0103-connection-import-export-editor-error-boundary-firestore</guid>
      <pubDate>Mon, 11 May 2026 10:00:00 GMT</pubDate>
      <description>v0.10.3 lands JSON-based connection export and import (passwords included, keychain-safe), an editor error boundary that keeps the workspace alive when a driver crashes the result grid, a portal-rendered notebook database selector, a fix for drivers that return unnamed columns, and a new community-built Firestore plugin.</description>
      <content:encoded><![CDATA[<h1>v0.10.3: Portable Connections, an Editor Error Boundary, and Firestore</h1>
<p><strong>v0.10.3</strong> is a community-heavy follow-up to <a href="https://tabularis.dev/blog/v0102-postgres-rds-tls-cell-selection-sql-insert">v0.10.2</a>. Three external contributors land in this tag — two of them new — and a fourth ships a Firestore driver to the plugin registry on the same day. The headline change is being able to move your connections between machines without rebuilding them by hand; the rest is the kind of resilience work that&#39;s invisible until the day it isn&#39;t.</p>
<p>If v0.10.2 was about getting connections to work where they should, v0.10.3 is about getting them — and the editor that consumes them — to travel.</p>
<hr>
<h2>Connection Export and Import</h2>
<p>Up to v0.10.2, the only way to move your connection profiles between two installations was to copy <code>connections.json</code>, copy <code>ssh_connections.json</code>, then re-enter every password by hand on the new machine because the secrets live in the OS keychain and the JSON files don&#39;t include them. Doable, but painful past a handful of connections.</p>
<p>PRs <a href="https://github.com/TabularisDB/tabularis/pull/175">#175</a> and <a href="https://github.com/TabularisDB/tabularis/pull/176">#176</a> — both originating in <a href="https://github.com/zhaopengme">@zhaopengme</a>&#39;s combined PR <a href="https://github.com/TabularisDB/tabularis/pull/172">#172</a>, split into two focused PRs for review — replace that with a single round-trip through a JSON file.</p>
<p>The Connections page gets <strong>Export</strong> and <strong>Import</strong> buttons in the toolbar. Export walks every connection group, saved database connection, and SSH profile, pulls the relevant password out of the OS keychain (database password, SSH password, SSH key passphrase), and writes the lot into a JSON file. Import takes that file back, merges it with the existing config, writes the embedded passwords back into the keychain, and persists the connection files — so the imported entries behave exactly like manually-created ones. A fresh install also picks up an <strong>Import</strong> button on the empty-state view, so you have a way in before you&#39;ve created the first profile.</p>
<p>The trade-off is unavoidable: the exported JSON contains plaintext passwords. Keep the file the way you&#39;d keep a <code>.env</code>. If you only need to move connection shape and not credentials, you can strip the password fields before importing — Import writes back whatever passwords are present and leaves the keychain alone for empty ones.</p>
<p>The same PR also lands password visibility toggles on every password input across the New Connection, SSH, and AI provider modals. Small ergonomic win when you&#39;re pasting a password and want to see whether the paste landed correctly.</p>
<p>Full reference in the wiki: <a href="https://tabularis.dev/wiki/connections#export--import">Connections → Export / Import</a>.</p>
<hr>
<h2>An Editor Error Boundary</h2>
<p><a href="https://github.com/saurabh500">@saurabh500</a> reported and fixed a sharp edge in <a href="https://github.com/TabularisDB/tabularis/pull/173">#173</a>: some drivers return columns with no name. The two examples in the wild are SQL Server&#39;s <code>SELECT @@VERSION</code> and PostgreSQL&#39;s <code>SELECT 1 AS &quot;&quot;</code>. The data grid couldn&#39;t render the empty column header — the whole editor pane blanked out instead, with no result, no error message, and no recovery short of reopening the tab.</p>
<p>The fix is small: empty column names are handled internally without breaking the grid. Drivers that return real names are unaffected.</p>
<p>That bug surfaced something else — there was no top-level error boundary around the editor. So one driver edge case could take down the whole workspace. PR <a href="https://github.com/TabularisDB/tabularis/pull/173">#173</a> closes the immediate crash, and a follow-up commit wraps the editor surface in an <strong>Editor Error Boundary</strong> with a fallback UI (&quot;Editor crashed — try again / report&quot;), translated across English, Italian, Spanish, French, German, and Chinese.</p>
<p>Together they&#39;re the difference between &quot;your query crashed the app&quot; and &quot;your query crashed; here&#39;s the trace and a reload button.&quot;</p>
<p>Saurabh also lands a small refactor in <a href="https://github.com/TabularisDB/tabularis/pull/174">#174</a> — not user-visible, but the kind of housekeeping you only do when you&#39;ve started reading the code seriously.</p>
<hr>
<h2>Notebook Database Selector, Now Through a Portal</h2>
<p>The scrollable database selector that landed in v0.10.1 (<a href="https://github.com/TabularisDB/tabularis/pull/160">#160</a>) had a clipping bug nobody hit until a Notebook cell with a tall dropdown showed up. Short cells — no result yet, the last cell in a notebook, anything with collapsed neighbors — cut off the lower half of the dropdown, and the inner scrollbar became unreachable.</p>
<p>New contributor <a href="https://github.com/ymadd">@ymadd</a> shipped the fix in PR <a href="https://github.com/TabularisDB/tabularis/pull/178">#178</a>. The dropdown now renders on top of the page rather than inside the cell, so it can&#39;t be clipped regardless of how tall or short its container is. Behavior across scroll and resize is consistent with the rest of the app, and the existing styling, height cap, click-outside-to-close, and &quot;show only when more than one database&quot; condition are all preserved.</p>
<p>If you have a MySQL host with many schemas and you&#39;ve been bouncing off the Notebook DB selector since v0.10.1, this is the upgrade.</p>
<hr>
<h2>A New Plugin: Firestore (Community)</h2>
<p>The plugin ecosystem picks up a sixth third-party driver, and the first one to target a managed NoSQL platform: <strong>firestore-tabularis</strong> by <a href="https://github.com/NewtTheWolf">@NewtTheWolf</a>, connecting Tabularis to <a href="https://cloud.google.com/firestore">Google Cloud Firestore</a>. It&#39;s now published to the <a href="https://tabularis.dev/plugins">plugin registry</a>, so it&#39;s a one-click install from the in-app Plugin Manager.</p>
<p>The mapping is the interesting part. Firestore is collection/document, not table/row, and it has no schema. The plugin fits Firestore into Tabularis&#39; relational worldview by listing root collections as tables and sampling N documents per collection (default 50) to infer column types. Inferred schemas are cached per process; an optional set of JSON override files lets you pin required-ness, correct types, hide fields, or declare extras per project/database.</p>
<p>What works today (tagged <code>v0.1.0</code>):</p>
<ul>
<li><strong>Connection lifecycle</strong> — install from the in-app Plugin Manager, then connect like any other driver.</li>
<li><strong>Schema discovery</strong> — root collections appear as tables, columns are inferred from document samples, and the ER diagram is populated with inferred foreign keys.</li>
<li><strong>A SQL subset for queries</strong> — <code>SELECT</code> with <code>WHERE</code>, <code>AND</code> / <code>OR</code> / <code>NOT</code>, <code>IN</code>, array contains, ordering, <code>LIMIT</code> / <code>OFFSET</code>, and cursor pagination.</li>
<li><strong><code>EXPLAIN</code></strong> mapped to Firestore&#39;s plan endpoint, with documents returned, documents scanned, index used, and execution time.</li>
<li><strong>CRUD</strong> via the data grid context menu (insert, update, delete rows, rename document IDs).</li>
<li><strong>The full Google auth chain</strong> — service account JSON, Application Default Credentials, <code>GOOGLE_APPLICATION_CREDENTIALS</code>, or the Firestore emulator host.</li>
</ul>
<p>What&#39;s not in v0.1.0 yet: writing through raw SQL statements (<code>INSERT INTO</code>, <code>UPDATE</code>, <code>DELETE</code>) — those return a friendly redirect pointing you at the grid actions instead, with SQL-side DML on the plugin&#39;s roadmap. DDL is intentionally absent because Firestore is schemaless. Subcollections, multi-database, and live mode are listed as future phases.</p>
<p>The plugin is hosted on Codeberg (<a href="https://codeberg.org/NewtTheWolf/firestore-tabularis">NewtTheWolf/firestore-tabularis</a>) with binaries mirrored for installation from the registry. If you&#39;ve been waiting to point Tabularis at a Firestore project, this is the upgrade — and the plugin is open for issues on Codeberg.</p>
<hr>
<h2>A Discord Community Channel</h2>
<p>Tabularis now has a <strong>dedicated Discord channel</strong> for the community — a place to ask questions, share what you&#39;re building, and follow what&#39;s coming next.</p>
<p>To make it findable from inside the app, a small tile appears in the sidebar the first time you launch this version, inviting you to join the server. Dismiss it once and it&#39;s gone for good. Every Discord link across the app, the README, and the contributing guide now points to the same invite, so wherever you click, you land in the same room.</p>
<p>Come say hi.</p>
<p style="margin-top: 1.5rem;"><a href="https://discord.com/invite/K2hmhfHRSt" class="discord-btn" target="_blank" rel="noopener noreferrer" style="padding: 0.75rem 1.5rem; font-size: 1rem; text-decoration: none;">Join the Discord →</a></p>

<hr>
<h2>Smaller Things</h2>
<p>A handful of polish items round out the release:</p>
<ul>
<li><strong>Connections page — import on empty state</strong>. The empty Connections view (&quot;No saved connections yet&quot;) used to offer only a &quot;New Connection&quot; button. It now also offers an <strong>Import</strong> button, so a fresh install with an exported payload in hand is one click from being usable.</li>
</ul>
<hr>
<h2>Thanks</h2>
<p>Three external contributors land in v0.10.3, and one community plugin ships alongside it.</p>
<p><strong><a href="https://github.com/zhaopengme">@zhaopengme</a></strong> is new to the contributor list and lands the headline feature. The original PR <a href="https://github.com/TabularisDB/tabularis/pull/172">#172</a> bundled both password visibility toggles and connection export/import; splitting it into <a href="https://github.com/TabularisDB/tabularis/pull/175">#175</a> and <a href="https://github.com/TabularisDB/tabularis/pull/176">#176</a> for separate review was friction we asked for and you accommodated without pushback — thank you.</p>
<p><strong><a href="https://github.com/saurabh500">@saurabh500</a></strong> continues a streak that started outside this window: two PRs in this tag (<a href="https://github.com/TabularisDB/tabularis/pull/173">#173</a> and <a href="https://github.com/TabularisDB/tabularis/pull/174">#174</a>), one bug fix sharp enough to expose a missing error boundary, one small refactor that&#39;s the kind of thing you only do when you&#39;ve started reading the code seriously.</p>
<p><strong><a href="https://github.com/ymadd">@ymadd</a></strong> is also new — PR <a href="https://github.com/TabularisDB/tabularis/pull/178">#178</a> is a textbook portal-rendering fix, with the test plan, the reproduction conditions, and the cross-reference to the existing pattern already written. The kind of PR that&#39;s just done when it lands.</p>
<p><strong><a href="https://github.com/NewtTheWolf">@NewtTheWolf</a></strong> ships <a href="https://codeberg.org/NewtTheWolf/firestore-tabularis">firestore-tabularis</a> the same day as the release — a full Firestore driver written from scratch against the plugin protocol, with schema inference, a SELECT parser, EXPLAIN plan extraction, schema overrides, and the entire Google auth chain wired up. The plugin protocol exists so this kind of thing can happen without us touching the core, and it&#39;s still satisfying when it does.</p>
<p>If you&#39;ve been moving between machines and rebuilding your connection list by hand, hitting unnamed columns in SQL Server or Postgres, bouncing off the Notebook DB selector, or waiting to point Tabularis at Firestore — this is the upgrade.</p>
<hr>
<p><em>v0.10.3 is available now. Update via the in-app updater, or download from the <a href="https://github.com/TabularisDB/tabularis/releases/tag/v0.10.3">releases page</a>.</em></p>
]]></content:encoded>
      <enclosure url="https://tabularis.dev/blog/v0103-connection-import-export-editor-error-boundary-firestore/opengraph-image.png" type="image/png" />
      <category>release</category>
      <category>feature</category>
      <category>ui</category>
      <category>ux</category>
      <category>plugin</category>
      <category>community</category>
    </item>
    <item>
      <title>v0.10.2: Postgres on AWS RDS, Cell-Level Copy, and SQL INSERT Export</title>
      <link>https://tabularis.dev/blog/v0102-postgres-rds-tls-cell-selection-sql-insert</link>
      <guid isPermaLink="true">https://tabularis.dev/blog/v0102-postgres-rds-tls-cell-selection-sql-insert</guid>
      <pubDate>Fri, 08 May 2026 08:59:00 GMT</pubDate>
      <description>v0.10.2 fixes a Postgres TLS handshake that broke AWS RDS connections on macOS, lands cell-level selection and SQL INSERT as a copy format in the data grid, restores MySQL passwordless connections, and unbreaks the Manage SSH Connections button.</description>
      <content:encoded><![CDATA[<h1>v0.10.2: Postgres on AWS RDS, Cell-Level Copy, and SQL INSERT Export</h1>
<p><strong>v0.10.2</strong> is another short follow-up to <a href="https://tabularis.dev/blog/v0101-pagination-fix-context-menu-postgres-bindings">v0.10.1</a>. Four days after the patch, three users opened three independent issues — two against the connection layer, one against an SSH modal that suddenly stopped opening — and a handful of data grid features were already on their way in. v0.10.2 closes the issues, lands the features, and goes out the door.</p>
<p>If v0.10.1 was about smoothing the AI safety release, v0.10.2 is about getting connections to work where they should and making the data grid a little more useful once you&#39;re inside.</p>
<hr>
<h2>Postgres on AWS RDS Works now</h2>
<p>This is the headline fix, and the kind of bug that&#39;s particularly painful: &quot;Test connection&quot; succeeds, schemas load, then 30 seconds later the health check pings the pool, the TLS handshake fails, and the UI tells you the connection was lost. Reproducible across restarts. Indistinguishable from &quot;the database is down&quot; if you don&#39;t read the logs.</p>
<p><a href="https://github.com/benedettoraviotta">@benedettoraviotta</a> reported it in <a href="https://github.com/TabularisDB/tabularis/issues/166">#166</a> and shipped the fix in PR <a href="https://github.com/TabularisDB/tabularis/pull/167">#167</a>. The diagnosis took some patience: <code>tokio_postgres</code> only surfaces <code>error performing TLS handshake</code> and hides the underlying cause. Walking <code>source()</code> on the error chain exposes the real story — on macOS, Secure Transport applies a strict <code>id-kp-serverAuth</code> Extended Key Usage check to user-supplied root anchors and rejects valid CAs (the AWS RDS bundle is a textbook example) with &quot;The extended key usage is not valid&quot;. Independently, the system keychain doesn&#39;t trust the regional Amazon RDS root CAs, so platform verification fails with <code>errSecNotTrusted (-67843)</code>.</p>
<p>The fix replaces <code>postgres-native-tls</code> with <code>tokio-postgres-rustls</code> for the deadpool path, switches the trust source to <code>rustls-platform-verifier</code>, and starts honoring <code>params.ssl_ca</code>. RDS users can now paste <code>https://truststore.pki.rds.amazonaws.com/global/global-bundle.pem</code> into the connection&#39;s CA Certificate field and connect cleanly. MySQL/sqlx remains on <code>native-tls</code> — the bug was specific to the Postgres pool path and there was no reason to churn the rest.</p>
<p>The RDS bundle is intentionally <strong>not</strong> vendored. AWS rotates these CAs every one to three years; a vendored copy would silently break released apps the moment the next rotation lands and the user is on an old binary. Distributors who want out-of-the-box RDS support can pull the bundle at packaging time (Dockerfile <code>RUN</code>, build script, etc.) and ship it alongside the app.</p>
<p>If you&#39;ve been bouncing off RDS since upgrading to v0.10.x, this is the upgrade.</p>
<hr>
<h2>Cell-Level Selection and Copy</h2>
<p>Up to v0.10.1, the data grid let you select rows. You could shift-click a range, ctrl-click to add to the set, and copy the lot to the clipboard. What you couldn&#39;t do was select a single cell — the whole row came along, every time.</p>
<p>PR <a href="https://github.com/TabularisDB/tabularis/pull/161">#161</a> from <a href="https://github.com/thomaswasle">@thomaswasle</a> adds cell-level selection. Click any cell and it gets a focused outline; the row checkbox stays untouched. <code>Cmd/Ctrl+C</code> copies just the cell value, formatted using the same null/length/type rules the row copy uses. A new &quot;Copy cell&quot; entry appears in the right-click menu for the moments when keyboard isn&#39;t faster.</p>
<p>The two interaction modes don&#39;t fight each other: clicking a row checkbox clears the cell focus, clicking a cell clears the row selection. So copy with an active cell focus copies the cell, copy with selected rows copies the rows. The behavior you&#39;d expect, just without the surprises.</p>
<hr>
<h2>SQL INSERT as a Copy Format</h2>
<p>The flip side of &quot;I want this row&quot; is &quot;I want to put this row somewhere else&quot;. CSV, TSV, and JSON copy formats already covered most exports. PR <a href="https://github.com/TabularisDB/tabularis/pull/168">#168</a>, also from <a href="https://github.com/thomaswasle">@thomaswasle</a>, adds <strong>SQL INSERT</strong> as a fourth option.</p>
<p>Set it once in <strong>Settings → General → Copy format</strong>, then copy any selected rows from the data grid. You get back a sequence of <code>INSERT INTO \</code>table` (`col1`, `col2`, …) VALUES (…);<code>statements, one per line. NULLs render as</code>NULL<code>, booleans as </code>TRUE<code>/</code>FALSE`, numbers unquoted, strings single-quoted with single quotes doubled-up — the basics that make the output paste-able into another shell or query window without hand-editing.</p>
<p>It complements the duplicate-row context menu action that landed in v0.10.1: that one stays in-grid and inserts the row right where it sits, this one ships the row out as text.</p>
<hr>
<h2>Postgres Boolean Submit Error</h2>
<p><a href="https://github.com/simonwang1024">@simonwang1024</a> reported in <a href="https://github.com/TabularisDB/tabularis/issues/155">#155</a> that editing a value in a Postgres result grid and pressing <strong>Submit</strong> returned an error. The path involved was the <code>binding</code> module that landed in v0.10.1: when the data grid sends an edited value back, it serializes the cell as a string, and the binding layer maps it to a typed parameter based on the column type.</p>
<p>For boolean columns, the column type was correctly identified, but the value still arrived as a string (<code>&quot;true&quot;</code>, <code>&quot;false&quot;</code>, <code>&quot;t&quot;</code>, <code>&quot;f&quot;</code>, <code>&quot;1&quot;</code>, <code>&quot;0&quot;</code>) and the bind was rejected because Postgres expects a real <code>bool</code>. The fix coerces the common string forms to a <code>bool</code> before binding, with 105 lines of new tests covering the cases that come out of the data grid, JSON inputs, and SQL editor parameters. Edits to boolean columns now go through cleanly.</p>
<hr>
<h2>Smaller Things</h2>
<p>Two community-reported regressions round out the release, both from <a href="https://github.com/MischaKr">@MischaKr</a>:</p>
<ul>
<li><strong>MySQL passwordless connections</strong> (<a href="https://github.com/TabularisDB/tabularis/issues/164">#164</a>, fixed in <a href="https://github.com/TabularisDB/tabularis/pull/169">#169</a>). After the v0.10.1 connection URL refactor, MySQL connections without a password were producing URLs with a trailing colon (<code>user:@host</code>), which some servers reject and some accept silently with surprising behavior. The fix simply omits the password segment entirely when the field is empty, so the URL ends up as <code>user@host</code>. The connection-URL test fixture got an updated assertion to lock the behavior in.</li>
<li><strong>Manage SSH Connections button</strong> (<a href="https://github.com/TabularisDB/tabularis/issues/163">#163</a>, fixed in commit <a href="https://github.com/TabularisDB/tabularis/commit/9eb48e28da50fefaaab712f282ea76b9a58fa735">9eb48e2</a>). The button rendered, but clicking it did nothing — the SSH connections modal was opening underneath another overlay and getting click-blocked. A z-index bump and a backdrop-blur tweak put it on top where it belongs.</li>
</ul>
<hr>
<h2>Thanks</h2>
<p>Three external contributors land in v0.10.2 and each fixed a different layer of the stack. <strong><a href="https://github.com/benedettoraviotta">@benedettoraviotta</a></strong> for the AWS RDS TLS investigation — diagnosing a &quot;TLS handshake failed&quot; through two layers of error wrapping, two macOS-specific quirks, and two TLS stacks isn&#39;t trivial work, and the PR came in with the rustls swap, the platform verifier, the <code>ssl_ca</code> honoring, and the call to <em>not</em> vendor the RDS bundle. That last call is the one I&#39;d have got wrong. <strong><a href="https://github.com/thomaswasle">@thomaswasle</a></strong> for two more data grid PRs — cell selection and SQL INSERT export — both small in diff and immediately useful. <strong><a href="https://github.com/simonwang1024">@simonwang1024</a></strong> and <strong><a href="https://github.com/MischaKr">@MischaKr</a></strong> for the bug reports that kept the release honest. Two of Mischa&#39;s three issues this cycle turned into shipped fixes; the third (<code>#164</code>, MySQL passwordless) became the first commit on the way to this tag.</p>
<p>If you connect to AWS RDS, edit boolean columns, or use MySQL without a password, this is the upgrade.</p>
<hr>
<p><em>v0.10.2 is available now. Update via the in-app updater, or download from the <a href="https://github.com/TabularisDB/tabularis/releases/tag/v0.10.2">releases page</a>.</em></p>
]]></content:encoded>
      <enclosure url="https://tabularis.dev/blog/v0102-postgres-rds-tls-cell-selection-sql-insert/opengraph-image.png" type="image/png" />
      <category>release</category>
      <category>bugfix</category>
      <category>postgres</category>
      <category>data-grid</category>
      <category>tls</category>
      <category>community</category>
    </item>
    <item>
      <title>v0.10.1: Pagination Fix, Context Menu Actions, and Postgres Bindings</title>
      <link>https://tabularis.dev/blog/v0101-pagination-fix-context-menu-postgres-bindings</link>
      <guid isPermaLink="true">https://tabularis.dev/blog/v0101-pagination-fix-context-menu-postgres-bindings</guid>
      <pubDate>Mon, 04 May 2026 12:00:00 GMT</pubDate>
      <description>v0.10.1 is a short follow-up to v0.10.0: a sneaky LIMIT bug in SQL pagination gets a proper SQL tokenizer, the data grid gains multi-row deletion, duplicate row and insert-current-time actions, and the PostgreSQL driver moves to a parameterized binding module.</description>
      <content:encoded><![CDATA[<h1>v0.10.1: Pagination Fix, Context Menu Actions, and Postgres Bindings</h1>
<p><strong>v0.10.1</strong> is a short follow-up to <a href="https://tabularis.dev/blog/v0100-ai-safety-audit-approval">v0.10.0</a>. The AI safety release shipped a week ago, and a few bug reports came back almost the same day. v0.10.1 closes those, lands three new context menu actions in the data grid, and rewires how the PostgreSQL driver binds parameters under the hood.</p>
<p>If v0.10.0 was about giving the agent a safer door into your database, v0.10.1 is about smoothing out the things you bumped into while using it.</p>
<hr>
<h2>A Sneaky LIMIT Bug</h2>
<p>This is the headline fix, and it&#39;s the kind of bug that&#39;s invisible until the day it isn&#39;t.</p>
<p><a href="https://github.com/midasism">@midasism</a> reported and fixed it in PR <a href="https://github.com/TabularisDB/tabularis/pull/154">#154</a>. When Tabularis paginates a <code>SELECT</code> in the data grid, it strips the user&#39;s <code>LIMIT</code> / <code>OFFSET</code> (if any), wraps the query, and re-applies its own pagination. The two helpers responsible — <code>strip_limit_offset</code> and <code>extract_user_limit</code> — used a naive <code>rfind(&quot;LIMIT&quot;)</code> against the raw SQL string.</p>
<p>That works right up until you query a table whose name happens to contain the substring <code>limit</code> (<code>tapp_appointment_message_event_limit</code> was the real-world example), or a string literal that mentions the word, or a quoted identifier. The pagination wrapper would clip the query in the wrong place and you&#39;d end up with corrupted SQL or duplicated <code>LIMIT</code> clauses.</p>
<p>The fix replaces the raw string search with a small SQL tokenizer (<code>tokenize_sql</code>) that treats single-quoted strings, double-quoted identifiers, backtick-quoted identifiers, and parenthesized groups as opaque tokens. <code>strip_limit_offset</code> and <code>extract_user_limit</code> now scan backward over those tokens, so only standalone <code>LIMIT</code> / <code>OFFSET</code> keywords match. 11 new test cases cover table names containing <code>limit</code>, quoted identifiers, string literals with SQL keywords, and subqueries.</p>
<p>The MCP <code>run_query</code> tool got a small upgrade in the same PR: it now accepts an optional <code>limit</code> parameter (default <code>100</code>), and respects user <code>LIMIT</code> clauses inside the SQL when present. Agents that want the full result can pass an explicit value; the default keeps them from accidentally pulling a million rows into context.</p>
<p>If you ran into this bug after upgrading to v0.10.0, the upgrade to v0.10.1 is the fix.</p>
<hr>
<h2>Three New Data Grid Actions</h2>
<p>Three back-to-back PRs from <a href="https://github.com/thomaswasle">@thomaswasle</a> extend the data grid context menu — the same one that already handles single-row delete and copy-as.</p>
<p><strong>Multi-row deletion</strong> (PR <a href="https://github.com/TabularisDB/tabularis/pull/158">#158</a>). Select multiple rows in the grid, right-click, and delete them in one shot instead of repeating the action row by row. The deletion goes through the same path as single-row delete — same confirmation, same reload behavior — so there&#39;s nothing new to learn.</p>
<p><strong>Duplicate row</strong> (PR <a href="https://github.com/TabularisDB/tabularis/pull/159">#159</a>). Right-click any row and copy it as a new INSERT, with the primary key cleared (or auto-incremented, depending on the column). Useful when you&#39;re seeding data, building a quick test fixture, or making a small variation of an existing record without writing the SQL by hand.</p>
<p><strong>Insert current time</strong> (also PR <a href="https://github.com/TabularisDB/tabularis/pull/159">#159</a>). On any timestamp/datetime cell, the context menu now offers an &quot;insert current time&quot; action that drops <code>NOW()</code> (or the driver&#39;s equivalent) into the cell. Small ergonomic win when you&#39;re filling rows manually.</p>
<p>All three actions are translated across English, Italian, Spanish, French, German, and Chinese.</p>
<hr>
<h2>Parameterized Bindings for PostgreSQL</h2>
<p>The PostgreSQL driver had grown a sprawl of inline string-to-SQL conversions in <code>mod.rs</code> — formatting numbers, escaping strings, converting JSON arrays to PostgreSQL array literals, handling UUIDs and blobs. It worked, but each call site had to remember the right escape rules, and edge cases were easy to miss.</p>
<p>PR <a href="https://github.com/TabularisDB/tabularis/pull/156">#156</a> extracts all of that into a dedicated <code>binding</code> module. Values are bound as proper <code>tokio-postgres</code> parameters (<code>$1</code>, <code>$2</code>, ...) instead of being interpolated into the SQL string. Numbers are cast through <code>bigint</code> / <code>double precision</code> so the bind succeeds against <code>int2</code> / <code>int4</code> / <code>int8</code> / <code>real</code> columns; UUID strings are detected and bound as the <code>Uuid</code> type so PostgreSQL receives the matching OID; arrays go through a JSON-to-array literal conversion that handles nested types; blobs respect the configured <code>max_blob_size</code>.</p>
<p>This is the kind of refactor where the user-facing diff is &quot;nothing changed&quot;. 208 lines of new tests and a 350-line trim in <code>mod.rs</code> argue otherwise: edits that touch numbers, UUIDs, arrays, or binary columns now go through one well-tested code path instead of seven slightly-different ones. The kind of work that&#39;s worth doing once, before it bites again.</p>
<hr>
<h2>Smaller Things</h2>
<p>A handful of polish items round out the release:</p>
<ul>
<li><strong>Scrollable database dropdowns</strong> (<a href="https://github.com/TabularisDB/tabularis/pull/160">#160</a>, thomaswasle). The Editor and Notebook database selectors used to render a flat dropdown that grew indefinitely. Past 10 databases it became unusable. Now the menu caps its visible height and scrolls.</li>
<li><strong>Connection-modal placeholders</strong> (<a href="https://github.com/TabularisDB/tabularis/pull/157">#157</a>, thomaswasle). Empty fields in the New Connection modal were rendered with a value-styled appearance, which made them look pre-filled when they weren&#39;t. Now an empty field looks empty.</li>
<li><strong>Semver-aware &quot;What&#39;s New&quot;</strong>. The in-app changelog and &quot;What&#39;s New&quot; dialog used naive string comparison to figure out which release notes to surface. That worked fine until a <code>0.10.x</code> versus <code>0.9.x</code> comparison came around, where <code>&quot;0.10&quot;</code> is lexically less than <code>&quot;0.9&quot;</code>. A new <code>versionCompare</code> utility (with tests) compares releases as proper semver and the changelog parser now also accepts level-one headings, so the right release notes show up after every upgrade.</li>
<li><strong>Visual Query Builder polish</strong>. The graph view gained a small embedded result grid, a schema metadata cache hook, and a dagre-based auto-layout pass. Useful when you&#39;re iterating on a builder query and don&#39;t want to switch to the editor tab to see the rows.</li>
</ul>
<hr>
<h2>Thanks</h2>
<p>A patch release is mostly bug reports turning into PRs and PRs turning into a tag. <strong><a href="https://github.com/midasism">@midasism</a></strong> for finding and fixing the LIMIT bug — that one was easy to ship and hard to spot, and you nailed both. <strong><a href="https://github.com/thomaswasle">@thomaswasle</a></strong> for four PRs in a single window, all small, all good, all making the app a little more pleasant to use.</p>
<p>If you&#39;ve been holding off because of a bug v0.10.0 left behind, this is the upgrade.</p>
<hr>
<p><em>v0.10.1 is available now. Update via the in-app updater, or download from the <a href="https://github.com/TabularisDB/tabularis/releases/tag/v0.10.1">releases page</a>.</em></p>
]]></content:encoded>
      <enclosure url="https://tabularis.dev/blog/v0101-pagination-fix-context-menu-postgres-bindings/opengraph-image.png" type="image/png" />
      <category>release</category>
      <category>bugfix</category>
      <category>postgres</category>
      <category>ux</category>
      <category>data-grid</category>
      <category>community</category>
    </item>
    <item>
      <title>AI Safety, Audit Log and Approval Gates: v0.10.0</title>
      <link>https://tabularis.dev/blog/v0100-ai-safety-audit-approval</link>
      <guid isPermaLink="true">https://tabularis.dev/blog/v0100-ai-safety-audit-approval</guid>
      <pubDate>Sat, 25 Apr 2026 10:00:00 GMT</pubDate>
      <description>v0.10.0 ships an AI audit log, MCP read-only mode, and approval gates with pre-flight EXPLAIN preview. Built around a 200-line file-queue between two processes.</description>
      <content:encoded><![CDATA[<h1>AI Safety, Audit Log and Approval Gates: v0.10.0</h1>
<p>Tabularis has been <a href="https://tabularis.dev/wiki/mcp-server">MCP-native</a> since v0.9.9: Claude Desktop, Claude Code, Cursor, Windsurf and Antigravity can all talk to your saved connections through the <code>tabularis --mcp</code> server, with schema reading, table description and query execution. The catch up to now is that once you set MCP up, the agent had the same level of access you do.</p>
<p>v0.10.0 is the release that closes that gap. Three features land together, all visible at the first launch after the upgrade:</p>
<ul>
<li>An <a href="https://tabularis.dev/wiki/ai-audit-log">audit log</a> of every MCP tool call, stored locally and queryable from a new Settings panel.</li>
<li><a href="https://tabularis.dev/wiki/mcp-readonly-mode">Read-only mode</a> to block writes per-connection or globally.</li>
<li><a href="https://tabularis.dev/wiki/mcp-approval-gates">Approval gates</a> that pause writes and require user confirmation, with a pre-flight EXPLAIN plan rendered inside the modal.</li>
</ul>
<p>Two smaller features come along for the ride: exporting an entire AI session as a SQL notebook, and jumping from any audit row into Visual Explain.</p>
<img src="https://tabularis.dev/img/tabularis-mcp-server.png" alt="Tabularis MCP Server Integration panel showing one-click install for Claude Desktop, Claude Code, Cursor, Windsurf and Antigravity" style="width:100%;border-radius:8px;margin:1.5rem 0" />

<hr>
<h2>1. The audit log</h2>
<p>Every MCP tool call is now recorded as one line of JSON in <code>~/.config/tabularis/ai_activity.jsonl</code>:</p>
<pre><code class="language-json">{&quot;id&quot;:&quot;4f9b…&quot;,&quot;sessionId&quot;:&quot;a8c1…&quot;,&quot;timestamp&quot;:&quot;2026-04-24T14:02:11Z&quot;,
 &quot;tool&quot;:&quot;run_query&quot;,&quot;connectionId&quot;:&quot;prod-pg&quot;,&quot;connectionName&quot;:&quot;prod&quot;,
 &quot;query&quot;:&quot;SELECT count(*) FROM orders&quot;,&quot;queryKind&quot;:&quot;select&quot;,
 &quot;durationMs&quot;:42,&quot;status&quot;:&quot;success&quot;,&quot;rows&quot;:1,
 &quot;clientHint&quot;:&quot;claude-desktop&quot;,&quot;approvalId&quot;:null}
</code></pre>
<p>A new <strong>MCP → Activity</strong> tab in the app reads this file (the plug icon in the sidebar opens the MCP page). It has two sub-tabs:</p>
<ul>
<li><strong>Events</strong>: flat, filterable, exportable to CSV or JSON.</li>
<li><strong>Sessions</strong>: events auto-grouped by 10-minute inactivity gaps, with a per-session <strong>Export as Notebook</strong> button.</li>
</ul>
<p>The Sessions sub-tab is probably the most useful of the two. One click and you get a valid <code>.tabularis-notebook</code> file you can replay or attach to a PR:</p>
<ul>
<li>A markdown header with session metadata (client, connections, time range, event count).</li>
<li>One SQL cell per <code>run_query</code>, in chronological order.</li>
<li>Cell names taken from the first <code>--</code> comment in the query when present.</li>
<li>Markdown context cells for the <code>list_tables</code> and <code>describe_table</code> calls so the agent&#39;s investigation trail stays intact.</li>
</ul>
<p>Results aren&#39;t embedded; opening the notebook re-executes the cells, same as every other Tabularis notebook.</p>
<p>If you want to disable the audit log entirely, setting <code>aiAuditEnabled: false</code> in <code>config.json</code> falls back to the original code path with zero overhead.</p>
<img src="https://tabularis.dev/img/tabularis-ai-audit-log-sessions.png" alt="MCP Activity panel grouped by sessions, with Export as Notebook button" style="width:100%;border-radius:8px;margin:1.5rem 0" />

<hr>
<h2>2. Read-only mode</h2>
<p>The simplest of the three features, configured under <strong>MCP → Safety → Read-only mode</strong>:</p>
<ul>
<li><em>Allow-list of read-only connections</em> (default off, e.g. mark <code>prod</code> as read-only).</li>
<li><em>Allow-list of writable connections</em> (default on, e.g. mark <code>local-sqlite</code> as writable).</li>
</ul>
<p>The classifier strips strings, comments and quoted identifiers before scanning the SQL keyword, and catches CTEs that end in <code>UPDATE</code> / <code>INSERT</code> / <code>DELETE</code>. Anything ambiguous is treated as a write: fail-closed is the safer default when the alternative is a corrupted production table.</p>
<p>Blocked calls land in the audit log with <code>status = blocked_readonly</code> and the agent gets:</p>
<blockquote>
<p>Query blocked by Tabularis read-only mode. Enable writes for this connection in Settings → MCP → Read-only mode.</p>
</blockquote>
<p>Most agents handle this gracefully — they rewrite as a <code>SELECT</code> or surface the error to you.</p>
<hr>
<h2>3. Approval gates with pre-flight EXPLAIN</h2>
<p>Approval gates are the most involved of the three features, and the part that makes giving an agent write access to a real database feel like a sane choice.</p>
<p>When the agent fires a write, Tabularis pauses it and shows an <strong>AI Approval Modal</strong>:</p>
<ul>
<li>The full SQL in a Monaco editor (read-only by default; toggle &quot;Edit before approving&quot; to modify it).</li>
<li>The <strong>execution plan</strong>, rendered with the same Visual Explain component used for ad-hoc EXPLAINs.</li>
<li>An optional reason field. Approve, Deny, or close the modal.</li>
</ul>
<p>The point is that you can see, <em>before any row is touched</em>, that the <code>UPDATE</code> would do a sequential scan over 1.2 million rows. You can fix the WHERE clause, add the right index hint, then approve. The audit log captures both the original and the edited query, linked by an <code>approvalId</code>.</p>
<p>There are three modes: <code>off</code>, <code>writes_only</code> (the default) and <code>all queries</code>. The timeout is configurable (120 s by default). Pre-flight EXPLAIN is best-effort: if it fails (DDL, syntax errors, missing permissions) the modal still opens with an &quot;EXPLAIN unavailable&quot; notice and you can decide anyway.</p>
<hr>
<h2>How approval gates actually work</h2>
<p>The MCP server runs as a separate subprocess. The AI client spawns <code>tabularis --mcp</code> as a child process and the two talk over JSON-RPC 2.0 on stdin/stdout. That subprocess has no Tauri runtime, no <code>AppHandle</code>, and no socket back to the main app.</p>
<p>Asking the user to approve a write across that boundary needs some kind of channel. Three options were on the table:</p>
<ol>
<li><strong>A real RPC channel</strong> between the MCP subprocess and the main Tabularis app. Workable, but it means teaching the MCP binary to discover the running app, open a Unix socket or named pipe, handle disconnect and reconnect, deal with ports on Windows, and so on. A lot of moving parts for something fragile.</li>
<li><strong>Desktop notifications</strong> from the OS. Quick to implement, but a desktop notification can&#39;t render a Visual Explain plan, which would defeat half the point of the feature.</li>
<li><strong>A file queue.</strong> Both processes touch the same directory: the MCP server writes a request file and polls for a response file, while the Tabularis app uses <code>notify</code> (the inotify/FSEvents/ReadDirectoryChangesW crate) to watch the directory and pops up the modal as soon as a file appears.</li>
</ol>
<p>Option 3 turned out to be the best fit. The directory looks like this:</p>
<pre><code>~/.config/tabularis/pending_approvals/
  ├── {uuid}.pending.json    ← MCP server writes
  └── {uuid}.decision.json   ← Tabularis app writes
</code></pre>
<p><code>pending.json</code> carries the full payload: query, classifier kind, connection, EXPLAIN plan as JSON, and the agent&#39;s <code>clientInfo.name</code>. <code>decision.json</code> carries the verdict (<code>approve</code> or <code>deny</code>), an optional <code>reason</code>, and an optional <code>editedQuery</code> if the user touched the SQL before approving.</p>
<p>The MCP server polls every 500 ms. The Tabularis app&#39;s file watcher fires the modal almost instantly. A periodic janitor (every 60 s) wipes anything older than an hour, so the directory never grows.</p>
<p>The whole thing is roughly 200 lines of Rust, with no IPC framework involved. It also works if you launch the agent before opening Tabularis: the request queues in the directory and the modal handles it the moment the app comes up. If Tabularis stays closed for the entire timeout (120 s by default), the call returns a clear error to the agent telling it to start the app first.</p>
<p>A nice side effect of the file queue is that the flow is testable end-to-end without an MCP client. Drop a <code>pending.json</code> with a fake payload into the directory, watch the modal pop up, click Approve, and a <code>decision.json</code> appears. No mocking required.</p>
<hr>
<h2>Bonuses</h2>
<p><strong>Open in Visual Explain.</strong> Every <code>run_query</code> row in the AI Activity panel has a one-click jump into the same Visual Explain modal that the query editor uses. It opens with the query and connection pre-loaded, runs <code>EXPLAIN</code>, and shows you the plan. Handy when a slow query shows up in the log and you want to know why.</p>
<p><strong>Export Session as Notebook.</strong> Already covered above, but worth repeating: this is how an otherwise opaque AI conversation turns into something a human can review, diff and re-run. Attach the notebook to a PR, share it with a colleague, archive it alongside the ticket.</p>
<hr>
<h2>Defaults</h2>
<p>After this upgrade:</p>
<table>
<thead>
<tr>
<th>Setting</th>
<th>Default</th>
</tr>
</thead>
<tbody><tr>
<td><code>aiAuditEnabled</code></td>
<td><code>true</code></td>
</tr>
<tr>
<td><code>aiAuditMaxEntries</code></td>
<td><code>5000</code></td>
</tr>
<tr>
<td><code>aiSessionGapMinutes</code></td>
<td><code>10</code></td>
</tr>
<tr>
<td><code>mcpReadonlyDefault</code></td>
<td><code>false</code></td>
</tr>
<tr>
<td><code>mcpReadonlyConnections</code></td>
<td><code>[]</code></td>
</tr>
<tr>
<td><code>mcpApprovalMode</code></td>
<td><code>writes_only</code></td>
</tr>
<tr>
<td><code>mcpApprovalTimeoutSeconds</code></td>
<td><code>120</code></td>
</tr>
<tr>
<td><code>mcpPreflightExplain</code></td>
<td><code>true</code></td>
</tr>
</tbody></table>
<p>Audit on, approval on <code>writes_only</code>, pre-flight EXPLAIN on. The first time the agent tries to write after upgrading, the modal will pop up. <code>SELECT</code>s go through without any friction.</p>
<p>To keep the previous behaviour wholesale, set <code>aiAuditEnabled = false</code> and <code>mcpApprovalMode = &quot;off&quot;</code> in <code>config.json</code> (or do the same from the <strong>MCP</strong> page in the app).</p>
<hr>
<h2>Where to read more</h2>
<ul>
<li>Wiki: <a href="https://tabularis.dev/wiki/ai-audit-log">AI Audit Log</a> · <a href="https://tabularis.dev/wiki/mcp-readonly-mode">Read-only Mode</a> · <a href="https://tabularis.dev/wiki/mcp-approval-gates">Approval Gates</a> · <a href="https://tabularis.dev/wiki/mcp-server">MCP Server</a></li>
</ul>
<p>None of this changes anything from the agent&#39;s point of view; it sees the same MCP server with the same tools. What changes is on the human side. The MCP page in the app (plug icon in the sidebar) is now organised into three tabs (<strong>Setup</strong>, <strong>Activity</strong>, <strong>Safety</strong>), and a modal will show up the next time the agent reaches for the database with anything sharper than a <code>SELECT</code>.</p>
<hr>
<h2>Summary</h2>
<table>
<thead>
<tr>
<th>Area</th>
<th>What&#39;s new</th>
</tr>
</thead>
<tbody><tr>
<td>AI Activity</td>
<td>New <strong>MCP → Activity</strong> tab with Events + Sessions sub-tabs</td>
</tr>
<tr>
<td>AI Activity</td>
<td>Local JSONL audit log of every MCP tool call (5,000-entry rotation × 5 archives)</td>
</tr>
<tr>
<td>AI Activity</td>
<td>One-click &quot;Export as Notebook&quot; per session</td>
</tr>
<tr>
<td>AI Activity</td>
<td>&quot;Open in Visual Explain&quot; on every <code>run_query</code> row</td>
</tr>
<tr>
<td>MCP</td>
<td>Read-only mode — global default + per-connection override list</td>
</tr>
<tr>
<td>MCP</td>
<td>Approval gates — three modes (<code>off</code> / <code>writes_only</code> / <code>all</code>)</td>
</tr>
<tr>
<td>MCP</td>
<td>Pre-flight EXPLAIN inside the approval modal</td>
</tr>
<tr>
<td>MCP</td>
<td>Edit-before-approving — modify the SQL before it executes</td>
</tr>
<tr>
<td>Architecture</td>
<td>File-queue IPC between the MCP subprocess and the Tabularis app — no socket needed</td>
</tr>
</tbody></table>
<hr>
<h2>Thanks</h2>
<p>A safety release is the kind of work that lives or dies on the questions people ask before merging, and on the bug reports that come back the same day a build ships. Thanks to everyone who tested the modal flows, pointed at edge cases in the read-only classifier, and helped shape what <code>writes_only</code> should actually mean in practice.</p>
<hr>
<p><em>v0.10.0 is available now. Update via the in-app updater, or download from the <a href="https://github.com/TabularisDB/tabularis/releases/tag/v0.10.0">releases page</a>.</em></p>
]]></content:encoded>
      <enclosure url="https://tabularis.dev/blog/v0100-ai-safety-audit-approval/opengraph-image.png" type="image/png" />
      <category>release</category>
      <category>mcp</category>
      <category>ai</category>
      <category>safety</category>
      <category>audit</category>
    </item>
    <item>
      <title>SQL Server Driver: Looking for Contributors</title>
      <link>https://tabularis.dev/blog/sql-server-looking-for-contributors</link>
      <guid isPermaLink="true">https://tabularis.dev/blog/sql-server-looking-for-contributors</guid>
      <pubDate>Thu, 23 Apr 2026 12:00:00 GMT</pubDate>
      <description>Native Microsoft SQL Server as a built-in driver, now on the feat/sql-server branch as a read-only preview. Phase 2 is open and needs contributors for editing, TLS options and composite primary keys.</description>
      <content:encoded><![CDATA[<h1>SQL Server Driver: Looking for Contributors</h1>
<p>SQL Server will be a built-in driver. Not a plugin — peer to MySQL, PostgreSQL, SQLite, registered in the same <code>lib.rs</code>, served out of the same <code>pool_manager</code>, showing up in the connection modal with nothing to install first. Code lives on <a href="https://github.com/TabularisDB/tabularis/tree/feat/sql-server"><code>feat/sql-server</code></a> today. Not on <code>main</code>. Not in any released build. Phase 1 on the branch is read-only browsing + query execution. Phase 2 — editing, TLS, composite primary keys — is six GitHub issues away. Help close it and the whole branch squashes back into <code>main</code>. Full scope, issue list and architecture notes are on the <a href="https://tabularis.dev/roadmap/sql-server/">roadmap page</a>.</p>
<h2>Why built-in instead of a plugin</h2>
<p>Tabularis has a plugin system. DuckDB, Google Sheets, Redis, and others reach the app through it, talking JSON-RPC over stdin/stdout. The system is stable and deliberately simple. It was still the wrong choice for SQL Server, for four reasons that matter in practice:</p>
<p><strong>Streaming latency.</strong> Plugin drivers serialise every row through a JSON-RPC frame. On a 100k-row result the overhead is visible — for the built-in it&#39;s a <code>Vec&lt;Row&gt;</code> hand-off inside the host process.</p>
<p><strong>Capability flags.</strong> The ER diagram&#39;s batch snapshot, the pager, the explain tree all branch on <code>DriverCapabilities</code>. Built-ins set those natively from the <code>DatabaseDriver</code> trait; plugins translate them through a JSON manifest, which drifts and needs to be kept in sync.</p>
<p><strong>Credential and pool reuse.</strong> SSH tunnels, the keychain-backed credential cache, and the health pinger hold <code>Arc&lt;T&gt;</code> state inside the host binary. A plugin driver re-implements what it needs from that stack; a built-in shares one pool manager.</p>
<p><strong>Install step.</strong> The plugin manager exists and works. Expecting a user to visit it for SQL Server specifically — day zero, from a fresh install — is the wrong default.</p>
<p>Costs: <code>~2.5 MB</code> on the release binary (tiberius + deadpool + tokio-util compat layer), and the driver ships on the main release cadence instead of its own. We took those over the alternative.</p>
<h2>What Phase 1 actually does</h2>
<p>The driver lives under <code>src-tauri/src/drivers/sqlserver/</code>. It is <code>readonly: true</code> in its manifest — the UI honours that flag automatically and hides INSERT/UPDATE/DELETE controls — so users can browse and query without ever putting data at risk.</p>
<p>Concretely:</p>
<ul>
<li>Connect over SQL authentication; <code>sys.schemas</code> filtered against role schemas for the tree</li>
<li>Table, view, routine discovery; column / PK / FK / index introspection</li>
<li><code>execute_query</code> streaming over <code>tiberius::Client::query</code></li>
<li>Pagination via a new <code>PaginationDialect</code> enum in <code>drivers/common/query.rs</code> — the legacy <code>build_paginated_query(q, ps, p)</code> signature still produces the same MySQL/PG/SQLite <code>LIMIT/OFFSET</code> output it always has. The SQL Server branch synthesises <code>ORDER BY (SELECT NULL)</code> when the caller query has no top-level <code>ORDER BY</code>, using a paren-depth-aware matcher (documented false positives on string literals, accepted trade-off)</li>
<li>Type extraction dispatched off <code>tiberius::ColumnType</code>: int family, float family, <code>Decimal</code> with <code>Numeric</code> fallback for NUMERIC(38), <code>Uuid</code>, chrono temporals incl. <code>datetimeoffset</code>, <code>varbinary</code> → base64, <code>xml</code>, <code>sql_variant</code></li>
<li>Runtime version detection from <code>SERVERPROPERTY(&#39;ProductMajorVersion&#39;)</code>, cached per pool. Default major = 14 (2017) when parsing fails. <code>supports_offset_fetch</code> gates on ≥ 11, <code>supports_string_agg</code> on ≥ 14</li>
<li>Batch endpoints for the ER diagram: <code>get_all_columns_batch</code>, <code>get_all_foreign_keys_batch</code>, <code>get_schema_snapshot</code></li>
</ul>
<p>The CI number: 471 Rust tests, 0 regressions on the existing MySQL/PostgreSQL/SQLite drivers. Every pure helper — identifier quoting, decimal normalization, query builders, the SQL-string constants themselves — ships with co-located <code>#[cfg(test)] mod tests</code>.</p>
<h2>Phase 2 — six issues</h2>
<p>Phase 1 was the part with the unknowns: whether <code>tiberius</code> 0.12 composes with the current tokio version, whether a non-sqlx pool can sit next to the sqlx ones in <code>pool_manager</code>, whether the <code>DatabaseDriver</code> trait generalises to a driver that doesn&#39;t speak <code>LIMIT/OFFSET</code>. Answers came out yes, yes, yes. Those risks are gone.</p>
<p>What&#39;s left is scoped and mostly independent. The epic is <a href="https://github.com/TabularisDB/tabularis/issues/150">#150</a>; the six sub-issues:</p>
<ul>
<li><a href="https://github.com/TabularisDB/tabularis/issues/144">#144</a> — <code>ConnectionParams</code> extension (<code>trust_server_certificate</code>, <code>encrypt</code>, <code>instance_name</code>, <code>domain</code>, <code>auth_mode</code>). All <code>Option&lt;T&gt;</code> with <code>#[serde(default)]</code> so old saved connections deserialize untouched. Labelled <code>good first issue</code></li>
<li><a href="https://github.com/TabularisDB/tabularis/issues/145">#145</a> — <code>delete_record_composite</code> / <code>update_record_composite</code> as default methods on the trait, forwarding to the legacy single-key path when <code>pk_cols.len() == 1</code>. No change to the other three drivers</li>
<li><a href="https://github.com/TabularisDB/tabularis/issues/146">#146</a> — FK aggregation: <code>STRING_AGG(…) WITHIN GROUP (ORDER BY constraint_column_id)</code> on 2017+ servers, <code>FOR XML PATH(&#39;&#39;)</code> fallback for 2012–2016</li>
<li><a href="https://github.com/TabularisDB/tabularis/issues/147">#147</a> — <code>IDENTITY_INSERT ON/…/OFF</code> wrapper inside an explicit transaction, triggered when the insert data contains a value for the IDENTITY column</li>
<li><a href="https://github.com/TabularisDB/tabularis/issues/148">#148</a> — frontend: <code>pkColumns?: string[]</code> on <code>DataGrid</code>, composite detection in <code>Editor.tsx</code>, aggregate-by-constraint-name in <code>SchemaDiagram.tsx</code>. Depends on #145 and #146</li>
<li><a href="https://github.com/TabularisDB/tabularis/issues/149">#149</a> — flip <code>readonly: false</code>, <code>manage_tables: true</code>. Closes Phase 2</li>
</ul>
<p>Everything — architecture, module layout, type coverage, dependencies between issues, local setup — is on the <a href="https://tabularis.dev/roadmap/">roadmap page</a>.</p>
<h2>Ground rules</h2>
<p>Three invariants get checked at review, they&#39;re not negotiable:</p>
<p>No GPL-licensed code copied from other open source SQL clients. The driver is written against Microsoft TDS / T-SQL docs and observable server behaviour; Tabularis stays Apache-2.0.</p>
<p>New struct fields are <code>Option&lt;T&gt;</code> / <code>Vec&lt;T&gt;</code> with <code>#[serde(default)]</code> + <code>skip_serializing_if</code>. Saved connections from previous releases deserialize untouched, and the MySQL / Postgres / SQLite drivers stay byte-identical — <code>cargo test --lib</code> catches regressions before the PR lands.</p>
<p>Every pure helper ships with <code>#[cfg(test)] mod tests</code> in the same PR. Happy path plus at least one edge case. SQL-string constants count as pure helpers — assert the query contains the expected <code>sys.*</code> / <code>INFORMATION_SCHEMA.*</code> tables and the right <code>@P1</code> / <code>@P2</code> placeholders.</p>
<h2>If you want in</h2>
<ul>
<li>Rust, backend: <a href="https://github.com/TabularisDB/tabularis/issues/144">#144</a> (good first issue), <a href="https://github.com/TabularisDB/tabularis/issues/145">#145</a>, <a href="https://github.com/TabularisDB/tabularis/issues/146">#146</a>, <a href="https://github.com/TabularisDB/tabularis/issues/147">#147</a></li>
<li>TypeScript / React: <a href="https://github.com/TabularisDB/tabularis/issues/148">#148</a> — DataGrid + Editor + ER diagram composite-PK support</li>
<li>Just testing: spin up <code>mcr.microsoft.com/mssql/server:2022-latest</code> against your own schema, file issues for whatever doesn&#39;t match</li>
<li>Architecture input: the epic <a href="https://github.com/TabularisDB/tabularis/issues/150">#150</a> is the right thread</li>
</ul>
<p>Full architecture reference, module layout, type-extraction details, local setup: <a href="https://tabularis.dev/roadmap/">roadmap page</a>.</p>
<p>Phase 2 will land either way. It lands sooner, and better, with a couple more people on it.</p>
]]></content:encoded>
      <enclosure url="https://tabularis.dev/blog/sql-server-looking-for-contributors/opengraph-image.png" type="image/png" />
      <category>sql-server</category>
      <category>roadmap</category>
      <category>contribute</category>
      <category>rust</category>
    </item>
    <item>
      <title>Your First Tabularis Driver in 20 Minutes: Google Sheets, Step by Step</title>
      <link>https://tabularis.dev/blog/google-sheets-driver-tutorial</link>
      <guid isPermaLink="true">https://tabularis.dev/blog/google-sheets-driver-tutorial</guid>
      <pubDate>Tue, 21 Apr 2026 12:00:00 GMT</pubDate>
      <description>A hands-on walkthrough of @tabularis/create-plugin and @tabularis/plugin-api — from an empty directory to a working Google Sheets driver with OAuth, custom connection form, and sheets-as-SQL tables.</description>
      <content:encoded><![CDATA[<h1>Your First Tabularis Driver in 20 Minutes: Google Sheets, Step by Step</h1>
<p>Tabularis&#39; plugin system has had three missing pieces since <a href="https://tabularis.dev/posts/plugin-ecosystem">v0.9.0</a> launched it:</p>
<ol>
<li>A <strong>published npm package</strong> with the plugin UI types — so authors stop copying type definitions from the host repo by hand.</li>
<li>A <strong>scaffolder CLI</strong> — so nobody has to write 33 JSON-RPC stubs, a cross-platform release workflow, and a manifest against a 230-line JSON schema from scratch.</li>
<li>An <strong>actual tutorial</strong> — not a reference; something you can follow top to bottom and end with a working driver.</li>
</ol>
<p>The first two shipped as <a href="https://www.npmjs.com/package/@tabularis/plugin-api"><code>@tabularis/plugin-api</code></a> and <a href="https://www.npmjs.com/package/@tabularis/create-plugin"><code>@tabularis/create-plugin</code></a>. This post is the third.</p>
<p>I wrote it while scaffolding a <strong>Google Sheets</strong> driver from zero, so every command is one I actually ran. The final plugin lives at <a href="https://github.com/TabularisDB/tabularis-google-sheets-plugin"><code>tabularis-google-sheets-plugin</code></a> — clone it if you want the finished state to diff against.</p>
<p><strong>What you&#39;ll end with:</strong> Google Sheets shows up in Tabularis&#39; driver picker. Authenticate once with OAuth. Paste a spreadsheet URL. Sidebar lists every tab as a table. Run <code>SELECT * FROM &quot;Sheet1&quot; LIMIT 5</code> and get rows.</p>
<hr>
<h2>Why Google Sheets</h2>
<p>Two reasons.</p>
<p><strong>It&#39;s not a database.</strong> Real drivers don&#39;t wrap RDBMSs exclusively — a registry plugin can expose anything queryable as SQL. Hacker News (<a href="https://tabularis.dev/posts/hackernews-plugin">posted here</a>) exposes the HN Firebase API. A CSV-folder plugin exposes a directory. Google Sheets is another point on that axis: a row-oriented data source where each tab is a table and the first row is the header. No host, no port, no password — just OAuth.</p>
<p><strong>It exercises two UI extension slots.</strong> The tutorial walks through both. Most plugins touch zero slots; some touch one. Two is the point at which the scaffolder&#39;s <code>--with-ui</code> defaults stop fitting and you learn how the IIFE loader actually works.</p>
<hr>
<h2>1. Scaffold</h2>
<pre><code class="language-bash">npm create @tabularis/plugin@latest -- \
  --db-type=api \
  --dir ~/Progetti/google-sheets \
  google-sheets
</code></pre>
<p>Three flags matter:</p>
<ul>
<li><strong><code>--db-type=api</code></strong> — Google Sheets has no host/port/user/pass. The scaffolder sets <code>no_connection_required: true</code> in <code>manifest.json</code> and leaves the default ports null.</li>
<li><strong><code>--dir</code></strong> — scaffold outside your normal cwd so you can keep a &quot;before/after&quot; next to it.</li>
<li><strong><code>google-sheets</code></strong> — the plugin id. Used for the crate name, the binary, the manifest <code>id</code>, and the install path (<code>~/.local/share/tabularis/plugins/google-sheets/</code> on Linux).</li>
</ul>
<p>Ten seconds later, <code>~/Progetti/google-sheets/</code> contains:</p>
<pre><code>google-sheets/
├── Cargo.toml
├── manifest.json           # metadata + UI extensions + data types
├── justfile                # build / install / test recipes
├── rust-toolchain.toml
├── .github/workflows/release.yml    # 5-platform matrix for v* tags
└── src/
    ├── main.rs             # JSON-RPC stdin/stdout loop
    ├── rpc.rs              # dispatch → handlers/
    ├── handlers/{metadata,query,crud,ddl}.rs
    ├── utils/{identifiers,pagination}.rs    # tested helpers
    ├── client.rs           # scaffold leftover (delete later)
    ├── error.rs            # scaffold leftover
    ├── models.rs           # scaffold leftover
    └── bin/test_plugin.rs  # local REPL
</code></pre>
<p>Every handler returns something valid:</p>
<ul>
<li>Metadata methods return empty arrays — the plugin <strong>loads</strong> in Tabularis without errors.</li>
<li><code>test_connection</code> returns <code>{&quot;success&quot;: true}</code> hard-coded — the driver <strong>appears in the picker</strong> immediately after <code>just dev-install</code>.</li>
<li>Query/CRUD/DDL methods return <code>-32601 method not implemented</code> — you haven&#39;t implemented them yet, and the host surfaces a clean error rather than crashing.</li>
</ul>
<p>This matters. A newcomer to any plugin system needs to see their driver in the UI before writing a single line of real logic. &quot;Empty but alive&quot; is the right default.</p>
<pre><code class="language-bash">cd ~/Progetti/google-sheets
cargo check  # should be green in seconds
</code></pre>
<hr>
<h2>2. Declare the driver</h2>
<p>Open <code>manifest.json</code>. The scaffold gives you the right structural defaults for <code>--db-type=api</code>. You need to add three things: the settings, the UI extensions, and the data types.</p>
<p><strong>Settings</strong> — five fields the plugin persists across restarts. The user never edits these; the OAuth wizard (step 6) writes them:</p>
<pre><code class="language-json">&quot;settings&quot;: [
  { &quot;key&quot;: &quot;client_id&quot;,     &quot;label&quot;: &quot;OAuth Client ID&quot;,     &quot;type&quot;: &quot;string&quot; },
  { &quot;key&quot;: &quot;client_secret&quot;, &quot;label&quot;: &quot;OAuth Client Secret&quot;, &quot;type&quot;: &quot;string&quot; },
  { &quot;key&quot;: &quot;access_token&quot;,  &quot;label&quot;: &quot;Access Token&quot;,        &quot;type&quot;: &quot;string&quot; },
  { &quot;key&quot;: &quot;refresh_token&quot;, &quot;label&quot;: &quot;Refresh Token&quot;,       &quot;type&quot;: &quot;string&quot; },
  { &quot;key&quot;: &quot;token_expiry&quot;,  &quot;label&quot;: &quot;Token Expiry&quot;,        &quot;type&quot;: &quot;number&quot; }
]
</code></pre>
<p><strong>UI extensions</strong> — two slots. <code>module</code> paths point to the IIFE bundles Vite will produce in step 6:</p>
<pre><code class="language-json">&quot;ui_extensions&quot;: [
  { &quot;slot&quot;: &quot;settings.plugin.before_settings&quot;,
    &quot;module&quot;: &quot;ui/dist/google-auth.js&quot;, &quot;order&quot;: 10 },
  { &quot;slot&quot;: &quot;connection-modal.connection_content&quot;,
    &quot;module&quot;: &quot;ui/dist/google-sheets-db-field.js&quot;, &quot;order&quot;: 10,
    &quot;driver&quot;: &quot;google-sheets&quot; }
]
</code></pre>
<p><code>settings.plugin.before_settings</code> mounts a component <strong>above</strong> the settings form of this plugin — perfect for an OAuth setup wizard. <code>connection-modal.connection_content</code> replaces the default host/port/user/pass form in the &quot;new connection&quot; modal with a custom layout — we need this because a Google Sheets connection has <strong>one field</strong> (spreadsheet id or URL) and none of the usual ones.</p>
<p><strong>Data types</strong> — the three Sheets uses. <code>infer_type</code> in <code>src/sheets.rs</code> will pick one of these per column when the user opens a table:</p>
<pre><code class="language-json">&quot;data_types&quot;: [
  { &quot;name&quot;: &quot;TEXT&quot;,    &quot;category&quot;: &quot;string&quot;,  &quot;requires_length&quot;: false, &quot;requires_precision&quot;: false },
  { &quot;name&quot;: &quot;INTEGER&quot;, &quot;category&quot;: &quot;numeric&quot;, &quot;requires_length&quot;: false, &quot;requires_precision&quot;: false },
  { &quot;name&quot;: &quot;REAL&quot;,    &quot;category&quot;: &quot;numeric&quot;, &quot;requires_length&quot;: false, &quot;requires_precision&quot;: false }
]
</code></pre>
<hr>
<h2>3. Three helper modules</h2>
<p>Google Sheets is <strong>an API call away</strong> — the heavy lifting lives in three small Rust modules you drop into <code>src/</code>. They&#39;re not generated by the scaffolder because they&#39;re Sheets-specific; everything in <code>src/handlers/</code> routes through them.</p>
<p><strong><code>src/auth.rs</code></strong> — a module-level <code>Mutex&lt;AuthState&gt;</code> holding OAuth tokens. Exposes <code>access_token(&amp;client) -&gt; Result&lt;String&gt;</code> that transparently refreshes via <code>https://oauth2.googleapis.com/token</code> if the cached token is expired. ~110 lines. The <code>initialize</code> RPC (step 5) pushes saved settings into this state.</p>
<p><strong><code>src/sheets.rs</code></strong> — a blocking <code>reqwest</code> client for the Sheets REST API. The public surface is thin:</p>
<pre><code class="language-rust">pub fn get_sheet_names(spreadsheet_id: &amp;str) -&gt; Result&lt;Vec&lt;String&gt;&gt;
pub fn get_sheet_data(spreadsheet_id: &amp;str, sheet_name: &amp;str) -&gt; Result&lt;(Vec&lt;String&gt;, Vec&lt;Vec&lt;Value&gt;&gt;)&gt;
pub fn append_row(spreadsheet_id: &amp;str, sheet_name: &amp;str, row: Vec&lt;String&gt;) -&gt; Result&lt;()&gt;
pub fn update_cell(spreadsheet_id: &amp;str, sheet_name: &amp;str, col: &amp;str, row: usize, value: &amp;str) -&gt; Result&lt;()&gt;
pub fn delete_row(spreadsheet_id: &amp;str, sheet_id: i64, row: usize) -&gt; Result&lt;()&gt;
pub fn infer_type(values: &amp;[Value]) -&gt; &amp;&#39;static str    // TEXT | INTEGER | REAL
pub fn extract_spreadsheet_id(raw: &amp;str) -&gt; &amp;str       // accepts full URL or bare id
</code></pre>
<p>Every call goes through <code>auth::access_token()</code>. No service accounts — OAuth2 desktop flow only. ~300 lines.</p>
<p><strong><code>src/sql.rs</code></strong> — a regex-based parser for the subset of SQL the driver handles:</p>
<pre><code class="language-rust">pub enum Query { Select(...), Insert(...), Update(...), Delete(...) }
pub fn parse(raw: &amp;str) -&gt; Result&lt;Query&gt;
pub fn eval_where(where_clause: &amp;str, row: &amp;HashMap&lt;String, String&gt;) -&gt; bool
pub fn extract_row_num(where_clause: &amp;str) -&gt; Result&lt;usize&gt;  // for UPDATE/DELETE &quot;WHERE _row = N&quot;
</code></pre>
<p><strong>Don&#39;t write this by hand.</strong> It supports <code>SELECT</code>, <code>INSERT</code>, <code>UPDATE WHERE _row = N</code>, <code>DELETE WHERE _row = N</code>, <code>COUNT(*)</code>, plus basic <code>WHERE</code> with <code>AND</code>/<code>LIKE</code>/<code>=</code>/<code>&gt;</code>/etc. Nothing fancy. Copy from the <a href="https://github.com/TabularisDB/tabularis-google-sheets-plugin/blob/main/src/sql.rs">companion repo</a> — it&#39;s 320 lines of compiled regexes and string slicing. Replace with <a href="https://crates.io/crates/sqlparser"><code>sqlparser</code></a> when you care about joins and subqueries.</p>
<p>Add the dependencies to <code>Cargo.toml</code>:</p>
<pre><code class="language-toml">anyhow = &quot;1&quot;
serde = { version = &quot;1&quot;, features = [&quot;derive&quot;] }
serde_json = &quot;1&quot;
reqwest = { version = &quot;0.12&quot;, features = [&quot;blocking&quot;, &quot;json&quot;] }
regex = &quot;1&quot;
</code></pre>
<p>And register the modules in <code>src/main.rs</code>:</p>
<pre><code class="language-rust">mod auth;
mod handlers;
mod rpc;
mod sheets;
mod sql;
// ...plus the scaffold leftovers for now
</code></pre>
<hr>
<h2>4. Metadata — make the sidebar come alive</h2>
<p><code>src/handlers/metadata.rs</code> starts with every method returning an empty array. Three of them need real data.</p>
<p><strong><code>get_databases</code></strong> — one &quot;database&quot; per connection: the spreadsheet id extracted from the <code>database</code> field of the connection form. The host calls this when opening the connection picker — it drives what shows up in the sidebar as the top-level node.</p>
<pre><code class="language-rust">pub fn get_databases(id: Value, params: &amp;Value) -&gt; Value {
    match spreadsheet_id(&amp;id, params) {
        Ok(sid) =&gt; ok_response(id, json!([sid])),
        Err(resp) =&gt; resp,
    }
}
</code></pre>
<p><strong><code>get_tables</code></strong> — each sheet tab becomes a table. <code>get_sheet_names</code> calls <code>GET /v4/spreadsheets/{id}</code>, reads <code>sheets[].properties.title</code>, returns a list.</p>
<pre><code class="language-rust">pub fn get_tables(id: Value, params: &amp;Value) -&gt; Value {
    let sid = match spreadsheet_id(&amp;id, params) { Ok(s) =&gt; s, Err(resp) =&gt; return resp };
    match get_sheet_names(&amp;sid) {
        Ok(names) =&gt; {
            let tables: Vec&lt;Value&gt; = names.into_iter()
                .map(|n| json!({ &quot;name&quot;: n, &quot;schema&quot;: null, &quot;comment&quot;: null }))
                .collect();
            ok_response(id, json!(tables))
        }
        Err(e) =&gt; error_response(id, -32000, &amp;e.to_string()),
    }
}
</code></pre>
<p><strong><code>get_columns</code></strong> — read row 1 as headers, sample rows 2..102, infer each column&#39;s type. Prepend a synthetic <code>_row INTEGER PRIMARY KEY</code> — this is what UPDATE/DELETE will <code>WHERE</code> on (the Sheets API indexes by position, there&#39;s no surrogate key).</p>
<p>Fill in <code>get_schema_snapshot</code> (for the ER diagram) and <code>get_all_columns_batch</code> (batch fetch at connection load) with the same pattern. Everything else (<code>get_foreign_keys</code>, <code>get_indexes</code>, <code>get_views</code>, routines) stays empty. Google Sheets has no such concepts; returning empty is the <strong>correct</strong> answer, not a stub.</p>
<p><strong>Checkpoint.</strong> <code>cargo check</code>. If it compiles, the driver will light up the sidebar when installed. Save yourself some time and keep a second terminal open with <code>cargo check --all-targets</code> on <code>fswatch</code> — the scaffold&#39;s <code>rust-toolchain.toml</code> pins a stable channel so you won&#39;t hit nightly incompatibilities.</p>
<hr>
<h2>5. Initialize and execute</h2>
<p>Two more handler files.</p>
<h3><code>src/handlers/init.rs</code></h3>
<p>The scaffold&#39;s default <code>initialize</code> returns <code>null</code> — fine for simple plugins, not fine here. The host sends <code>params.settings</code> containing whatever we saved via the UI extension (client_id, tokens, etc.), and we need to push those into the <code>auth</code> module:</p>
<pre><code class="language-rust">pub fn initialize(id: Value, params: &amp;Value) -&gt; Value {
    let settings = params.get(&quot;settings&quot;).cloned().unwrap_or(Value::Null);
    let mut state = auth().lock().unwrap();
    *state = AuthState::default();
    state.oauth_client_id     = string_setting(&amp;settings, &quot;client_id&quot;);
    state.oauth_client_secret = string_setting(&amp;settings, &quot;client_secret&quot;);
    state.oauth_access_token  = string_setting(&amp;settings, &quot;access_token&quot;);
    state.oauth_refresh_token = string_setting(&amp;settings, &quot;refresh_token&quot;);
    state.oauth_token_expiry  = settings.get(&quot;token_expiry&quot;).and_then(Value::as_u64);
    ok_response(id, Value::Null)
}
</code></pre>
<p>Register it in <code>src/handlers/mod.rs</code> (<code>pub mod init;</code>) and in <code>src/rpc.rs</code>:</p>
<pre><code class="language-rust">&quot;initialize&quot; =&gt; handlers::init::initialize(id, &amp;params),
</code></pre>
<h3><code>src/handlers/query.rs</code></h3>
<p>Replace the scaffold&#39;s hard-coded <code>test_connection</code> with a real check, then implement <code>execute_query</code> by dispatching on the parsed query:</p>
<pre><code class="language-rust">match parsed {
    Query::Select(sel) =&gt; run_select(id, &amp;sid, sel, page, page_size, t0),
    Query::Insert(ins) =&gt; { /* fetch headers, build row in column order, sheets::append_row */ }
    Query::Update(upd) =&gt; { /* extract _row from WHERE, sheets::update_cell per SET entry */ }
    Query::Delete(del) =&gt; { /* extract _row from WHERE, sheets::delete_row */ }
}
</code></pre>
<p><code>run_select</code> is the biggest function (~80 lines): fetches the sheet, prepends a synthetic <code>_row</code> column to every row, applies <code>sql::eval_where</code> in-memory, handles <code>COUNT(*)</code>, applies LIMIT/OFFSET, projects columns. Copy it from the <a href="https://github.com/TabularisDB/tabularis-google-sheets-plugin/blob/main/src/handlers/query.rs">companion repo&#39;s <code>handlers/query.rs</code></a>.</p>
<p>Fill in <code>handlers/crud.rs</code> (insert/update/delete via <code>_row</code> primary key) and <code>handlers/ddl.rs</code> (<code>get_create_table_sql</code> reflects types inferred from row samples; every other DDL method returns <code>-32601</code> with a <strong>clear</strong> message like <code>&quot;Google Sheets does not support indexes.&quot;</code> Users see these messages in the UI — ambiguity costs them a trip to GitHub issues).</p>
<p><strong>Checkpoint.</strong></p>
<pre><code class="language-bash">cargo build --release
</code></pre>
<p>30–60 seconds. The binary is at <code>target/release/google-sheets-plugin</code>. It&#39;s ~3 MB thanks to the scaffold&#39;s <code>[profile.release]</code> with <code>lto</code>, <code>codegen-units = 1</code>, <code>strip = &quot;symbols&quot;</code>.</p>
<hr>
<h2>6. UI extensions, the typed way</h2>
<p>Tabularis loads plugin UI as <strong>IIFE bundles</strong> — self-contained <code>.js</code> files assigning a React component to <code>__tabularis_plugin__</code>. You can hand-write raw IIFE and drop it in, or — the point of this whole exercise — you write TSX, Vite produces the IIFE, and the <a href="https://www.npmjs.com/package/@tabularis/plugin-api"><code>@tabularis/plugin-api</code></a> npm package gives you typed slot contracts and hook signatures.</p>
<p>The scaffold&#39;s <code>--with-ui</code> flag already wired this up for one slot (<code>data-grid.toolbar.actions</code>). We need two slots, so we replace the single-entry Vite config with two configs sharing the same externals and output directory.</p>
<h3>Workspace</h3>
<pre><code>ui/
├── package.json              # @tabularis/plugin-api + react + vite
├── tsconfig.json             # strict mode
├── vite.auth.config.ts       # entry: src/google-auth.tsx
├── vite.db-field.config.ts   # entry: src/google-sheets-db-field.tsx
└── src/
    ├── google-auth.tsx
    ├── google-sheets-db-field.tsx
    └── styles.ts             # shared CSSProperties objects
</code></pre>
<p>Each Vite config is a ~20-line <code>defineConfig</code> with <code>build.lib.entry</code> pointing at one TSX file, <code>formats: [&quot;iife&quot;]</code>, and the critical externals map:</p>
<pre><code class="language-ts">rollupOptions: {
  external: [&quot;react&quot;, &quot;react/jsx-runtime&quot;, &quot;@tabularis/plugin-api&quot;],
  output: {
    globals: {
      react: &quot;React&quot;,
      &quot;react/jsx-runtime&quot;: &quot;ReactJSXRuntime&quot;,
      &quot;@tabularis/plugin-api&quot;: &quot;__TABULARIS_API__&quot;,
    },
  },
},
</code></pre>
<p>That&#39;s the whole protocol contract: <strong>the host injects the globals, the bundle consumes them</strong>. No React gets shipped twice.</p>
<h3>The connection form (<code>src/google-sheets-db-field.tsx</code>)</h3>
<p>Slot: <code>connection-modal.connection_content</code>. When the user picks &quot;Google Sheets&quot; as the driver in the new-connection modal, the host renders this component <strong>in place of</strong> the usual host/port/user/pass grid. One labeled text input for the spreadsheet ID or URL.</p>
<pre><code class="language-tsx">import { defineSlot, type TypedSlotProps } from &quot;@tabularis/plugin-api&quot;;
import { PLUGIN_ID } from &quot;./styles&quot;;

// plugin-api v0.1.0 types this slot&#39;s context as { driver: string }, but the
// host also passes `database` and `onDatabaseChange`. Augment locally until
// the next plugin-api release tightens the shape.
type FieldContext =
  TypedSlotProps&lt;&quot;connection-modal.connection_content&quot;&gt;[&quot;context&quot;]
  &amp; { database?: string; onDatabaseChange?: (value: string) =&gt; void };

const GoogleSheetsDatabaseField = defineSlot(
  &quot;connection-modal.connection_content&quot;,
  ({ context }) =&gt; {
    const c = context as FieldContext;
    if (c.driver !== PLUGIN_ID) return null;  // don&#39;t render for other drivers

    return (
      &lt;div&gt;
        &lt;label&gt;Spreadsheet ID or URL&lt;/label&gt;
        &lt;input
          type=&quot;text&quot;
          value={c.database ?? &quot;&quot;}
          onChange={e =&gt; c.onDatabaseChange?.(e.target.value)}
          placeholder=&quot;https://docs.google.com/spreadsheets/d/…&quot;
        /&gt;
      &lt;/div&gt;
    );
  },
);

export default GoogleSheetsDatabaseField.component;
</code></pre>
<p><code>defineSlot</code> is what the package brings. It wraps your component in a tagged <code>{ __slot, component }</code> and — more importantly — <strong>types <code>context</code> per slot</strong>. If you wrote <code>context.tableName</code> here, TypeScript would refuse to compile: that field exists on <code>data-grid.toolbar.actions</code>, not here. The <code>as FieldContext</code> cast is only needed because this one slot&#39;s types are temporarily too narrow.</p>
<p><code>default export</code> must be the component itself (<code>.component</code>) — the host loader reads that off the IIFE return value.</p>
<h3>The OAuth wizard (<code>src/google-auth.tsx</code>)</h3>
<p>Slot: <code>settings.plugin.before_settings</code>. Renders in the plugin&#39;s row in Settings. Click &quot;Connect with Google&quot; → a two-step modal wizard handles the OAuth dance.</p>
<p>Three plugin-api hooks do the heavy lifting:</p>
<pre><code class="language-tsx">const { getSetting, setSetting, setSettings } = usePluginSetting(PLUGIN_ID);
const { openModal, closeModal }               = usePluginModal();
// plus the standalone `openUrl` helper
</code></pre>
<ul>
<li><strong><code>usePluginSetting(PLUGIN_ID)</code></strong> — typed <code>getSetting&lt;T&gt;</code>, <code>setSetting</code>, <code>setSettings</code> for the five OAuth fields from the manifest. Persists across restarts; the Rust side reads the same keys on <code>initialize</code>.</li>
<li><strong><code>usePluginModal()</code></strong> — host-managed modal. Pass it a React element as <code>content</code> and it portals to <code>document.body</code>. We use it for the wizard&#39;s two steps (credentials → paste redirect URL).</li>
<li><strong><code>openUrl(url)</code></strong> — <code>window.open</code> does not open external URLs in a Tauri webview. <code>openUrl</code> routes through <code>@tauri-apps/plugin-opener</code> and launches the system browser. Always use this for external URLs in plugins.</li>
</ul>
<p>Slot component outline:</p>
<pre><code class="language-tsx">const GoogleSheetsOAuth = defineSlot(
  &quot;settings.plugin.before_settings&quot;,
  ({ context }) =&gt; {
    if (context.targetPluginId !== PLUGIN_ID) return null;  // typed!
    const { getSetting, setSetting, setSettings } = usePluginSetting(PLUGIN_ID);
    const { openModal, closeModal } = usePluginModal();

    const isConnected = !!(getSetting(&quot;refresh_token&quot;) || getSetting(&quot;access_token&quot;));

    return (
      &lt;div className=&quot;google-account-panel&quot;&gt;
        &lt;Header connected={isConnected} /&gt;
        {isConnected
          ? &lt;ConnectedActions onReauth={/* openModal(...) */} onDisconnect={/* setSettings(...) */} /&gt;
          : &lt;ConnectButton onClick={/* openModal(...) */} /&gt;}
      &lt;/div&gt;
    );
  },
);
export default GoogleSheetsOAuth.component;
</code></pre>
<p>Inside the wizard, the exchange is plain <code>fetch</code>:</p>
<pre><code class="language-tsx">async function exchangeCode(clientId, clientSecret, code) {
  const body = new URLSearchParams({
    code, client_id: clientId, client_secret: clientSecret,
    redirect_uri: &quot;http://127.0.0.1&quot;,
    grant_type: &quot;authorization_code&quot;,
  });
  const resp = await fetch(&quot;https://oauth2.googleapis.com/token&quot;, {
    method: &quot;POST&quot;,
    headers: { &quot;Content-Type&quot;: &quot;application/x-www-form-urlencoded&quot; },
    body: body.toString(),
  });
  if (!resp.ok) throw new Error(await resp.text());
  return resp.json();
}
</code></pre>
<p>Complete file (~330 lines with the full wizard UI and styling) at <a href="https://github.com/TabularisDB/tabularis-google-sheets-plugin/blob/main/ui/src/google-auth.tsx"><code>ui/src/google-auth.tsx</code></a>.</p>
<h3>Build</h3>
<pre><code class="language-bash">cd ui
pnpm install
pnpm run typecheck    # strict mode catches slot/context mismatches at build time
pnpm run build        # → dist/google-auth.js (≈8.5 KB) + dist/google-sheets-db-field.js (≈1.2 KB)
</code></pre>
<p>Gzipped, the two bundles add up to ~4 KB — because React and <code>@tabularis/plugin-api</code> are externalised, not bundled.</p>
<hr>
<h2>7. Install and demo</h2>
<p>With <code>just</code> (one command builds Rust + UI, copies everything):</p>
<pre><code class="language-bash">just dev-install
</code></pre>
<p>Without <code>just</code>:</p>
<pre><code class="language-bash">cargo build --release
pnpm --dir ui install &amp;&amp; pnpm --dir ui build

PLUGIN_DIR=&quot;$HOME/.local/share/tabularis/plugins/google-sheets&quot;
mkdir -p &quot;$PLUGIN_DIR/ui/dist&quot;
cp target/release/google-sheets-plugin &quot;$PLUGIN_DIR/&quot;
cp manifest.json &quot;$PLUGIN_DIR/&quot;
cp ui/dist/*.js &quot;$PLUGIN_DIR/ui/dist/&quot;
</code></pre>
<p>Restart Tabularis (or toggle the plugin in <strong>Settings → Plugins</strong> if it was already enabled). Then:</p>
<ol>
<li><strong>Settings → Plugins → Google Sheets → gear icon.</strong> The OAuth wizard renders above the settings form thanks to <code>settings.plugin.before_settings</code>.</li>
<li>Paste Client ID + Client Secret from Google Cloud Console. Click <strong>Open Authorization Page →</strong>. Grant access in the browser, copy the redirect URL, paste it back, click <strong>Save Token</strong>.</li>
<li><strong>New Connection → Driver: Google Sheets.</strong> The whole form collapses to a single &quot;Spreadsheet ID or URL&quot; input thanks to <code>connection-modal.connection_content</code>. Paste a spreadsheet URL.</li>
<li><strong>Connect.</strong> The sidebar lists every tab as a table. Click one: row 1 becomes the column header, rows 2..N the data.</li>
<li>Try it in the editor: <code>SELECT * FROM &quot;Sheet1&quot; LIMIT 5</code>.</li>
</ol>
<p>That&#39;s the full loop.</p>
<hr>
<h2>What this tutorial cut</h2>
<p>The 20-minute budget was honest. These each deserve their own post:</p>
<ul>
<li><strong>Row-level editing in the data grid.</strong> The <code>crud.rs</code> handlers are implemented but not demoed — once <code>capabilities.readonly</code> is <code>false</code> the Tabularis grid offers inline row editing via the <code>_row</code> primary key.</li>
<li><strong>Deep dive on <code>@tabularis/plugin-api</code>.</strong> The tutorial uses <code>defineSlot</code>, <code>usePluginSetting</code>, <code>usePluginModal</code>, and <code>openUrl</code>. The package also ships <code>usePluginQuery</code>, <code>usePluginToast</code>, <code>usePluginTranslation</code>, <code>usePluginTheme</code>, <code>usePluginConnection</code>, and version-compatibility helpers — all typed, all worth a separate post.</li>
<li><strong>Release packaging.</strong> The scaffold&#39;s <code>.github/workflows/release.yml</code> builds for 5 platforms on every <code>v*</code> tag and uploads zipped artifacts ready for the <a href="https://github.com/TabularisDB/tabularis/blob/main/plugins/registry.json">registry</a>.</li>
<li><strong>A real SQL parser.</strong> The 320-line regex parser handles Sheets&#39; needs; it won&#39;t handle joins, CTEs, or window functions. Swap in <a href="https://crates.io/crates/sqlparser"><code>sqlparser</code></a> when your driver grows up.</li>
</ul>
<hr>
<h2>Where to go next</h2>
<ul>
<li><strong><a href="https://github.com/TabularisDB/tabularis/blob/main/plugins/PLUGIN_TUTORIAL.md"><code>plugins/PLUGIN_TUTORIAL.md</code></a></strong> — the canonical, repo-versioned copy of the walkthrough above.</li>
<li><strong><a href="https://github.com/TabularisDB/tabularis/blob/main/plugins/PLUGIN_GUIDE.md"><code>plugins/PLUGIN_GUIDE.md</code></a></strong> — the complete reference. Every RPC method, every manifest field, every UI slot, every capability flag.</li>
<li><strong><a href="https://www.npmjs.com/package/@tabularis/plugin-api"><code>@tabularis/plugin-api</code></a></strong> — TypeScript types for slot contexts and host hooks.</li>
<li><strong><a href="https://www.npmjs.com/package/@tabularis/create-plugin"><code>@tabularis/create-plugin</code></a></strong> — scaffolder CLI source and flags.</li>
<li><strong><a href="https://github.com/TabularisDB/tabularis-google-sheets-plugin"><code>tabularis-google-sheets-plugin</code></a></strong> — the finished plugin from this tutorial. Clone it for the full working state.</li>
<li><strong><a href="https://github.com/TabularisDB/tabularis/blob/main/plugins/registry.json">Plugin registry</a></strong> — eight community drivers with eight different shapes worth copying from.</li>
</ul>
<p>The promise from <a href="https://tabularis.dev/posts/plugin-ecosystem">v0.9.0</a> was that adding a database to Tabularis should not require a patch to the core app. With the scaffolder and the plugin-api package, it now takes about twenty minutes.</p>
<p>Write one. It&#39;s faster than you think.</p>
]]></content:encoded>
      <enclosure url="https://tabularis.dev/blog/google-sheets-driver-tutorial/opengraph-image.png" type="image/png" />
      <category>plugins</category>
      <category>tutorial</category>
      <category>google-sheets</category>
      <category>rust</category>
      <category>oauth</category>
      <category>extensibility</category>
    </item>
    <item>
      <title>v0.9.20: Clipboard Import, Visual EXPLAIN Import, and a Community Look &amp; Feel</title>
      <link>https://tabularis.dev/blog/v0920-clipboard-import-visual-explain-videos</link>
      <guid isPermaLink="true">https://tabularis.dev/blog/v0920-clipboard-import-visual-explain-videos</guid>
      <pubDate>Tue, 21 Apr 2026 12:00:00 GMT</pubDate>
      <description>v0.9.20 is a community-shaped release: clipboard data import lands in the app, Visual EXPLAIN gains import support for existing plans, every built-in theme gets a readability pass, and the website goes from static screenshots to dynamic video demos across the hero, wiki, and feature pages.</description>
      <content:encoded><![CDATA[<h1>v0.9.20: Clipboard Import, Visual EXPLAIN Import, and a Community Look &amp; Feel</h1>
<p><strong>v0.9.20</strong> owes a lot to the community. The two headline app features, <strong>clipboard data import</strong> and <strong>plan import for Visual EXPLAIN</strong>, ship alongside two outside contributions that change how Tabularis feels to use: a readability pass across every built-in theme, and a full visual overhaul of the website with video demos instead of screenshots.</p>
<p>If v0.9.19 was about polish, v0.9.20 is about reach. Getting data into Tabularis, understanding query plans inside it, and figuring out what Tabularis actually is before you install it are all easier in this release.</p>
<hr>
<h2>A New Look &amp; Feel for the Website</h2>
<p>The most visible change in v0.9.20 is not inside the app. It&#39;s on <a href="https://tabularis.dev">tabularis.dev</a> itself, and it comes from <a href="https://github.com/Nako0">@Nako0</a> in PR <a href="https://github.com/TabularisDB/tabularis/pull/142">#142</a>.</p>
<p>Screenshots are fine for static features, but a database client is about motion: you type in the editor, results appear, tabs get flipped, plans get expanded. A still image can&#39;t really carry any of that, and the old site didn&#39;t try to.</p>
<p>Here&#39;s what changed:</p>
<ul>
<li>The <strong>hero section</strong> now auto-plays a short overview video instead of a static screenshot, so the first thing you see is the app actually running.</li>
<li>The <strong>wiki</strong> gained <strong>11 embedded video demos</strong>: first connection, SQL editor, Visual Query Builder, SQL Notebook, Visual EXPLAIN, data grid, split view, plugins, AI assistant, keyboard shortcuts, and more. Each one has an auto-generated poster frame so the page still reads well before the video loads.</li>
<li>A reusable <code>VideoPlayer</code> client component with loading states, an error overlay with a retry button, multi-event readiness detection (<code>canplay</code>, <code>playing</code>, <code>loadeddata</code>, <code>timeupdate</code>), and <code>prefers-reduced-motion</code> support for visitors who don&#39;t want autoplay.</li>
<li>A server-side <code>wrapVideosInHtml</code> helper so wiki, blog, and SEO markdown authors can drop a plain <code>&lt;video&gt;</code> tag into content and get the same player for free.</li>
<li>The GitHub README now opens with an <code>overview.gif</code> instead of a static PNG, so people coming in from search or social see the product in motion before they reach the site.</li>
</ul>
<p>The site used to be a page of screenshots. It&#39;s a page of demos now, and that&#39;s entirely Nako&#39;s work.</p>
<p>Here&#39;s the overview video itself, the one that sits at the top of the home page. It&#39;s a good sample of the care that went into the rest:</p>
<p><video src="https://tabularis.dev/videos/overview.mp4" controls muted playsinline loop autoplay controlsList="nodownload noremoteplayback noplaybackrate" disablePictureInPicture></video></p>
<p>Nako&#39;s own project, <a href="https://devglobe.xyz">devglobe.xyz</a>, is a genius idea and worth a detour. It&#39;s a live 3D globe of developers coding around the world in real time: you can see who is writing what, in which language, from which city, right now. A small Language Server plugin sends a heartbeat every 30 seconds while you&#39;re active, sharing only the language, a city-level location, and the editor name. There&#39;s a <a href="https://github.com/CaadriFR/zed-devglobe">Zed extension</a> already, and more editors in the works. It&#39;s the kind of thing that sounds obvious in hindsight and somehow nobody had built yet.</p>
<hr>
<h2>Readability Across Every Theme</h2>
<p><a href="https://github.com/thomaswasle">@thomaswasle</a>&#39;s PR <a href="https://github.com/TabularisDB/tabularis/pull/139">#139</a> is a readability pass across <strong>every built-in theme</strong>, not just the default.</p>
<p>Tabularis ships with a range of themes (Dracula, light, dark, high-contrast, and more), and each one had picked up small contrast issues over time. Thomas went through all of them and normalized the spots where text, borders, or highlighted rows were harder to read than they should be. v0.9.18 did this for Dracula on its own; v0.9.20 brings the rest up to the same bar.</p>
<p>Easy to miss if you stick to one theme. Very noticeable if you switch around, or if you&#39;ve been quietly putting up with low contrast in one of the less-used ones.</p>
<hr>
<h2>Clipboard Import — Paste Data Directly Into a Table</h2>
<p>Sometimes the shortest path from a spreadsheet to a database is a copy and a paste.</p>
<p>v0.9.20 adds a <strong>Clipboard Import</strong> flow: copy tabular data from a spreadsheet, a CSV preview, a rendered Markdown table, or another SQL client, and paste it straight into Tabularis. The app detects the structure, proposes a target table and column mapping, and lets you review the rows before they&#39;re inserted.</p>
<p>It pairs well with the existing CSV and SQL dump imports. When you only have a handful of rows and no file on disk, opening a dialog just to save a throwaway <code>.csv</code> is friction. Clipboard import skips that step.</p>
<p>The wiki has the full walkthrough, and the website&#39;s <strong>Features</strong> section now has a dedicated entry on when clipboard import is the right choice versus a file-based import.</p>
<hr>
<h2>Import Existing Plans Into Visual EXPLAIN</h2>
<p><a href="https://tabularis.dev/blog/v0917-visual-explain">Visual EXPLAIN</a> gains <strong>import support</strong> in v0.9.20: EXPLAIN output captured elsewhere (a plan pasted from a colleague, a snippet from a GitHub issue, an export from another client) opens straight into the viewer without re-running the query.</p>
<p>Makes it much easier to share a plan and reason about it together, instead of treating Visual EXPLAIN as a purely local tool.</p>
<hr>
<h2>Comparison Pages and Other Website Work</h2>
<p>v0.9.20 also ships the first batch of <strong>comparison pages</strong>: side-by-side pages showing where Tabularis lines up with (and differs from) other database clients. They use a shared comparison-builder component, proper logos (now in PNG for crisper rendering), and styling consistent with the rest of the site.</p>
<p>Alongside them:</p>
<ul>
<li>The website now reads the app version from a single <code>APP_VERSION</code> source of truth, so release-tied strings stay in sync automatically.</li>
<li>The sitemap is referenced from <code>robots.txt</code>, which helps the new comparison pages get indexed faster.</li>
<li>A new long-form post, <a href="https://tabularis.dev/blog/databases-are-not-becoming-chatbots"><em>Databases Are Not Becoming Chatbots</em></a>, ships alongside the release.</li>
</ul>
<hr>
<h2>Under the Hood</h2>
<p>Two smaller changes worth calling out:</p>
<ul>
<li><strong>React hook deps and tooltip wrapper fix.</strong> A rare case where memoized settings definitions could end up with stale state in a few tooltip-wrapped controls. Memoization and callbacks are stable now, state no longer drifts.</li>
<li><strong>CLI parsing extracted into its own module.</strong> The Tauri entry point in the Rust backend used to carry its argument-parsing logic inline. Pure refactor, no behavior change, but it makes future CLI flags (and their tests) a lot easier to add.</li>
</ul>
<hr>
<h2>A Community Release</h2>
<p>Worth naming plainly what this release is: of the six headline changes, <strong>three came from outside contributors</strong>. That ratio is new for Tabularis, and it&#39;s a good sign.</p>
<p>To <strong><a href="https://github.com/Nako0">@Nako0</a></strong> for turning the site from screenshots into demos: thank you, seriously. Tabularis looks like a different product now. And go check out <a href="https://devglobe.xyz">devglobe.xyz</a> while you&#39;re at it.</p>
<p>To <strong><a href="https://github.com/thomaswasle">@thomaswasle</a></strong> for quietly pushing the themes towards being uniformly readable: the app is more comfortable to spend a day in because of your work.</p>
<hr>
<p><em>v0.9.20 is available now. Update via the in-app updater, or download from the <a href="https://github.com/TabularisDB/tabularis/releases/tag/v0.9.20">releases page</a>.</em></p>
]]></content:encoded>
      <enclosure url="https://tabularis.dev/blog/v0920-clipboard-import-visual-explain-videos/opengraph-image.png" type="image/png" />
      <category>release</category>
      <category>import</category>
      <category>explain</category>
      <category>website</category>
      <category>community</category>
      <category>themes</category>
    </item>
    <item>
      <title>Databases Are Not Becoming Chatbots</title>
      <link>https://tabularis.dev/blog/building-tabularis-future-of-databases-ai</link>
      <guid isPermaLink="true">https://tabularis.dev/blog/building-tabularis-future-of-databases-ai</guid>
      <pubDate>Fri, 17 Apr 2026 12:30:00 GMT</pubDate>
      <description>AI is not replacing databases. What is changing is the layer around them: context, interpretation, explainability, notebooks, agents, and the workflows that connect them.</description>
      <content:encoded><![CDATA[<h1>Databases Are Not Becoming Chatbots</h1>
<p>Over the last months, while building <code>Tabularis</code>, I started realizing that I was not just building a database client.</p>
<p>I was running into a bigger question: what happens to databases when software no longer just reads and writes records, but also tries to interpret schema, suggest queries, explain execution plans, preserve working context, and collaborate with language models?</p>
<p>I do not have a final answer, but I am starting to form a position.</p>
<p>My current view is that the natural evolution of databases is not that they become chatbots. I also do not think they become &quot;AI-native&quot; in the vague marketing sense that phrase usually carries.</p>
<p>What is changing is the layer around the database.</p>
<p>Databases still matter because structured truth still matters. Tables, constraints, indexes, transactions, and schemas are not becoming obsolete just because language models are good at generating plausible text. If anything, they become more important when the surrounding software becomes probabilistic.</p>
<p>What AI changes is not the need for a system of record. It changes the system of interpretation, navigation, and action around that record.</p>
<p>That shift is becoming very concrete to me in <code>Tabularis</code>.</p>
<div style="height: 1.25rem"></div>

<p>Once you put a SQL editor, AI-assisted query generation, query explanation, visual explain plans, notebooks, MCP integration, and a plugin system in the same product, the old idea of a database client starts to feel incomplete.</p>
<p>A database client used to be a place where you connected to a server, browsed tables, wrote some SQL, inspected results, and moved on. That model is still useful, but it no longer feels sufficient.</p>
<p>The moment AI enters the workflow, the client becomes something else: a coordination layer between a human trying to understand a system, a database holding structured truth, and a model trying to compress, interpret, and act on context.</p>
<p><img src="https://tabularis.dev/img/posts/tabularis-visual-explain-ai-analysis-recommendations.png" alt="Tabularis visual explain with AI analysis and recommendations"></p>
<p><em>One of the signals for me: the useful part is not just generating SQL, but helping make reasoning around queries and plans more legible.</em></p>
<h2>The Part I Keep Coming Back To</h2>
<p>The strongest idea I keep circling back to is this: the real change is not that databases start speaking natural language. The real change is that the workflow around data becomes more contextual, more stateful, and more collaborative.</p>
<p>In <code>Tabularis</code>, that is visible in small ways and large ones.</p>
<p>An AI overlay in the editor is not just a shortcut for writing SQL faster. Query explanation is not just a convenience feature for beginners. Visual EXPLAIN is not just a prettier way to look at query plans. Notebooks are not just a nicer place to collect queries. MCP is not just another integration checkbox.</p>
<p>Taken together, they point in the same direction. Database work is moving away from isolated commands and toward systems that help users build, inspect, preserve, and reuse context.</p>
<p>That is the part that feels important to me.</p>
<h2>The Technical Decisions I Am Making in Tabularis</h2>
<p>Building <code>Tabularis</code> has forced me to make a few technical bets. They are still bets, not conclusions, but they say a lot about what I currently believe.</p>
<h3>1. AI should not become the source of truth</h3>
<p>This is the most important one.</p>
<p>I do not want AI to replace schema, queries, plans, or results. I want it to sit above them as a layer of interpretation.</p>
<p>That sounds obvious, but it is easy to blur this boundary. A generated query can start feeling authoritative. A natural-language explanation can sound more reliable than the actual execution plan. A confident answer can quietly push the user into trusting the model more than the database.</p>
<p>I do not think that is a healthy direction.</p>
<p>In <code>Tabularis</code>, the database should remain the thing that is real. The AI layer should help the user access, understand, and manipulate that reality, but it should not quietly replace it.</p>
<h3>2. AI should stay optional</h3>
<p>I am keeping AI optional in <code>Tabularis</code>, and that choice is partly practical but also philosophical.</p>
<p>I do not think the future of database tooling should collapse into a single provider, a single model family, or even a single interaction style. Some users want OpenAI-compatible APIs. Some want Anthropic. Some want Ollama. Some want no AI at all.</p>
<p>I do not think that flexibility is just a commercial checkbox. I think it reflects the right abstraction. The stable thing is not the model. The stable thing is the workflow.</p>
<h3>3. Preserved context matters more than one-shot generation</h3>
<p>This is probably the bet that has become stronger as I kept building.</p>
<p>A lot of AI product design still assumes that the main value is in producing an answer quickly. Sometimes that is true. But in database work, I suspect a large part of the value is not generation but preserved context.</p>
<p>Why did I run this query?</p>
<p>What assumption was I testing?</p>
<p>What did the previous result suggest?</p>
<p>Which part of the schema turned out to matter?</p>
<p>Why does this plan get expensive at this join?</p>
<p>That is why notebooks and explainability feel so important to me in <code>Tabularis</code>.</p>
<p>A notebook is not just a better place to put SQL and Markdown. It is a form of working memory. Visual explainability is not just an educational feature. It is a way to make optimization reasoning legible.</p>
<p>The more I work on the product, the more I think memory and explanation are more durable primitives than raw generation.</p>
<p><img src="https://tabularis.dev/img/posts/tabularis-notebook-sql-cell-pie-chart-data-grid.png" alt="Tabularis SQL notebook with results and charts"></p>
<p><em>The more I work on notebooks, the more I think preserved context is a deeper primitive than one-shot AI output.</em></p>
<h3>4. The future probably looks more composable than monolithic</h3>
<p>I am also leaning into MCP and plugins because I increasingly doubt that the future here will be monolithic.</p>
<p>I do not think there will be one magical &quot;AI database&quot; that cleanly absorbs querying, reasoning, context management, automation, and verification into a single perfect layer.</p>
<p>I think the more likely future is composable.</p>
<p>Databases will remain databases. Models will remain models. Clients, agents, plugins, and protocols will mediate between them.</p>
<p><code>Tabularis</code> keeps pushing me toward that conclusion because the moment you try to support real workflows, you discover that no single surface is enough.</p>
<h2>The Mistakes I Think I May Be Making</h2>
<p>This is the part I trust the most, because it is the least polished.</p>
<p>I am not just making decisions. I am also building under uncertainty, and some of that uncertainty is probably pointing at mistakes.</p>
<h3>I may be using AI where better UX would solve the problem more cleanly</h3>
<p>There is always a temptation to add a generative layer when the actual problem is discoverability, information architecture, or interface design.</p>
<p>If a user cannot find the right table, understand a relationship, or inspect a plan easily, an AI-generated explanation may help. But it may also hide the fact that the product itself is not yet clear enough.</p>
<p>I think this is one of the easiest traps to fall into. AI can compensate for weak product decisions just well enough to make those decisions look acceptable.</p>
<h3>I may be overestimating how often users want generation instead of control</h3>
<p>It is easy, especially when building AI features, to assume that the more the system does for the user, the better.</p>
<p>I am not convinced that is true in database tooling.</p>
<p>A lot of serious database work is not blocked by typing speed. It is blocked by ambiguity, risk, context switching, and lack of confidence.</p>
<p>In that world, the winning feature may not be &quot;generate the query for me.&quot; It may be &quot;help me trust what I am about to run.&quot;</p>
<p>Those are very different product instincts.</p>
<h3>I may be underestimating reproducibility and auditability</h3>
<p>A query written by a human is imperfect, but it is usually inspectable in a straightforward way.</p>
<p>A suggestion generated from a changing prompt, dynamic schema context, model-specific behavior, and hidden retrieval steps is much harder to reason about after the fact.</p>
<p>If AI becomes part of database work, then being able to understand why something was suggested, what context was used, and how a decision was derived becomes much more important.</p>
<p>I suspect the industry still talks too much about generation quality and too little about decision traceability.</p>
<h3>I may be designing too much for the future</h3>
<p>This is the most uncomfortable possibility.</p>
<p>Once you start seeing where things might go, it becomes tempting to over-architect for a world that has not arrived yet.</p>
<p>Maybe some of what I think will become core will remain peripheral. Maybe the average user does not want an AI-mediated database environment. Maybe they just want a fast editor, a clear schema browser, and fewer rough edges.</p>
<p>I try to keep that possibility in mind because future-facing product conviction can become self-indulgent very quickly.</p>
<h2>What I Think Is Actually Emerging</h2>
<p>I do not think databases are going away.</p>
<p>I do not think SQL is going away.</p>
<p>I do not think language models replace the need for carefully modeled data, explicit constraints, or systems that can be trusted.</p>
<p>But I do think the center of gravity is shifting.</p>
<p>More of the value around data work will sit in context management, interpretation, explainability, derivation, memory of prior work, and tool-mediated action around the database itself.</p>
<p>That does not make the database less important. If anything, it makes it more important, because the rest of the system becomes softer and less deterministic.</p>
<p>The database remains the anchor.</p>
<p>What changes is everything around it.</p>
<p><code>Tabularis</code>, at least for me, is a way to explore that shift in concrete form.</p>
<p>Not as a grand theory, and definitely not as a finished answer, but as a product that keeps forcing the question.</p>
<p>Every time I add AI to query writing, explanation to plans, notebooks to analysis, or MCP to agent workflows, I feel the same tension: the old boundaries between client, assistant, and interface to the database are getting weaker.</p>
<p>I may be wrong about where that leads.</p>
<p>I may be making some of the wrong bets too early.</p>
<p>But I am increasingly convinced that the old model of &quot;database client as editor plus grid plus manual querying&quot; is no longer enough.</p>
<p>Something wider is emerging around the database, and building <code>Tabularis</code> is my way of trying to understand what it is.</p>
]]></content:encoded>
      <enclosure url="https://tabularis.dev/blog/building-tabularis-future-of-databases-ai/opengraph-image.png" type="image/png" />
      <category>ai</category>
      <category>databases</category>
      <category>product</category>
      <category>architecture</category>
      <category>opinion</category>
    </item>
    <item>
      <title>v0.9.19: Polish, Bug Fixes, French and German</title>
      <link>https://tabularis.dev/blog/v0919-polish-french-german</link>
      <guid isPermaLink="true">https://tabularis.dev/blog/v0919-polish-french-german</guid>
      <pubDate>Thu, 16 Apr 2026 22:30:00 GMT</pubDate>
      <description>v0.9.19 is a short follow-up to v0.9.18: a round of UI polish and bug fixes on top of the new History workflow, plus two new locales — French and German — bringing the UI to six languages.</description>
      <content:encoded><![CDATA[<h1>v0.9.19: Polish, Bug Fixes, French and German</h1>
<p><strong>v0.9.19</strong> is a short follow-up to <a href="https://tabularis.dev/blog/v0918-query-history">v0.9.18</a>. It does not introduce a new headline feature. Instead, it smooths out the rough edges around the History and Favorites workflows that just landed, fixes a few bugs, and brings two new locales — <strong>French</strong> and <strong>German</strong> — into the UI.</p>
<p>If v0.9.18 was about shipping the new sidebar workspace, v0.9.19 is about making it feel finished.</p>
<hr>
<h2>Two New Languages: French and German</h2>
<p>Tabularis now ships in <strong>six languages</strong>: English, Italian, Spanish, Chinese, <strong>French</strong>, and <strong>German</strong>.</p>
<p>Both new locales were produced with AI assistance and then wired into the standard i18n pipeline alongside the existing translations. Language detection is still automatic based on your system locale, with a manual override in Settings → General.</p>
<p>AI-generated translations are a practical way to unblock users who were locked out by the language barrier, but they are not a replacement for a native speaker&#39;s eye. Some phrasings will feel off, some terminology will not match what a French or German developer would actually say in front of a database.</p>
<p>If you speak either language and spot something that should be rewritten, the locale files are plain JSON — no build step, no toolchain, no ceremony. Open a pull request against <a href="https://github.com/TabularisDB/tabularis/blob/main/src/i18n/locales/fr.json"><code>src/i18n/locales/fr.json</code></a> or <a href="https://github.com/TabularisDB/tabularis/blob/main/src/i18n/locales/de.json"><code>src/i18n/locales/de.json</code></a> and it will land in the next release. The same invitation stands for the other locales too.</p>
<p>The project README is also available in both languages: <a href="https://github.com/TabularisDB/tabularis/blob/main/README.fr.md">README.fr.md</a> and <a href="https://github.com/TabularisDB/tabularis/blob/main/README.de.md">README.de.md</a>.</p>
<hr>
<h2>Sidebar Polish</h2>
<p>The Explorer sidebar gained a few small but noticeable touches on top of the structure introduced in v0.9.18.</p>
<ul>
<li><strong>SQL preview highlighting</strong> — history and favorites entries now render their SQL preview with lightweight syntax highlighting instead of flat text. It makes the sidebar much easier to scan when you are looking for a specific query at a glance.</li>
<li><strong>Grouped favorites</strong> — saved queries are now sorted and grouped in a way that keeps related items close together, instead of relying on a single flat list.</li>
<li><strong>Delete confirmation</strong> — removing a favorite now goes through a confirmation step, so a misclick in the sidebar no longer silently loses a saved query.</li>
<li><strong>Single-click selection</strong> — sidebar items now highlight on a single click, which makes keyboard and mouse navigation feel consistent with the rest of the app.</li>
<li><strong>SQL preview truncation</strong> — long queries in the sidebar now truncate cleanly instead of pushing the layout around.</li>
</ul>
<p><img src="https://tabularis.dev/img/tabularis-favorites-sidebar.png" alt="Explorer sidebar showing Favorites and History with highlighted SQL previews"></p>
<hr>
<h2>Database Context for History and Favorites</h2>
<p>A subtle but important fix around multi-database sessions: both <strong>query history</strong> and <strong>saved queries</strong> now persist the <strong>database</strong> the query was originally run against.</p>
<p>That means when you re-run a query from History or reopen a favorite, Tabularis brings you back to the right database automatically, instead of leaving you on whichever database happened to be active in the editor. It is the kind of small correctness fix that you only appreciate after it has been wrong once in production.</p>
<p>On top of that, connections that transition from a <strong>single-database</strong> setup to <strong>multi-database</strong> mode now have their database field backfilled, so existing history and favorites remain valid across the change.</p>
<hr>
<h2>Other Fixes</h2>
<p>A handful of smaller improvements rounding out the release:</p>
<ul>
<li>The Query History section in the sidebar got layout and interaction polish alongside the new highlighting.</li>
<li>The saved-query modal now surfaces timestamp metadata and a database picker when it matters.</li>
<li>The website&#39;s overview image was refreshed.</li>
</ul>
<p>Nothing dramatic, but each item removes a small friction point reported after v0.9.18 went out.</p>
<hr>
<h2>Small Release, Real Improvements</h2>
<p><strong>v0.9.19</strong> is not a big release on paper. It does not add a new tab, a new engine, or a new mode. What it does is take the new workflows introduced in v0.9.18 and bring them closer to feeling finished — cleaner sidebar previews, correct database handling on re-run, and two more languages in the UI.</p>
<p>If you are already on v0.9.18, the upgrade is painless and worth it for the polish alone. If you speak French or German and want to improve the fresh translations, pull requests are very welcome.</p>
<hr>
<p><em>v0.9.19 is available now. Update via the in-app updater, or download from the <a href="https://github.com/TabularisDB/tabularis/releases/tag/v0.9.19">releases page</a>.</em></p>
]]></content:encoded>
      <enclosure url="https://tabularis.dev/blog/v0919-polish-french-german/opengraph-image.png" type="image/png" />
      <category>release</category>
      <category>i18n</category>
      <category>bugfix</category>
      <category>ui</category>
      <category>sidebar</category>
      <category>community</category>
    </item>
    <item>
      <title>v0.9.18: Query History Becomes a Workflow</title>
      <link>https://tabularis.dev/blog/v0918-query-history</link>
      <guid isPermaLink="true">https://tabularis.dev/blog/v0918-query-history</guid>
      <pubDate>Thu, 16 Apr 2026 12:15:00 GMT</pubDate>
      <description>v0.9.18 adds a real query history workflow to Tabularis: per-connection storage, search, date grouping, fast re-run actions, and retention controls. The release also includes a strong set of community-driven improvements across PostgreSQL, MySQL, AI settings, and theming.</description>
      <content:encoded><![CDATA[<h1>v0.9.18: Query History Becomes a Workflow</h1>
<p><strong>v0.9.18</strong> is mainly about one thing: making <strong>History</strong> useful enough to become part of the normal SQL editing loop.</p>
<p>Before this release, Tabularis already gave you strong editing, favorites, notebooks, and now Visual EXPLAIN. What was missing was a lightweight way to go back through the actual queries you ran during exploration. This update adds that missing layer: a per-connection query history in the Explorer sidebar, with search, grouping, quick actions, and retention controls.</p>
<hr>
<h2>Query History, Per Connection</h2>
<p>Every executed query is now stored in the Explorer&#39;s <strong>History</strong> tab for the active connection.</p>
<p>That sounds straightforward, but the important part is the scope: history is <strong>per connection</strong>, not global. Your PostgreSQL session, your MySQL analytics connection, and your local SQLite scratchpad each keep their own timeline.</p>
<p>This matters because query history is only useful when it stays close to context. If you are debugging a production issue in one connection and testing a schema idea in another, you do not want those timelines mixed together.</p>
<p><img src="https://tabularis.dev/img/tabularis-explorer-overview.png" alt="Explorer sidebar showing Structure, Favorites, and History tabs"></p>
<hr>
<h2>A Better Sidebar for Ongoing Work</h2>
<p>To make room for these new workflows, the <strong>Explorer sidebar</strong> also becomes more clearly structured.</p>
<p>Instead of treating everything as one long schema tree, Tabularis now gives the active connection a small workspace of its own: <strong>Structure</strong>, <strong>Favorites</strong>, and <strong>History</strong> live side by side in the same panel.</p>
<p>That change matters because the feature set is no longer just about browsing tables. The sidebar now has to support three different kinds of work:</p>
<ul>
<li><strong>Structure</strong> when you need schema objects and navigation</li>
<li><strong>Favorites</strong> when you want to keep reusable SQL close at hand</li>
<li><strong>History</strong> when you want to go back through what you just ran</li>
</ul>
<p>It is a small UI change on paper, but it is an important one architecturally. It turns the sidebar from a database tree into a more complete working surface for exploration, repetition, and recall.</p>
<hr>
<h2>Built for Real Iteration</h2>
<p>The new <strong>History</strong> tab is not just a raw log.</p>
<p>Each entry stores:</p>
<ul>
<li>The executed SQL</li>
<li>The execution timestamp</li>
<li>The duration</li>
<li>Whether it succeeded or failed</li>
<li>Rows affected when available</li>
</ul>
<p>From there, Tabularis gives you the actions you actually need while iterating:</p>
<ul>
<li><strong>Search</strong> by SQL text</li>
<li><strong>Date grouping</strong> such as Today and Yesterday</li>
<li><strong>Double-click to reopen</strong> a previous query in the editor</li>
<li><strong>Run again</strong> or <strong>run in a new tab</strong></li>
<li><strong>Copy SQL</strong></li>
<li><strong>Save to Favorites</strong></li>
<li><strong>Delete a single entry</strong> or <strong>clear all history</strong> for the current connection</li>
</ul>
<p>This turns history into a fast loop: run a query, tweak it, compare with an older version, reopen it, and keep going without hunting through tabs or clipboard fragments.</p>
<p><img src="https://tabularis.dev/img/tabularis-query-history-sidebar.png" alt="Query History tab in the Explorer sidebar with grouped entries and search"></p>
<hr>
<h2>Small Details That Make It Better</h2>
<p>Two practical choices make the feature feel more polished than a basic history panel.</p>
<p>First, repeated executions of the exact same SQL do not immediately spam duplicate entries one after another. Tabularis de-duplicates consecutive identical queries and updates the latest entry instead.</p>
<p>Second, history surfaces failures as first-class information instead of pretending only successful queries matter. That is important in real database work, because the query you need to revisit is often the one that failed five minutes ago.</p>
<p>There is also a retention control in <strong>Settings → General → Query History</strong>, with <code>queryHistoryMaxEntries</code> defaulting to <code>500</code> per connection.</p>
<hr>
<h2>Multi-Caret Editing</h2>
<p>The SQL editor in Tabularis supports <strong>multi-caret editing</strong>, which lets you place several cursors in the editor and type, delete, or select at all of them simultaneously.</p>
<p>This is useful more often than it sounds. Renaming a column alias in four places at once, wrapping several lines in a function call, adding commas to a list of values, commenting out a block of conditions one by one: these are the kind of micro-edits that slow you down when you have to repeat them manually.</p>
<p>Here are the shortcuts that make it work:</p>
<table>
<thead>
<tr>
<th align="left">Action</th>
<th align="left">macOS</th>
<th align="left">Windows / Linux</th>
</tr>
</thead>
<tbody><tr>
<td align="left">Add cursor at click</td>
<td align="left"><code>⌘+Click</code></td>
<td align="left"><code>Ctrl+Click</code></td>
</tr>
<tr>
<td align="left">Add next occurrence</td>
<td align="left"><code>⌘+D</code></td>
<td align="left"><code>Ctrl+D</code></td>
</tr>
<tr>
<td align="left">Select all occurrences</td>
<td align="left"><code>⌘+Shift+L</code></td>
<td align="left"><code>Ctrl+Shift+L</code></td>
</tr>
<tr>
<td align="left">Cursors at line ends</td>
<td align="left"><code>⌥+Shift+I</code></td>
<td align="left"><code>Alt+Shift+I</code></td>
</tr>
</tbody></table>
<h3>Paste Into Multiple Carets</h3>
<p><code>v0.9.18</code> adds one more piece to this workflow: <strong>pasting into multiple carets</strong>.</p>
<p>When you have multiple cursors active and paste text from the clipboard, Tabularis distributes the pasted lines across the cursors, one line per caret. If the number of lines in the clipboard matches the number of cursors, each cursor receives its own line. Otherwise, every cursor receives the full pasted text.</p>
<p>This makes column-wise edits, repeated line transformations, and bulk query rewrites much less awkward. It is the kind of change you notice immediately because it removes a break in muscle memory.</p>
<img src="https://tabularis.dev/img/tabularis-multi-carets.gif" alt="Multi-caret paste in the Tabularis SQL editor distributing clipboard lines across cursors" loading="lazy" decoding="async" style="width:100%;border-radius:8px;margin:1rem 0" />

<hr>
<h2>Other Notable Improvements in v0.9.18</h2>
<p>The release is centered on History, but there are several other useful additions and fixes around it.</p>
<p>This is <a href="https://github.com/midasism">@midasism</a>&#39;s contribution in PR <a href="https://github.com/TabularisDB/tabularis/pull/132">#132</a>: <strong>PostgreSQL schema mode</strong> now gets a proper <strong>table search filter</strong>, bringing it closer to the multi-database browsing experience already available elsewhere in the app.</p>
<p>This is <a href="https://github.com/traustitj">@traustitj</a>&#39;s contribution in PR <a href="https://github.com/TabularisDB/tabularis/pull/133">#133</a>: <strong>MySQL connections</strong> now expose <strong>SSL configuration options</strong> directly in the connection flow, which is an important upgrade for real deployments where plaintext local-style settings are not enough.</p>
<p>This is <a href="https://github.com/thomaswasle">@thomaswasle</a>&#39;s contribution in PR <a href="https://github.com/TabularisDB/tabularis/pull/134">#134</a>: <strong>MySQL connection URLs</strong> now use the <strong>system timezone</strong> correctly instead of forcing UTC behavior.</p>
<p>This is <a href="https://github.com/thomaswasle">@thomaswasle</a>&#39;s contribution in PR <a href="https://github.com/TabularisDB/tabularis/pull/135">#135</a>: the <strong>Dracula theme</strong> gets a readability pass, improving contrast in places that previously felt harder to scan.</p>
<p>This is <a href="https://github.com/krissss">@krissss</a>&#39;s contribution in PR <a href="https://github.com/TabularisDB/tabularis/pull/138">#138</a>: <strong>custom OpenAI provider URLs</strong> no longer duplicate path segments or assume a hardcoded <code>/v1</code>, which makes alternative provider setups much more reliable.</p>
<p>Alongside the community contributions, the core app also picks up several maintainer-authored improvements in this release: a plugin settings page, better plugin config caching, an explain-selection modal, an open source libraries modal, a welcome screen toggle, and a few sidebar and editor polish fixes.</p>
<hr>
<h2>History That Stays in the Editor</h2>
<p>The value of this feature is not just that Tabularis now stores past SQL. The useful part is that the history stays inside the same workspace where you browse schema objects, save favorites, write notebooks, and inspect query plans.</p>
<p>That makes <code>v0.9.18</code> a smaller release than <code>v0.9.17</code>, but also a very practical one. It removes friction from the everyday loop of writing SQL, rerunning it, comparing versions, and returning to something that worked.</p>
<hr>
<p><em>v0.9.18 is available now. Update via the in-app updater, or download from the <a href="https://github.com/TabularisDB/tabularis/releases/tag/v0.9.18">releases page</a>.</em></p>
]]></content:encoded>
      <enclosure url="https://tabularis.dev/blog/v0918-query-history/opengraph-image.png" type="image/png" />
      <category>release</category>
      <category>history</category>
      <category>sql-editor</category>
      <category>community</category>
      <category>postgresql</category>
      <category>mysql</category>
    </item>
    <item>
      <title>v0.9.17: Visual EXPLAIN Arrives</title>
      <link>https://tabularis.dev/blog/v0917-visual-explain</link>
      <guid isPermaLink="true">https://tabularis.dev/blog/v0917-visual-explain</guid>
      <pubDate>Tue, 14 Apr 2026 12:00:00 GMT</pubDate>
      <description>v0.9.17 brings Visual EXPLAIN to Tabularis: interactive execution plan graphs, table and raw views, AI-assisted analysis, and cross-database support for PostgreSQL, MySQL, MariaDB, and SQLite.</description>
      <content:encoded><![CDATA[<h1>v0.9.17: Visual EXPLAIN Arrives</h1>
<p><strong>v0.9.17</strong> is centered around one feature: <strong>Visual EXPLAIN</strong>.</p>
<p>Instead of treating query plans as raw text you have to decode manually, Tabularis now opens them in a dedicated full-screen workflow with a graph view, a table view, raw output, and optional AI analysis. If you spend time tuning joins, tracking down slow scans, or checking whether an index is actually used, this is the release that makes that workflow much more practical.</p>
<hr>
<h2>Visual EXPLAIN, Now Built In</h2>
<p>You can now run <strong>Visual EXPLAIN</strong> directly from the <strong>SQL Editor</strong> and from <strong>Notebook SQL cells</strong>.</p>
<p>Tabularis selects the right explain format for the current driver, parses the result, and shows the plan in four synchronized views:</p>
<ul>
<li><strong>Graph</strong> for the execution tree and expensive nodes</li>
<li><strong>Table</strong> for exact metrics and per-node details</li>
<li><strong>Raw</strong> for the original database output</li>
<li><strong>AI Analysis</strong> for a second-pass explanation of likely bottlenecks</li>
</ul>
<p>The goal is simple: less time decoding plan output, more time understanding what the optimizer is doing.</p>
<p><img src="https://tabularis.dev/img/posts/tabularis-visual-explain-graph-view-execution-plan.png" alt="Visual EXPLAIN modal with graph view showing execution plan nodes, cost heatmap, and summary bar"></p>
<hr>
<h2>Why This Matters</h2>
<p><code>EXPLAIN</code> is one of the most useful tools in SQL, but the output is rarely pleasant to work with. PostgreSQL gives you rich JSON, MySQL changes behavior depending on the server version, MariaDB exposes its own fields, and SQLite gives you a much flatter structural plan.</p>
<p>In <strong>v0.9.17</strong>, Tabularis smooths over those differences and turns them into one consistent inspection workflow.</p>
<p>That means you can:</p>
<ul>
<li>Spot the highest-cost node without scanning raw output line by line</li>
<li>Compare estimated rows against actual rows when ANALYZE data is available</li>
<li>Inspect filters, index conditions, loops, and buffer data in one place</li>
<li>Re-run the plan after an index or query rewrite and check what changed</li>
</ul>
<p>For PostgreSQL in particular, this is a major upgrade over bouncing between the editor and external tooling just to inspect a single plan.</p>
<hr>
<h2>The Main Pieces of Visual EXPLAIN</h2>
<p>The new workflow is more than a single graph.</p>
<h3>Graph and Node Details</h3>
<p>The default view renders the execution plan as a node graph with cost-based coloring, so the expensive parts of the query stand out immediately. Selecting a node opens a detail panel with the metrics and conditions attached to that step.</p>
<p>This makes it much easier to answer questions like:</p>
<ul>
<li>Where is the scan happening?</li>
<li>Which join is dominating cost?</li>
<li>Is the estimate badly wrong at a specific node?</li>
<li>Is the plan using the index you expected?</li>
</ul>
<h3>Overview Bar</h3>
<p>An overview section highlights the most relevant signals from the plan, including the highest-cost node, the slowest step, large estimate gaps, sequential scans, and temporary operations. Instead of reading the whole plan top to bottom first, you can jump directly to the suspicious areas.</p>
<h3>AI Analysis</h3>
<p>Visual EXPLAIN also adds a dedicated <strong>AI analysis flow</strong>. Tabularis can send the query and raw plan output to the configured provider and return a structured explanation of what the plan is doing, where the likely bottlenecks are, and what might be worth testing next.</p>
<p>This release also introduces a cleaner <strong>AI dropdown button</strong> in the notebook and editor UI, which fits well with the new explain-analysis workflow.</p>
<hr>
<h2>Cross-Database Support</h2>
<p>Visual EXPLAIN is not PostgreSQL-only.</p>
<p><strong>v0.9.17</strong> adds driver-aware handling for:</p>
<ul>
<li><strong>PostgreSQL</strong> with JSON plans, ANALYZE support, and buffer statistics</li>
<li><strong>MySQL</strong> with automatic version detection and fallback between <code>EXPLAIN ANALYZE</code>, <code>EXPLAIN FORMAT=JSON</code>, and tabular <code>EXPLAIN</code></li>
<li><strong>MariaDB</strong> with JSON parsing improvements for filesort, wrappers, and subquery cache details</li>
<li><strong>SQLite</strong> with reconstructed plan trees from <code>EXPLAIN QUERY PLAN</code></li>
</ul>
<p>That cross-driver work is a large part of what makes this release important. The UI is visible, but most of the value comes from normalizing very different execution-plan formats into one model that Tabularis can actually visualize.</p>
<hr>
<h2>Safer Explain Workflows</h2>
<p>There are also a few practical guardrails in this release.</p>
<p>Tabularis now checks whether a query is actually explainable before sending it to the database, strips leading comments before validation, and handles <code>EXPLAIN ANALYZE</code> more carefully for data-modifying statements.</p>
<p>In practice, that means:</p>
<ul>
<li>DDL statements are blocked before they turn into confusing errors</li>
<li>Annotated queries still work as expected</li>
<li><code>INSERT</code>, <code>UPDATE</code>, and <code>DELETE</code> are treated more carefully when ANALYZE would execute them</li>
</ul>
<p>These details matter because they keep Visual EXPLAIN useful in real-world workflows, not just in ideal demos.</p>
<hr>
<h2>Smaller Improvements</h2>
<p>Visual EXPLAIN is the headline, but not the only change in <code>v0.9.17</code>.</p>
<ul>
<li>Multi-database connection editing now auto-loads databases</li>
</ul>
<p>That keeps the release focused, with a small usability improvement around connection setup alongside the new query-plan workflow.</p>
<hr>
<h2>Read the Plan, Stay in Context</h2>
<p>The interesting part of this release is not just that Tabularis can run <code>EXPLAIN</code>. Plenty of tools can do that. The useful part is that the result stays inside the same environment where you are writing and testing the query.</p>
<p>Run the query. Open the plan. Inspect the graph. Check the exact node metrics. Re-run after a change.</p>
<p>If you want the full walkthrough, there is now a dedicated <a href="https://tabularis.dev/wiki/visual-explain">Visual EXPLAIN documentation page</a> and the earlier <a href="https://tabularis.dev/blog/visual-explain-query-plan-analysis">feature preview post</a> with more screenshots and background.</p>
<hr>
<p><em>v0.9.17 is available now. Update via the in-app updater, or download from the <a href="https://github.com/TabularisDB/tabularis/releases/tag/v0.9.17">releases page</a>.</em></p>
]]></content:encoded>
      <enclosure url="https://tabularis.dev/blog/v0917-visual-explain/opengraph-image.png" type="image/png" />
      <category>release</category>
      <category>explain</category>
      <category>performance</category>
      <category>ai</category>
      <category>postgresql</category>
      <category>mysql</category>
      <category>sqlite</category>
    </item>
    <item>
      <title>From 0 to 1,000 GitHub Stars: What I Learned in 10 Weeks</title>
      <link>https://tabularis.dev/blog/from-zero-to-1000-github-stars</link>
      <guid isPermaLink="true">https://tabularis.dev/blog/from-zero-to-1000-github-stars</guid>
      <pubDate>Tue, 14 Apr 2026 09:00:00 GMT</pubDate>
      <description>Tabularis crossed 1,000 GitHub stars in under three months. No marketing team, no growth hacks. Just a useful tool and a community that showed up. Here&apos;s what actually worked, what didn&apos;t, and what I&apos;d do differently.</description>
      <content:encoded><![CDATA[<h1>From 0 to 1,000 GitHub Stars: What I Learned in 10 Weeks</h1>
<p>Eleven weeks ago I pushed the first binary of Tabularis to GitHub. A database client. One person, a late-night frustration, a Tauri app with a SQL editor and not much else.</p>
<p>Last week, ten weeks in, the repo crossed <strong>1,000 stars</strong>. Today, at week eleven, there are <strong>1,086 stars</strong>, <strong>70 forks</strong>, <strong>15 contributors</strong>, <strong>41 releases</strong>, and a plugin ecosystem that didn&#39;t exist eight weeks ago. The project started as &quot;debba.sql&quot;. I renamed it to Tabularis a few days in because I wanted it to feel like something bigger than a personal tool.</p>
<p>It became bigger than a personal tool. Not because of a launch strategy. Because people showed up.</p>
<p>This post is what I&#39;d tell myself ten weeks ago if I could go back. Not a growth playbook (those already exist and most of them are noise). This is what actually happened, what I think mattered, and what I got wrong.</p>
<p><a href="https://repostars.dev/?repos=debba%2Ftabularis&theme=dark"><img src="https://repostars.dev/api/embed?repo=debba%2Ftabularis&theme=dark" alt="RepoStars"></a></p>
<hr>
<h2>Week 1: Ship Fast, Ship Broken, Ship Anyway</h2>
<p>The first week produced fifteen releases. Fifteen. From v0.2.0 to v0.8.6. Some of them were embarrassing. I shipped a version that crashed on Wayland. I shipped a version where the database dropdown didn&#39;t work properly. I shipped anyway.</p>
<p>Here&#39;s what I learned: <strong>nobody remembers your v0.3.0</strong>. People remember whether the tool was useful when they tried it, and whether it got better when they came back. Shipping fast builds that muscle: the ability to push a fix in hours, not weeks. By the end of week one the release pipeline was solid, the update flow worked, and I had the confidence that I could fix anything that broke within a day.</p>
<p>The alternative, polishing in private for months, would have meant launching into silence. Instead, the first few users arrived while the project was still rough, and they stayed because they could see it moving.</p>
<hr>
<h2>Week 2-3: The First External Contributor Changes Everything</h2>
<p><a href="https://github.com/niklasschaeffer">@niklasschaeffer</a> opened the first external PR in week two: support for custom OpenAI-compatible API endpoints. It was a clean, well-scoped contribution. It also changed my relationship with the project overnight.</p>
<p>Before that PR, Tabularis was my code. After it, Tabularis was a codebase other people could read, understand, and modify. That shift, from &quot;my project&quot; to &quot;our project&quot;, is the most important thing that happened in the first month.</p>
<p>By week three, <a href="https://github.com/kconfesor">@kconfesor</a> contributed PostgreSQL schema selection and a full Spanish locale. Someone I&#39;d never met decided the tool was worth translating into their language. That&#39;s a signal no star count can match.</p>
<p><strong>What I&#39;d do differently</strong>: I should have written contributor documentation from day one. The first contributors succeeded despite the lack of guides, not because of them.</p>
<hr>
<h2>Week 4-5: The Plugin System, the Bet That Paid Off</h2>
<p>At the one-month mark we had ~270 stars and 5 contributors. Good momentum. But the decision that actually changed the trajectory was shipping the <strong>plugin system</strong> in v0.9.0.</p>
<p>A language-agnostic JSON-RPC protocol. Any language, any database. The first plugin (DuckDB) was ready on day one. Within two weeks the community had added <strong>Redis</strong>, <strong>ClickHouse</strong>, <strong>CSV</strong>, and a <strong>Hacker News</strong> plugin that turned a public API into a SQL-queryable database.</p>
<p>The plugin system did three things at once:</p>
<ol>
<li><p><strong>It multiplied what Tabularis could do</strong> without multiplying my workload. I didn&#39;t write the Redis driver. I didn&#39;t write the ClickHouse driver. The community did.</p>
</li>
<li><p><strong>It gave contributors a sandbox</strong>. Writing a plugin is self-contained: you don&#39;t need to understand the Tauri backend or the React frontend. That dramatically lowered the barrier to contribution.</p>
</li>
<li><p><strong>It changed the story</strong>. Tabularis went from &quot;a database client that supports three databases&quot; to &quot;a platform that can support any database.&quot; That&#39;s a much more interesting pitch.</p>
</li>
</ol>
<p><strong>Lesson</strong>: building an extension point early is one of the highest-leverage things a solo maintainer can do. It turns users into builders.</p>
<hr>
<h2>Week 6-8: Momentum Compounds</h2>
<p>This is the phase where everything started compounding. New contributors arrived every release. <a href="https://github.com/gzamboni">@gzamboni</a> and <a href="https://github.com/nicholas-papachriston">@nicholas-papachriston</a> with the Redis plugin. <a href="https://github.com/SergioChan">@SergioChan</a> fixing modal behavior. <a href="https://github.com/sycured">@sycured</a> adding PostgreSQL SSL modes. <a href="https://github.com/fandujar">@fandujar</a> building connection groups. <a href="https://github.com/GreenBeret9">@GreenBeret9</a> fixing SQLite issues.</p>
<p>Three things were happening simultaneously:</p>
<ul>
<li><p><strong>The product was improving faster than I could improve it alone.</strong> Community PRs were shipping features I hadn&#39;t planned: connection string import, drag-and-drop, bug fixes for databases I don&#39;t even use daily.</p>
</li>
<li><p><strong>The blog was building trust.</strong> I wrote about every major release, honestly. What worked, what didn&#39;t, what was coming. The HN plugin post was as much a stress test report as a feature announcement. People responded to that transparency.</p>
</li>
<li><p><strong>Word of mouth was doing the work.</strong> I never paid for promotion. Never ran ads. I did post on Reddit, Hacker News, X, Daily.dev, and dev.to, but from there the growth took on a life of its own: people sharing Tabularis in Slack channels, blog posts, and conversations I never saw.</p>
</li>
</ul>
<p><strong>Lesson</strong>: consistency &gt; virality. A new release every few days, a blog post every week, a Discord channel where questions get answered. That steady rhythm builds more trust than any single launch.</p>
<hr>
<h2>Week 9-10: International, AI-Powered, Notebook-Ready</h2>
<p>The last few weeks brought some of the most exciting contributions:</p>
<ul>
<li><strong>Chinese (Simplified) language support</strong> from <a href="https://github.com/GTLOLI">@GTLOLI</a>, the first Asian locale, opening up Tabularis to a massive developer community.</li>
<li><strong>MiniMax as a first-class AI provider</strong> from <a href="https://github.com/octo-patch">@octo-patch</a>, expanding the AI assistant beyond the usual suspects.</li>
<li><strong>Extended PostgreSQL type support</strong> from <a href="https://github.com/dev-void-7">@dev-void-7</a>: arrays, JSON, custom types. The kind of deep, unglamorous work that makes a database tool actually reliable.</li>
<li><strong>SQL Notebooks</strong>, the biggest feature release since the plugin system. Cell-based SQL analysis with inline charts, cell references, parameters, and parallel execution. Built directly into the app.</li>
</ul>
<p>And last week, <a href="https://github.com/thomaswasle">@thomaswasle</a> contributed drag-and-drop for connection groups. A small feature, but it represents something important: people are building the tool they want to use, not just the tool I envisioned.</p>
<hr>
<h2>The Numbers, Honestly</h2>
<p>The 1,000-star milestone landed at week ten. Here&#39;s where things stand today, one week later:</p>
<table>
<thead>
<tr>
<th>Metric</th>
<th>Week 4</th>
<th>Week 10</th>
<th>Week 11 (today)</th>
</tr>
</thead>
<tbody><tr>
<td>Stars</td>
<td>~270</td>
<td><strong>1,000</strong></td>
<td><strong>1,086</strong></td>
</tr>
<tr>
<td>Contributors</td>
<td>5</td>
<td>14</td>
<td><strong>15</strong></td>
</tr>
<tr>
<td>Releases</td>
<td>17</td>
<td>39</td>
<td><strong>41</strong></td>
</tr>
<tr>
<td>Forks</td>
<td>—</td>
<td>65</td>
<td><strong>70</strong></td>
</tr>
<tr>
<td>Plugins</td>
<td>1</td>
<td>7</td>
<td><strong>7</strong></td>
</tr>
<tr>
<td>Languages</td>
<td>2</td>
<td>4</td>
<td><strong>4</strong></td>
</tr>
<tr>
<td>Issues</td>
<td>—</td>
<td>~70</td>
<td><strong>81</strong></td>
</tr>
<tr>
<td>Pull Requests</td>
<td>—</td>
<td>~35</td>
<td><strong>41</strong></td>
</tr>
<tr>
<td>Downloads</td>
<td>—</td>
<td>~6,000</td>
<td><strong>7,100+</strong></td>
</tr>
</tbody></table>
<p>One thing that surprised me: where those stars come from. About half of our stargazers have a public location on their GitHub profile, and they span <strong>72 countries</strong> across every continent:</p>
<p><img src="https://tabularis.dev/img/posts/stargazers-by-country.svg" alt="Stargazers by country"></p>
<p>The United States and China lead, but what stands out is the long tail: South Korea, Germany, France, Brazil, Indonesia, Vietnam. Tabularis isn&#39;t a tool for one market. It&#39;s a tool for developers, and developers are everywhere.</p>
<p>The download breakdown by operating system tells a complementary story. Windows leads at 40.8%, followed by macOS at 32.3% and Linux at 26.9%:</p>
<p><img src="https://tabularis.dev/img/posts/downloads-by-os.svg" alt="Downloads by operating system"></p>
<p>What I find encouraging is that all three platforms have meaningful adoption. The Linux share is especially notable for a desktop app — it reflects the developer audience we&#39;re building for. And within each OS, the format diversity (setup vs. portable on Windows, .deb vs. AppImage vs. .rpm on Linux) suggests people are actually integrating Tabularis into their workflows, not just trying it once.</p>
<p>These numbers feel good. But I want to be honest about what they don&#39;t measure.</p>
<p>Stars are a vanity metric. I know that. A star doesn&#39;t mean someone uses Tabularis daily, or that it solved a real problem for them, or that they&#39;ll come back for v0.10. What I care about more: issues opened by people who actually tried the product. PRs from people who cared enough to fix something. Messages on Discord from people running Tabularis against their production databases.</p>
<p>1,000 stars is a milestone worth marking. It&#39;s not a destination.</p>
<hr>
<h2>What Didn&#39;t Work</h2>
<p>Not everything landed. A few honest misses:</p>
<ul>
<li><p><strong>Documentation lagged behind features.</strong> The wiki exists, but it&#39;s still thin. Features shipped faster than docs, and some users bounced because they couldn&#39;t figure out setup. This is the biggest gap I need to close.</p>
</li>
<li><p><strong>Windows testing was always behind.</strong> Most contributors (including me) develop on macOS or Linux. Windows bugs took longer to surface and longer to fix. A few early Windows users had a rough experience.</p>
</li>
<li><p><strong>I underestimated the support load.</strong> Solo maintainer math: every new feature creates new questions. Every new plugin creates new edge cases. I love that people are engaged. I&#39;m still learning how to scale my attention.</p>
</li>
</ul>
<hr>
<h2>The AI Factor</h2>
<p>I need to be honest about something: Tabularis would not exist without AI-assisted development. Specifically, without <a href="https://claude.ai/claude-code">Claude Code</a>.</p>
<p>A Tauri app with a Rust backend, a React/TypeScript frontend, a plugin system, an MCP server, SQL notebooks, and 41 releases in eleven weeks. One person. That math doesn&#39;t work without a force multiplier, and AI was that multiplier.</p>
<p>But here&#39;s the part that gets lost in the hype: <strong>AI doesn&#39;t replace experience. It amplifies it.</strong> Claude Code didn&#39;t design the plugin architecture. It didn&#39;t decide that JSON-RPC was the right protocol, or that the credential cache needed to wrap the system keychain, or that the notebook execution model should support parallel cells. Those decisions came from years of building software, understanding trade-offs, and knowing what users actually need.</p>
<p>What AI did was collapse the distance between a decision and its implementation. Once I knew <em>what</em> to build, I could build it in hours instead of days. The Rust backend, the React components, the test suites, the CI pipeline: Claude Code handled the volume while I handled the direction.</p>
<p>The analogy I keep coming back to: AI is like having an incredibly fast junior developer who never gets tired and knows every API. Powerful, but only if you know what to ask for. Without a clear architectural vision, without the experience to spot when the output is subtly wrong, without the judgment to know which corners not to cut, you just get bad code faster.</p>
<p>Three years ago, Tabularis would have been a side project that took a year to reach v0.5. Today it&#39;s at v0.9.16 with a real community. The difference isn&#39;t just speed. It&#39;s that AI let me stay in the creative, architectural layer, the part that actually matters, instead of spending most of my time on implementation details.</p>
<p>If you&#39;re an experienced developer and you&#39;re not using AI-assisted tools yet: try it. Not as a replacement for thinking, but as a way to spend more of your time on the thinking that counts.</p>
<hr>
<h2>What I&#39;d Tell Someone Starting Today</h2>
<p>If you&#39;re building an open source project and wondering whether it can find an audience, here&#39;s what I actually believe after ten weeks:</p>
<ol>
<li><p><strong>Ship before you&#39;re ready.</strong> Your first version will be embarrassing in retrospect. That&#39;s fine. The feedback you get from real users in week one is worth more than three months of private iteration.</p>
</li>
<li><p><strong>Build an extension point early.</strong> A plugin system, a hook system, a theme system — anything that lets other people build on top of your work without needing your permission or your codebase knowledge.</p>
</li>
<li><p><strong>Write about what you&#39;re building.</strong> Blog posts, release notes, changelogs. Not marketing copy. Honest accounts of what you built and why. People can tell the difference.</p>
</li>
<li><p><strong>Respond to every contributor.</strong> Review PRs quickly. Say thank you publicly. The first five contributors set the culture for the next fifty.</p>
</li>
<li><p><strong>Don&#39;t ask for stars.</strong> Build something useful. The stars follow.</p>
</li>
</ol>
<hr>
<h2>What&#39;s Next</h2>
<p>The plugin ecosystem is growing but still young. Notebooks need polish: performance with large documents, circular reference detection, keyboard navigation. The AI features are useful but could be smarter about schema context. And the documentation needs serious attention.</p>
<p>Beyond specific features: I want Tabularis to be the database client that developers actually enjoy using. Not the one with the most features on a comparison chart, but the one that feels fast, looks good, and gets out of your way. That&#39;s been the north star since day one, and it hasn&#39;t changed.</p>
<p>We&#39;re also approaching a plugin registry redesign, better onboarding for new contributors, and, if the community keeps growing at this pace, the possibility of Tabularis becoming more than a solo-maintained project.</p>
<hr>
<h2>Thank You</h2>
<p>1,000 stars means 1,000 people decided this project was worth remembering. Some of them went further — they opened issues, submitted PRs, translated the interface, wrote plugins, and helped shape what Tabularis is becoming.</p>
<p>A special thanks to our sponsors: <a href="https://www.serversmtp.com/?utm_source=tabularis&utm_medium=referral&utm_campaign=sponsor">turboSMTP</a>, <a href="https://www.kilo.ai/?utm_source=tabularis&utm_medium=referral&utm_campaign=sponsor">Kilo Code</a>, and <a href="https://usero.io/?utm_source=tabularis&utm_medium=referral&utm_campaign=sponsor">Usero</a>. They believed in the project early and help keep it free and independent. Their support covers development time that would otherwise come entirely out of pocket.</p>
<p>To every contributor, every user, every person who mentioned Tabularis to a colleague or dropped a link in a chat: thank you. This stopped being a solo project the moment you showed up.</p>
<p>If you want to get involved, the <a href="https://discord.com/invite/K2hmhfHRSt">Discord</a> is the fastest way in. Come say hi. There&#39;s plenty to build.</p>
<p>Here&#39;s to the next thousand.</p>
<hr>
<p><em>The Tabularis Team</em></p>
]]></content:encoded>
      <enclosure url="https://tabularis.dev/blog/from-zero-to-1000-github-stars/opengraph-image.png" type="image/png" />
      <category>community</category>
      <category>milestone</category>
      <category>open-source</category>
      <category>growth</category>
    </item>
    <item>
      <title>v0.9.16: Connection Groups, But Faster</title>
      <link>https://tabularis.dev/blog/v0916-connection-groups-drag-drop</link>
      <guid isPermaLink="true">https://tabularis.dev/blog/v0916-connection-groups-drag-drop</guid>
      <pubDate>Sun, 12 Apr 2026 09:47:47 GMT</pubDate>
      <description>v0.9.16 makes connection groups feel much more direct: drag groups to reorder them, drag connections into a different group, and get more consistent pagination behavior across built-in drivers.</description>
      <content:encoded><![CDATA[<h1>v0.9.16: Connection Groups, But Faster</h1>
<p>After the much bigger notebook release, <strong>v0.9.16</strong> is a smaller one. The focus here is on making connection organization less clunky: fewer context menu steps, more direct manipulation. If you use groups heavily, this release removes a bit of friction you&#39;ll feel immediately.</p>
<hr>
<h2>Drag and Drop for Connection Groups</h2>
<p>This is <a href="https://github.com/thomaswasle">@thomaswasle</a>&#39;s contribution in PR <a href="https://github.com/TabularisDB/tabularis/pull/126">#126</a>.</p>
<p>Connection groups on the <strong>Connections</strong> page are now draggable.</p>
<p>You can:</p>
<ul>
<li>Drag a <strong>group</strong> by its grip handle to reorder it</li>
<li>Drag a <strong>connection</strong> onto another group to move it there</li>
<li>See a visual highlight on the target group while dragging</li>
</ul>
<p>That sounds small, but it changes the workflow quite a bit. Before, reorganizing connections meant using menus and explicit move actions. Now it&#39;s the interaction you&#39;d expect: grab, drop, done.</p>
<p>For anyone with separate local, staging, production, analytics, or client-specific setups, this makes keeping the list tidy much less annoying.</p>
<hr>
<h2>Pagination Logic, Cleaned Up</h2>
<p>There is also a useful backend cleanup in the built-in drivers: pagination logic is now centralized for <strong>PostgreSQL</strong>, <strong>MySQL</strong>, and <strong>SQLite</strong> instead of each driver carrying its own slightly different implementation.</p>
<p>The practical effect is consistency. When Tabularis paginates a <code>SELECT</code>, it now preserves <code>ORDER BY</code> more safely and respects user-written <code>LIMIT</code> / <code>OFFSET</code> clauses as a cap instead of treating them differently depending on the driver.</p>
<p>This is not the kind of change that needs a screenshot, but it is the kind that prevents subtle &quot;why is page 2 weird?&quot; behavior later.</p>
<hr>
<h2>Small Follow-Up Fix</h2>
<p>A small UI follow-up also landed right after the drag-and-drop work: a corrected <code>useDatabase</code> destructure in the sidebar. Not headline material, but exactly the kind of cleanup worth shipping in a point release.</p>
<hr>
<h2>What&#39;s Next</h2>
<p>One of the next updates will be <strong>Visual Explain</strong> — the new query plan analysis workflow previewed in the <a href="https://tabularis.dev/blog/visual-explain-query-plan-analysis">dedicated blog post</a>.</p>
<hr>
<hr>
<p><em>v0.9.16 is available now. Update via the in-app updater, or download from the <a href="https://github.com/TabularisDB/tabularis/releases/tag/v0.9.16">releases page</a>.</em></p>
]]></content:encoded>
      <enclosure url="https://tabularis.dev/blog/v0916-connection-groups-drag-drop/opengraph-image.png" type="image/png" />
      <category>release</category>
      <category>connections</category>
      <category>ux</category>
      <category>community</category>
    </item>
    <item>
      <title>Visual EXPLAIN: Execution Plans You Can Actually Read</title>
      <link>https://tabularis.dev/blog/visual-explain-query-plan-analysis</link>
      <guid isPermaLink="true">https://tabularis.dev/blog/visual-explain-query-plan-analysis</guid>
      <pubDate>Fri, 10 Apr 2026 21:50:00 GMT</pubDate>
      <description>A preview of Visual EXPLAIN in Tabularis — interactive execution plan graphs, tabular breakdowns, raw JSON output, and AI-powered analysis. Works with PostgreSQL, MySQL, MariaDB, and SQLite. Still in development, with PostgreSQL as the primary focus.</description>
      <content:encoded><![CDATA[<h1>Visual EXPLAIN: Execution Plans You Can Actually Read</h1>
<p>Most of the time, you do not look at an execution plan out of curiosity. You look at it because a query is slow, a join is behaving strangely, an index is not being used, or the optimizer is doing something you did not expect. <code>EXPLAIN</code> is the tool you reach for when you need to answer a concrete question: where is the work happening, and why?</p>
<p>The problem is that execution plans are useful, but not especially pleasant to inspect in their raw form. You usually get a tree encoded as indented text, JSON meant more for machines than humans, or engine-specific output that changes depending on the server and version. To get from that output to an actual diagnosis, you have to reconstruct the shape of the plan, compare estimated and actual values, and identify which node is likely responsible for the cost.</p>
<p>Take a plan like this:</p>
<pre><code>Nested Loop Left Join  (cost=4.18..1247.32 rows=2400 width=128)
  -&gt;  Hash Join  (cost=3.75..142.56 rows=800 width=72)
        Hash Cond: (p.category_id = c.id)
        -&gt;  Seq Scan on posts p  (cost=0.00..124.00 rows=5000 width=68)
              Filter: (status = &#39;published&#39;)
        -&gt;  Hash  (cost=2.50..2.50 rows=100 width=12)
              -&gt;  Seq Scan on categories c  (cost=0.00..2.50 rows=100 width=12)
  -&gt;  Index Scan using idx_post_id on comments cm  (cost=0.29..1.35 rows=3 width=64)
        Index Cond: (post_id = p.id)
</code></pre>
<p>The information is there, but the inspection work is still mostly manual. You have to read the tree structure, compare node costs, check estimated versus actual rows, and keep the query itself in your head while doing it. On MySQL you also need to know which EXPLAIN format your server version supports. On SQLite you get a flat list with parent IDs and have to rebuild the tree mentally.</p>
<p>That is the reason for <strong>Visual EXPLAIN</strong> in Tabularis. The goal is not to replace EXPLAIN, but to make it faster to inspect. Tabularis takes the execution plan, turns it into a graph, highlights the expensive nodes, shows estimated and actual metrics side by side, and keeps the raw output available when you need it. If you want, it can also generate an AI analysis as a second pass over the plan.</p>
<p>It is still in active development on the <a href="https://github.com/TabularisDB/tabularis/tree/feat/visual-explain-analyze"><code>feat/visual-explain-analyze</code></a> branch, but it already works on PostgreSQL, MySQL, MariaDB, and SQLite.</p>
<hr>
<h2>How It Works</h2>
<p>Select a query and click the EXPLAIN button. Tabularis opens a full-screen modal, picks the right <code>EXPLAIN</code> syntax for the current database, runs it, parses the result, and exposes four views:</p>
<ul>
<li><strong>Graph</strong> — interactive node tree (ReactFlow + Dagre layout)</li>
<li><strong>Table</strong> — hierarchical tree table with a detail panel</li>
<li><strong>Raw</strong> — the original JSON/text output in Monaco</li>
<li><strong>AI Analysis</strong> — AI summarizes the plan and points out likely issues</li>
</ul>
<p>The top bar shows the main summary metrics: planning time, execution time, and total cost. You can switch views without re-running the query.</p>
<p><img src="https://tabularis.dev/img/posts/tabularis-visual-explain-graph-view-execution-plan.png" alt="Visual EXPLAIN modal with graph view showing execution plan nodes, cost heatmap, and summary bar"></p>
<hr>
<h2>The Graph View</h2>
<p>This is the default view.</p>
<p>Each operation in the plan becomes a node. Seq Scan, Index Scan, Hash Join, Nested Loop, Sort, Aggregate: the full tree is rendered as a graph, with edges showing the flow from leaf scans to the final result. Layout is computed with Dagre, so plans with multiple branches stay readable without manual positioning.</p>
<p>Each node shows:</p>
<ul>
<li><strong>Node type</strong> and <strong>relation</strong> (which table or index)</li>
<li><strong>Estimated rows</strong> and <strong>cost</strong> (startup + total)</li>
<li><strong>Actual rows, time, and loops</strong> (when ANALYZE is on)</li>
<li><strong>Filter and index conditions</strong></li>
</ul>
<p>Nodes are <strong>color-coded by relative cost</strong>: green for cheap operations, yellow for moderate ones, red for expensive ones. The scale is relative to the most expensive node in the current plan, so the expensive parts stand out immediately without having to compare raw numbers line by line.</p>
<p>The view supports zoom, pan, and fit-to-view. For larger plans, a minimap appears in the corner.</p>
<p><img src="https://tabularis.dev/img/posts/tabularis-visual-explain-graph-nodes-cost-heatmap.png" alt="Execution plan graph with color-coded nodes showing Seq Scan, Hash Join, and Sort operations"></p>
<hr>
<h2>The Table View</h2>
<p>The graph is useful for structure. The table is better when you want exact metrics.</p>
<p>On the left there is an expandable tree with columns for node type, relation, cost, estimated rows, time, and filter. Selecting a row opens a <strong>detail panel</strong> on the right with all metrics available for that node: cost breakdown, actual vs estimated rows, loops, buffer hits and reads, index conditions, hash conditions, and any engine-specific fields present in the source plan.</p>
<p>If you have used EXPLAIN in pgAdmin or DBeaver, the layout will feel familiar. The main difference is consistency: the same view model is used across PostgreSQL, MySQL, MariaDB, and SQLite.</p>
<p><img src="https://tabularis.dev/img/posts/tabularis-visual-explain-table-view-detail-panel.png" alt="Table view with hierarchical tree, cost columns, and node detail panel"></p>
<hr>
<h2>Raw Output</h2>
<p>Sometimes you just want the original payload.</p>
<p>The raw view shows the database response in a read-only Monaco editor, with syntax highlighting, wrapping, and search. No transformation, no interpretation.</p>
<p><img src="https://tabularis.dev/img/posts/tabularis-visual-explain-raw-json-output-monaco.png" alt="Raw EXPLAIN JSON output in Monaco editor with syntax highlighting"></p>
<hr>
<h2>AI Analysis</h2>
<p>The AI tab sends the query and the raw EXPLAIN output to the configured provider and returns a structured analysis: what the query is doing, where the likely bottlenecks are, which indexes may help, and which rewrites are worth testing.</p>
<p>It works with OpenAI, Anthropic, Ollama, and custom OpenAI-compatible endpoints. The response is generated in the language configured in Tabularis, which makes it more practical if you do not normally reason about plans in English.</p>
<p>It is not a replacement for knowing how to read execution plans. It is closer to a second pass over the plan, which is useful when the issue is not obvious or the query is large enough that manual inspection is slow.</p>
<p><img src="https://tabularis.dev/img/posts/tabularis-visual-explain-ai-analysis-recommendations.png" alt="AI analysis view with structured performance recommendations and optimization suggestions"></p>
<hr>
<h2>EXPLAIN vs EXPLAIN ANALYZE</h2>
<p>There is a toggle in the footer. <strong>EXPLAIN</strong> gives you the estimated plan: what the optimizer expects to happen. <strong>EXPLAIN ANALYZE</strong> executes the query and reports what actually happened, including actual rows, actual time, loop counts, and, where supported, buffer statistics.</p>
<p>The difference matters. A plan might estimate 100 rows and actually scan 100,000. You only see that with ANALYZE.</p>
<p>ANALYZE is enabled by default, except for <strong>data-modifying queries</strong>. For INSERT, UPDATE, and DELETE, the checkbox starts disabled and the UI shows a warning. <code>EXPLAIN ANALYZE</code> executes the statement, so treating those queries as read-only would be misleading.</p>
<p>DDL statements such as CREATE, DROP, ALTER, and TRUNCATE are blocked entirely. They are not valid inputs for this workflow, so Tabularis stops them before they turn into opaque database errors.</p>
<hr>
<h2>Multi-Database Support</h2>
<p>Visual EXPLAIN adapts to each engine. The differences are significant.</p>
<h3>PostgreSQL</h3>
<p>PostgreSQL is the primary target. Tabularis uses <code>EXPLAIN (FORMAT JSON, ANALYZE, BUFFERS)</code>, which gives structured output with planning time, execution time, buffer hit/read statistics, and the full node tree. All graph and table metrics are derived from that JSON.</p>
<p>This is the most complete implementation right now.</p>
<h3>MySQL</h3>
<p>MySQL is more version-dependent. Tabularis runs <code>SELECT VERSION()</code>, parses the result, and chooses the best supported format:</p>
<ul>
<li><strong>MySQL 8.0.18+</strong> — <code>EXPLAIN ANALYZE</code> (text tree with actual execution data)</li>
<li><strong>MySQL 5.6+</strong> — <code>EXPLAIN FORMAT=JSON</code> (structured plan, estimates only)</li>
<li><strong>Older versions</strong> — tabular EXPLAIN fallback</li>
</ul>
<p>There is no manual configuration for this. Detection is automatic.</p>
<h3>MariaDB</h3>
<p>MariaDB has similar capabilities, but with different syntax and fields. Tabularis detects the MariaDB version string and uses:</p>
<ul>
<li><strong>MariaDB 10.1+</strong> — <code>ANALYZE FORMAT=JSON</code> (executes the query, returns JSON with both estimated and actual <code>r_*</code> fields)</li>
<li><strong>MariaDB 10.1+</strong> — <code>EXPLAIN FORMAT=JSON</code> (estimates only)</li>
</ul>
<h3>SQLite</h3>
<p><code>EXPLAIN QUERY PLAN</code> returns a flat list of operations with parent IDs. Tabularis reconstructs the parent-child tree from that output. There are no execution metrics because SQLite does not expose an <code>ANALYZE</code>-style result here, but the structure of the plan is still useful.</p>
<hr>
<h2>Current State</h2>
<p>This feature is still under active development. Current status:</p>
<ul>
<li><strong>PostgreSQL is the most complete</strong> — full JSON parsing, all metrics, ANALYZE with buffers.</li>
<li><strong>MySQL and MariaDB work well</strong> — version detection, multiple format fallbacks, JSON and text tree parsing. Edge cases across the many server versions in the wild are the main gap.</li>
<li><strong>SQLite is basic</strong> — plan tree is there, execution metrics are not.</li>
<li><strong>Node interaction</strong> — clicking a graph node does not open the detail panel yet. Table view supports it, but the views are not linked.</li>
<li><strong>Cost bar visualization</strong> — proportional cost bars inside graph nodes, not just border colors.</li>
<li><strong>Plan comparison</strong> — EXPLAIN before and after a query or index change, side by side. Planned, not implemented yet.</li>
<li><strong>Plugin drivers</strong> — community database drivers can implement <code>explain_query</code> in the driver trait and get Visual EXPLAIN for free.</li>
</ul>
<hr>
<h2>Contributions Welcome</h2>
<p>This is open source, and there are several places where contributions would be useful.</p>
<p>If you know PostgreSQL internals well, the parser can expose more node-specific details such as parallel workers, CTE scans, and materialization hints. If you use MySQL or MariaDB heavily and run into parsing edge cases, bug reports with the raw EXPLAIN output are especially helpful. If you want to work on visualization, there is room for better layouts, cost distribution views, and comparison workflows.</p>
<p>The driver trait defines a standard <code>explain_query</code> interface. Any community plugin that implements it gets Visual EXPLAIN automatically.</p>
<p>Development is happening on the <a href="https://github.com/TabularisDB/tabularis/tree/feat/visual-explain-analyze"><code>feat/visual-explain-analyze</code></a> branch. Issues and PRs are welcome on the <a href="https://github.com/TabularisDB/tabularis">GitHub repository</a>.</p>
<hr>
<h2>Why This Matters</h2>
<p>Every database has some form of EXPLAIN. Very few database clients make it easy to work with.</p>
<p>In practice you usually get raw text, a table with overloaded columns, or a static tree that is hard to inspect once plans get larger. The underlying data is useful. The presentation is usually not.</p>
<p>The result is predictable: people delay looking at the plan until a query is already causing trouble, and then they spend time decoding output instead of fixing the actual problem.</p>
<p>Visual EXPLAIN is an attempt to make that workflow shorter. Run the query, inspect the plan in the same client, identify the expensive nodes, and iterate without copying JSON into external tools or switching to another application.</p>
<p>It is landing soon. Progress is tracked on <a href="https://github.com/TabularisDB/tabularis/tree/feat/visual-explain-analyze"><code>feat/visual-explain-analyze</code></a>.</p>
<hr>
<h2>Try It Yourself</h2>
<p>If you want to test Visual EXPLAIN on MySQL or MariaDB, I put together a demo database and a Tabularis notebook with more than 25 queries covering table scans, index access patterns, joins, subqueries, CTEs, aggregations, UNIONs, and deliberately expensive cases.</p>
<ul>
<li><a href="https://tabularis.dev/docs/explain-demo-database.sql"><code>explain-demo-database.sql</code></a> — MySQL/MariaDB schema with ~15k rows across 8 tables, a mix of indexed and unindexed columns</li>
<li><a href="https://tabularis.dev/docs/explain-showcase.tabularis-notebook"><code>explain-showcase.tabularis-notebook</code></a> — importable notebook with annotated queries, one per optimizer strategy</li>
</ul>
<p>Run the SQL file on your server, import the notebook into Tabularis, and click Explain on any cell.</p>
]]></content:encoded>
      <enclosure url="https://tabularis.dev/blog/visual-explain-query-plan-analysis/opengraph-image.png" type="image/png" />
      <category>feature</category>
      <category>explain</category>
      <category>performance</category>
      <category>ai</category>
      <category>postgresql</category>
      <category>mysql</category>
      <category>sqlite</category>
      <category>preview</category>
    </item>
    <item>
      <title>v0.9.15: The Notebook Release</title>
      <link>https://tabularis.dev/blog/v0915-notebooks-multi-query-ai-rename</link>
      <guid isPermaLink="true">https://tabularis.dev/blog/v0915-notebooks-multi-query-ai-rename</guid>
      <pubDate>Wed, 08 Apr 2026 18:00:00 GMT</pubDate>
      <description>SQL Notebooks land with cell references, inline charts, parameters, AI naming, and HTML export. The query editor gains multi-query execution with tabbed and stacked results, AI-powered tab renaming, and query splitting gets a proper parser.</description>
      <content:encoded><![CDATA[<h1>v0.9.15: The Notebook Release</h1>
<p>This is the one where Tabularis stops being just a query editor. SQL Notebooks — the feature previewed in the <a href="https://tabularis.dev/blog/notebooks-sql-analysis-reimagined">dedicated blog post</a> a few days ago — ship in this release. Alongside them, the regular query editor gets a major upgrade: multi-query execution with tabbed and stacked results, AI-powered tab naming, and a proper query splitter that finally handles edge cases correctly.</p>
<hr>
<h2>SQL Notebooks</h2>
<p>The headline feature. A full notebook environment inside Tabularis — no Jupyter kernel, no Python runtime, no context switching.</p>
<p>A notebook is a sequence of <strong>SQL</strong> and <strong>Markdown</strong> cells. SQL cells run against your database and show results inline — the same data grid, sorting, and filtering you already know from the query editor. Markdown cells let you document your analysis between queries.</p>
<p>But the real power is in what connects the cells:</p>
<p><strong>Cell references</strong> — any SQL cell can reference a previous cell&#39;s query using <code>{{cell_N}}</code> syntax. Tabularis resolves this at execution time by wrapping the referenced query in a CTE. Change the base query, re-run the downstream cells, everything stays consistent.</p>
<p><strong>Inline charts</strong> — bar, line, and pie charts render directly inside SQL cells. Pick a label column and value columns, and the chart updates live. Not a BI tool replacement — a quick visual check while you explore.</p>
<p><strong>Parameters</strong> — define <code>@start_date = &#39;2024-01-01&#39;</code> once, use it across every cell. Change the value, re-run, every query picks it up.</p>
<p><strong>Parallel execution</strong> — mark independent cells with the lightning bolt icon and they fire concurrently during Run All instead of waiting in line.</p>
<p><strong>AI integration</strong> — each cell has AI Generate and Explain buttons. The sparkles icon generates a descriptive name based on cell content, which feeds into the <strong>outline panel</strong> — a navigable table of contents for long notebooks.</p>
<p><strong>Persistence and export</strong> — notebooks auto-save to disk as <code>.tabularis-notebook</code> JSON files. Export as HTML for a standalone, dark-themed document ready to share. Import/export <code>.tabularis-notebook</code> files to collaborate with colleagues.</p>
<p>For the full walkthrough — cell management, execution history, multi-database queries, keyboard shortcuts — see the <a href="https://tabularis.dev/wiki/notebooks">SQL Notebooks wiki page</a>.</p>
<p><img src="https://tabularis.dev/img/posts/tabularis-notebook-sql-cell-pie-chart-data-grid.png" alt="SQL Notebook with cells, results, and pie chart"></p>
<hr>
<h2>Multi-Query Execution</h2>
<p>Run a script with multiple semicolon-separated queries and instead of a modal asking you to pick one, Tabularis now executes <strong>all of them</strong> and shows results in a dedicated multi-result panel.</p>
<p>The result panel ships with <strong>two view modes</strong>, switchable via a toggle button:</p>
<p><strong>Tab view</strong> (default) — each query gets its own tab with:</p>
<ul>
<li><strong>Collapsible query preview</strong> — expand any tab to see the SQL that produced it.</li>
<li><strong>Tab context menu</strong> — right-click to close, close others, or close tabs to the right.</li>
<li><strong>Inline rename</strong> — double-click a tab name to rename it.</li>
<li><strong>AI rename</strong> — click the sparkles icon to let AI generate a descriptive name based on the query content.</li>
<li><strong>Scrollable tabs</strong> — when you have more results than fit, tabs scroll horizontally.</li>
</ul>
<p><strong>Stacked view</strong> — inspired by SQL Server Management Studio, all results are displayed vertically in a single scrollable panel. No tab switching — every result set is visible at once.</p>
<ul>
<li><strong>Collapsible sections</strong> — click any result header to collapse or expand it. A top-bar button collapses or expands all at once.</li>
<li><strong>Resizable</strong> — drag the handle between results to adjust the height of each data grid.</li>
<li><strong>Full per-result actions</strong> — rename, AI rename, re-run, and close are available on each result header, just like in tab view.</li>
<li><strong>Compact metadata</strong> — row count, execution time, pagination controls, and auto-paginated badges are inline in the header so nothing is hidden.</li>
</ul>
<p>If your script uses parameters (<code>:param</code> syntax), Tabularis prompts for values once before running all queries.</p>
<p>The query selection modal also got a revamp — you can now run a single query from the list without switching to single-query mode.</p>
<hr>
<h2>Query Splitting: Done Right</h2>
<p>This is <a href="https://github.com/dev-void-7">@dev-void-7</a>&#39;s contribution in PR <a href="https://github.com/TabularisDB/tabularis/pull/119">#119</a>. The old regex-based query splitter had blind spots — string literals containing semicolons, dollar-quoted blocks in PostgreSQL, comments with semicolons — all would trip it up and split queries in the wrong places.</p>
<p>The fix replaces the custom splitter with <a href="https://github.com/nickytonline/dbgate-query-splitter"><code>dbgate-query-splitter</code></a>, a proper parser that understands SQL dialects. It correctly handles:</p>
<ul>
<li>Semicolons inside string literals and comments</li>
<li>PostgreSQL <code>$$</code>-quoted blocks</li>
<li>MySQL <code>DELIMITER</code> statements</li>
<li>Nested <code>BEGIN...END</code> blocks</li>
</ul>
<p>This was a prerequisite for multi-query execution to work reliably, and it quietly fixes a class of bugs that affected the existing query selection modal too.</p>
<hr>
<h2>AI Tab Rename</h2>
<p>The AI features in Tabularis extend to a new place: tab naming. Click the AI icon on any editor tab and Tabularis generates a descriptive name based on the query content. Instead of staring at &quot;Query 1&quot;, &quot;Query 2&quot;, &quot;Query 3&quot;, you get names that actually describe what each tab does.</p>
<p>The prompt is customizable in <strong>Settings &gt; AI &gt; Tab Rename Prompt</strong>.</p>
<hr>
<hr>
<p><em>v0.9.15 is available now. Update via the in-app updater, or download from the <a href="https://github.com/TabularisDB/tabularis/releases/tag/v0.9.15">releases page</a>.</em></p>
]]></content:encoded>
      <enclosure url="https://tabularis.dev/blog/v0915-notebooks-multi-query-ai-rename/opengraph-image.png" type="image/png" />
      <category>release</category>
      <category>notebooks</category>
      <category>multi-query</category>
      <category>ai</category>
      <category>community</category>
    </item>
    <item>
      <title>v0.9.14: Always Connected</title>
      <link>https://tabularis.dev/blog/v0914-health-check-postgres-types-modals</link>
      <guid isPermaLink="true">https://tabularis.dev/blog/v0914-health-check-postgres-types-modals</guid>
      <pubDate>Tue, 07 Apr 2026 22:12:00 GMT</pubDate>
      <description>Connection health monitoring lands, PostgreSQL gains 40+ new types from network to geometry, modals get a searchable type picker and keyboard navigation, and autocompletion finally respects multi-database contexts.</description>
      <content:encoded><![CDATA[<h1>v0.9.14: Always Connected</h1>
<p>The theme of this release is reliability. Tabularis now monitors every open connection in the background and tells you when something drops. The PostgreSQL type system takes another massive leap forward — this time covering network addresses, geometric shapes, full-text search, and a dozen more families. And the DDL modals get the UX pass they&#39;ve needed for a while.</p>
<hr>
<h2>Connection Health Check</h2>
<p>This is the headline feature. Previously, if a database connection dropped — network hiccup, server restart, idle timeout — you wouldn&#39;t find out until your next query failed. Now Tabularis knows before you do.</p>
<p>A background Tokio loop pings every active connection at a configurable interval (default: 30 seconds). Built-in drivers use a pool-level ping — no query is executed, no overhead. Plugin drivers receive a <code>ping</code> JSON-RPC call; if the plugin hasn&#39;t implemented it, Tabularis falls back to <code>test_connection</code> automatically.</p>
<p>After <strong>2 consecutive failures</strong>, the connection is closed, any SSH tunnel is torn down, and a toast notification tells you what happened with a button to jump to the Connections page.</p>
<p>The interval is configurable from <strong>Settings → General → Connection Health Check</strong> — a slider from 0 to 120 seconds. Setting it to 0 disables pings entirely.</p>
<p>For plugin authors: implementing <code>ping</code> is optional but recommended if your driver can do a cheaper liveness check than a full <code>test_connection</code>. The <a href="https://github.com/TabularisDB/tabularis/blob/main/plugins/PLUGIN_GUIDE.md">Plugin Guide</a> and the <a href="https://tabularis.dev/wiki/plugins#ping-optional">wiki</a> document the protocol and fallback behavior.</p>
<hr>
<h2>PostgreSQL: 40+ New Types</h2>
<p>This is <a href="https://github.com/dev-void-7">@dev-void-7</a>&#39;s second major contribution in a row. After range, multirange, and enum support in v0.9.13, PR <a href="https://github.com/TabularisDB/tabularis/pull/114">#114</a> lands with binary-level deserialization for an enormous set of PostgreSQL types that were previously opaque. The implementation lives in a dedicated <code>advanced_types.rs</code> module — over 1,500 lines of Rust, each type parsed from its binary wire format and serialized to a human-readable JSON representation.</p>
<h3>What&#39;s new</h3>
<p><strong>Network types</strong> — <code>INET</code>, <code>CIDR</code>, <code>MACADDR</code>, <code>MACADDR8</code>. IP addresses display as <code>192.168.1.0/24</code>, MAC addresses as <code>aa:bb:cc:dd:ee:ff</code>.</p>
<p><strong>Geometric types</strong> — <code>POINT</code>, <code>LSEG</code>, <code>BOX</code>, <code>PATH</code>, <code>POLYGON</code>, <code>LINE</code>, <code>CIRCLE</code>. Each renders in standard PostgreSQL notation: <code>(x, y)</code> for points, <code>&lt;(cx, cy), r&gt;</code> for circles, open <code>[...]</code> or closed <code>(...)</code> for paths.</p>
<p><strong>Full-text search types</strong> — <code>TSVECTOR</code> and <code>TSQUERY</code>. Vectors display lexemes with positions and weights. Queries are fully parsed as expression trees — operators, phrase matches, prefix wildcards, weight filters — and serialized back to readable query syntax.</p>
<p><strong>Time types</strong> — <code>TIMETZ</code> with timezone offset, <code>INTERVAL</code> with full decomposition into years, months, days, hours, minutes, seconds, and microseconds. Overflow is normalized correctly (e.g. 25 hours → 1 day + 1 hour).</p>
<p><strong>Money, XML, RefCursor, JsonPath</strong> — <code>MONEY</code> renders as a number (stored as i64 cents), <code>XML</code> and <code>REFCURSOR</code> as strings, <code>JSONPATH</code> strips the internal version prefix and returns the path expression.</p>
<p><strong>System internals</strong> — <code>PG_LSN</code> (log sequence numbers), <code>TXID_SNAPSHOT</code> / <code>PG_SNAPSHOT</code> (transaction snapshots), <code>AclItem</code>, <code>PgNodeTree</code>, and various statistics types (<code>PG_MCV_LIST</code>, <code>PG_DEPENDENCIES</code>, <code>PG_NDISTINCT</code>).</p>
<p><strong>Object identifiers</strong> — <code>XID</code>, <code>CID</code>, <code>XID8</code>, <code>TID</code>, and all <code>REG*</code> types (<code>REGCLASS</code>, <code>REGTYPE</code>, <code>REGPROC</code>, etc.) — returned as their underlying OID or formatted string.</p>
<p><strong>Bit types</strong> — <code>BIT</code> and <code>VARBIT</code>, decoded to human-readable binary strings with correct padding.</p>
<p>Along the way, the type picker in the schema editor also gained these types — grouped by category (<code>network</code>, <code>geometric</code>, <code>fulltext</code>, <code>xml</code>, <code>system</code>, <code>reg</code>) — plus extension-aware types: <code>HSTORE</code>, <code>LTREE</code>/<code>LQUERY</code>/<code>LTXTQUERY</code>, <code>CITEXT</code>, PostGIS types (<code>GEOMETRY</code>, <code>GEOGRAPHY</code>, and common <code>GEOMETRY(...)</code> variants), <code>INTARRAY</code>, and common array variants (<code>INTEGER[]</code>, <code>TEXT[]</code>, <code>UUID[]</code>, <code>JSONB[]</code>, etc.).</p>
<p>This is the kind of work that turns &quot;PostgreSQL support&quot; from a marketing bullet into a real claim.</p>
<hr>
<h2>Searchable Type Picker in DDL Modals</h2>
<p>The raw HTML <code>&lt;select&gt;</code> elements in the Create Table and Modify Column modals are gone. They&#39;ve been replaced with the custom searchable <code>Select</code> component — the same one used elsewhere in the app. Start typing and the list filters instantly.</p>
<p>This matters more than it sounds. With the PostgreSQL type list now exceeding 100 entries (thanks to the expansion above), scrolling through a native dropdown was painful. Now you type &quot;geo&quot; and see <code>GEOMETRY</code>, <code>GEOGRAPHY</code>, <code>POINT</code>, <code>POLYGON</code> — nothing else.</p>
<p>The column type column in the Create Table modal table has also been widened to accommodate longer type names like <code>GEOMETRY(Point, 4326)</code>.</p>
<hr>
<h2>Column Type Parsing and Extension Tracking</h2>
<p>A new utility module handles the round-trip between database-reported type strings and the form fields in the schema editor. <code>parseColumnType</code> correctly splits <code>VARCHAR(255)</code> into type + length, but knows not to strip the parenthesized arguments from PostGIS types like <code>GEOMETRY(Point, 4326)</code> where the parentheses are part of the type name.</p>
<p><code>buildColumnDefinition</code> does the reverse — composing a backend-compatible string from form data.</p>
<p><code>getRequiredExtensions</code> scans the columns in a table and returns the deduplicated list of PostgreSQL extensions they need (e.g. <code>[&quot;postgis&quot;, &quot;hstore&quot;]</code>). Each type in the picker now carries an optional <code>requires_extension</code> field so the UI can surface this information before you apply the DDL.</p>
<hr>
<h2>SMALLSERIAL Auto-Increment</h2>
<p>A missing branch in the PostgreSQL DDL generator. When you enabled auto-increment on a <code>SMALLINT</code> column, Tabularis would emit <code>SERIAL</code> instead of <code>SMALLSERIAL</code> — silently creating a column with a larger type than intended. Fixed. The logic now correctly maps <code>SMALLINT → SMALLSERIAL</code>, <code>INTEGER → SERIAL</code>, <code>BIGINT → BIGSERIAL</code>.</p>
<p>The UI also got a sync pass: enabling auto-increment now forces <code>NOT NULL</code> checked and disabled (serial columns are always NOT NULL) and clears the default value field.</p>
<hr>
<h2>Query Selection Modal: Keyboard Navigation</h2>
<p>The modal that appears when you execute a script with multiple semicolon-separated queries now supports full keyboard navigation:</p>
<ul>
<li><strong>Arrow Up / Down</strong> — move focus between queries, with auto-scroll</li>
<li><strong>Enter</strong> — execute the focused query</li>
<li><strong>Number keys 1–9</strong> — directly execute query N</li>
</ul>
<p>The focused item gets a blue border and a numbered badge. Mouse hover syncs with keyboard focus. All strings are translated (English, Spanish, Italian, Chinese).</p>
<hr>
<h2>Autocompletion Fix for Multiple Databases</h2>
<p>This is <a href="https://github.com/thomaswasle">@thomaswasle</a>&#39;s contribution in PR <a href="https://github.com/TabularisDB/tabularis/pull/115">#115</a>. The Monaco autocomplete provider was always using the top-level table list, ignoring which schema or databases were actually active. If you had a PostgreSQL connection with a non-default schema selected, or a MySQL connection with multiple databases checked, the suggestions were wrong.</p>
<p>The fix computes the effective table list based on context: schema tables for schema-aware connections, the union of selected database tables for multi-db connections, or the global list as fallback. The <code>useEffect</code> dependency array is corrected to re-register autocomplete when context changes.</p>
<hr>
<h2>ORDER BY Without Swallowing LIMIT / OFFSET</h2>
<p>When you click a column header to sort, Tabularis injects or replaces an <code>ORDER BY</code> clause in your query. The string manipulation was naive — it would slice from <code>ORDER BY</code> to the end of the query, destroying any <code>LIMIT</code> or <code>OFFSET</code> you had written. Now it detects trailing <code>LIMIT</code>/<code>OFFSET</code> clauses and preserves them. Fixed across all three built-in drivers (PostgreSQL, MySQL, SQLite).</p>
<hr>
<h2>Smaller Changes</h2>
<ul>
<li><strong>CI/CD</strong> — GitHub Actions bumped across all workflows: <code>actions/checkout</code> v6, <code>actions/setup-node</code> v6, <code>pnpm/action-setup</code> v5, <code>swatinem/rust-cache</code> v2.9.1.</li>
<li><strong>Driver fallback hardened</strong> — the <code>ping</code> fallback now catches both <code>&quot;Method not found&quot;</code> and <code>&quot;not implemented&quot;</code> error strings from plugins.</li>
<li><strong>Test coverage</strong> — new tests for <code>TsQuery</code>, <code>TsVector</code>, advanced types, and the column type parsing utilities (273 lines).</li>
</ul>
<hr>
<hr>
<p><em>v0.9.14 is available now. Update via the in-app updater, or download from the <a href="https://github.com/TabularisDB/tabularis/releases/tag/v0.9.14">releases page</a>.</em></p>
]]></content:encoded>
      <enclosure url="https://tabularis.dev/blog/v0914-health-check-postgres-types-modals/opengraph-image.png" type="image/png" />
      <category>release</category>
      <category>postgres</category>
      <category>health-check</category>
      <category>modals</category>
      <category>community</category>
    </item>
    <item>
      <title>Notebooks: SQL Analysis, Reimagined</title>
      <link>https://tabularis.dev/blog/notebooks-sql-analysis-reimagined</link>
      <guid isPermaLink="true">https://tabularis.dev/blog/notebooks-sql-analysis-reimagined</guid>
      <pubDate>Fri, 03 Apr 2026 10:00:00 GMT</pubDate>
      <description>A look at what&apos;s coming: a full notebook environment inside Tabularis — SQL and markdown cells, inline charts, cell references, parameters, parallel execution, and AI assistance. Still in development, but the shape is clear.</description>
      <content:encoded><![CDATA[<h1>Notebooks: SQL Analysis, Reimagined</h1>
<p>Every database client has a query editor. You write SQL, you run it, you see a table. If you need to build on that result, you copy-paste it into the next query. If you need a chart, you export to CSV and open a spreadsheet. If you want to document your analysis, you paste queries into a Notion doc and hope nothing drifts out of sync.</p>
<p>I have been doing this for years, and it has always felt like the wrong workflow. The database client knows your data. It knows your schema. It has the connection. Why should analysis happen somewhere else?</p>
<p>That question has been driving the next big feature in Tabularis: <strong>Notebooks</strong>. A cell-based environment for SQL analysis, built directly into the app. No Jupyter kernel to configure, no Python runtime to install, no context switching.</p>
<p>This is still a work in progress — not shipped yet, not final. But the core is functional, the pieces are connecting, and I want to share where this is heading and why I think it matters.</p>
<p><video src="https://tabularis.dev/videos/posts/tabularis-notebooks.mp4" controls muted playsinline loop></video></p>
<hr>
<h2>The Idea</h2>
<p>A notebook is a sequence of <strong>cells</strong>. Each cell is either SQL or markdown.</p>
<p>SQL cells run against your database and show results inline — the same data grid you already know from the query editor, with sorting, filtering, and resizable result panels. Markdown cells let you document what you are doing, why, and what the results mean. Together they create a single, self-contained analysis document.</p>
<p>But cells are just the foundation. The interesting part is what connects them.</p>
<p><img src="https://tabularis.dev/img/posts/tabularis-notebook-sql-cell-pie-chart-data-grid.png" alt="Tabularis notebook with SQL cell, data grid results, and inline pie chart"></p>
<hr>
<h2>Cell References: Queries That Build on Queries</h2>
<p>This is the feature I am most excited about.</p>
<p>Any SQL cell can reference a previous cell&#39;s query using <code>{{cell_N}}</code> syntax. Tabularis resolves this at execution time by wrapping the referenced query in a CTE:</p>
<pre><code class="language-sql">-- Cell 1: Base query
SELECT customer_id, SUM(amount) AS total
FROM orders
GROUP BY customer_id

-- Cell 3: References Cell 1
SELECT * FROM {{cell_1}} WHERE total &gt; 1000
</code></pre>
<p>When Cell 3 runs, Tabularis rewrites it to:</p>
<pre><code class="language-sql">WITH cell_1 AS (
  SELECT customer_id, SUM(amount) AS total
  FROM orders
  GROUP BY customer_id
)
SELECT * FROM cell_1 WHERE total &gt; 1000
</code></pre>
<p>No temp tables, no copy-paste, no drift. Change the base query, re-run the downstream cells, everything stays consistent. You can chain references across multiple cells to build complex analyses step by step, and each intermediate result remains visible and inspectable.</p>
<p><img src="https://tabularis.dev/img/posts/tabularis-notebook-cell-references-cte-query-chaining.png" alt="Two SQL cells with cell reference — Cell 11 filters results from Cell 10 using CTE syntax"></p>
<hr>
<h2>Inline Charts</h2>
<p>Every SQL result with at least two columns and one row can be visualized as a chart — directly inside the cell.</p>
<p>Three chart types so far: <strong>bar</strong>, <strong>line</strong>, and <strong>pie</strong>. You pick a label column and one or more value columns. Bar and line charts support multi-series. Pie charts take a single series. The chart configuration is saved with the cell — close the notebook, reopen it, the chart is still there.</p>
<p>This is not meant to compete with dedicated BI tools. It is for the moment when you are exploring data and want a quick visual confirmation of a pattern before writing the next query. That moment used to mean leaving the database client. Now it does not.</p>
<div class="post-gallery">

<p><img src="https://tabularis.dev/img/posts/tabularis-notebook-bar-chart-column-selector.png" alt="SQL cell with bar chart and label column selector dropdown open showing chart configuration"></p>
<p><img src="https://tabularis.dev/img/posts/tabularis-notebook-line-chart-pie-chart-variety.png" alt="Pie chart and line chart in separate notebook cells showing chart type variety"></p>
</div>

<hr>
<h2>Parameters</h2>
<p>Define a parameter once, use it across every cell:</p>
<pre><code>@start_date = &#39;2024-01-01&#39;
@end_date   = &#39;2024-12-31&#39;
@min_amount = 500
</code></pre>
<p>Any SQL cell containing <code>@start_date</code> will have it substituted before execution. Change the value, re-run, every query picks up the new value.</p>
<p>Particularly useful for recurring analyses — monthly reports, cohort comparisons, threshold testing — where the logic stays the same but the inputs change.</p>
<p><img src="https://tabularis.dev/img/posts/tabularis-notebook-parameters-panel-variables.png" alt="Notebook parameters panel with productCategory and orderStatus variables defined"></p>
<hr>
<h2>Parallel Execution</h2>
<p>Not every cell in a notebook depends on the previous one. Some are independent queries — pulling data from different tables, running complementary aggregations that do not reference each other.</p>
<p>Mark a cell with the lightning bolt icon, and it runs <strong>concurrently</strong> during &quot;Run All&quot; instead of waiting for the cells above it to finish. For notebooks with heavy queries against different datasets, this can cut total execution time significantly.</p>
<p>Sequential cells still run in order. Parallel cells fire together. You control it per cell, one click.</p>
<p><img src="https://tabularis.dev/img/posts/tabularis-notebook-parallel-execution-concurrent-cells.png" alt="Two SQL cells with parallel execution lightning bolt icons enabled for concurrent running"></p>
<hr>
<h2>Stop-on-Error and Run All</h2>
<p>Hit <strong>Run All</strong> (or <code>Ctrl+Shift+Enter</code>) and every SQL cell executes top to bottom. The <strong>Stop on Error</strong> toggle controls whether execution halts at the first failure or powers through the rest of the notebook.</p>
<p>After execution, a <strong>summary card</strong> shows succeeded, failed, and skipped cells. Failed cells are clickable — tap one and the notebook scrolls straight to it. No hunting through a long document for the red cell.</p>
<hr>
<h2>Multi-Database Queries</h2>
<p>If you have multiple databases connected, each SQL cell can target a different one. A dropdown in the cell header lets you pick the schema — pull data from production PostgreSQL in one cell and compare it with your analytics SQLite in the next, all within the same notebook.</p>
<p>Works across MySQL, MariaDB, PostgreSQL, and SQLite.</p>
<p><img src="https://tabularis.dev/img/posts/tabularis-notebook-multi-database-selector-dropdown.png" alt="SQL cell with database selector dropdown showing multiple MySQL, PostgreSQL, and SQLite connections"></p>
<hr>
<h2>Execution History</h2>
<p>Every SQL cell keeps a record of its last 10 executions — timestamp, execution time, row count. Expand the history panel, pick any previous run, and <strong>restore</strong> that query version. Useful when you have been iterating on a cell and want to go back, or when you need to compare current results against an earlier run.</p>
<p><img src="https://tabularis.dev/img/posts/tabularis-notebook-execution-history-panel.png" alt="Execution history panel showing timestamp, duration, and row count for previous query runs"></p>
<hr>
<h2>AI Integration</h2>
<p>Notebooks plug into the AI features that already exist in Tabularis, in two ways.</p>
<p><strong>In the editor</strong>: each SQL cell has &quot;AI&quot; and &quot;Explain&quot; buttons. &quot;AI&quot; opens the generation modal — describe what you want, get SQL back. &quot;Explain&quot; takes the current query and breaks it down. Same tools available in the main query editor, now available per cell.</p>
<p><strong>Cell naming</strong>: click the sparkles icon in any cell header, and AI generates a descriptive name based on the cell content. Named cells appear in the <strong>notebook outline</strong> — a collapsible panel that shows all cell names and markdown headings as a table of contents. For long notebooks, this turns a wall of anonymous cells into a navigable document.</p>
<div class="post-gallery">

<p><img src="https://tabularis.dev/img/posts/tabularis-notebook-ai-explain-buttons-history.png" alt="SQL cell with AI and Explain buttons, execution history, and collapsed cells overview"></p>
<p><img src="https://tabularis.dev/img/posts/tabularis-notebook-outline-panel-cell-navigation.png" alt="Notebook outline panel with AI-generated cell names and markdown headings as table of contents"></p>
</div>

<hr>
<h2>Organization</h2>
<p>Notebooks can grow. To keep them manageable:</p>
<ul>
<li><strong>Collapse</strong> any cell to hide its body and results, showing just the header.</li>
<li><strong>Drag and drop</strong> cells to reorder them.</li>
<li><strong>Cell names</strong> (manual or AI-generated) give each cell identity beyond &quot;Cell 7.&quot;</li>
<li><strong>Markdown cells</strong> act as section headers and documentation between query groups.</li>
</ul>
<p><img src="https://tabularis.dev/img/posts/tabularis-notebook-collapsed-expanded-cells-organization.png" alt="Notebook with collapsed and expanded SQL and markdown cells showing organization"></p>
<hr>
<h2>Import and Export</h2>
<p>Two export formats are taking shape:</p>
<p><strong><code>.tabularis-notebook</code></strong> — a JSON file containing the notebook structure: cells, parameters, chart configurations, cell names. No results or runtime data — it is a template. Share it with a colleague, they import it, connect to their database, and run it.</p>
<p><strong>HTML export</strong> — a self-contained HTML document with rendered markdown, SQL highlighting, and result tables embedded. Dark-themed, ready to share or archive. The &quot;send it to your manager&quot; format.</p>
<p>Individual cell results can also be exported as <strong>CSV</strong> or <strong>JSON</strong> from the result toolbar.</p>
<hr>
<h2>What Is Still Cooking</h2>
<p>This is an honest preview. Some things work, some things need polish, some things might change before release. A few areas I am still iterating on:</p>
<ul>
<li><strong>Performance with large notebooks</strong> — 30+ cells with heavy results need more work on virtualization and memory management.</li>
<li><strong>Cell reference validation</strong> — right now circular references are not caught before execution. That needs a proper dependency graph.</li>
<li><strong>Chart customization</strong> — axis labels, color palettes, and data formatting are minimal. Enough to explore, not enough to present.</li>
<li><strong>Keyboard navigation</strong> — moving between cells, adding cells, running cells — all of this should be possible without touching the mouse. Partially there, not fully.</li>
<li><strong>Undo/redo at the notebook level</strong> — cell-level undo works (it is Monaco), but notebook-level operations like reorder and delete are not undoable yet.</li>
</ul>
<p>I would rather ship this with rough edges acknowledged than wait for perfection. The core value — connected, documented, visual SQL analysis inside the database client — is already there. The rest is refinement.</p>
<hr>
<h2>Why This Matters</h2>
<p>Database clients have been stuck in a loop: connect, query, look at a table, repeat. Analysis tooling evolved — Jupyter, Observable, dbt — but the database client stayed behind. You still need to leave it the moment your work goes beyond a single query.</p>
<p>Notebooks in Tabularis are a bet that the database client is the right place for exploratory SQL analysis. You already have the connection. You already have the schema. You already have autocomplete and query history. Adding cells, charts, references, and parameters on top of that foundation means the entire workflow — from first query to shareable report — can happen without switching tools.</p>
<p>This is not a Jupyter replacement. There is no Python, no R, no arbitrary code execution. It is purpose-built for SQL. And for the kind of work most people actually do with their database every day — ad-hoc exploration, report building, data validation, performance investigation — that focus is a feature, not a limitation.</p>
<p>Stay tuned. This is landing soon.</p>
]]></content:encoded>
      <enclosure url="https://tabularis.dev/blog/notebooks-sql-analysis-reimagined/opengraph-image.png" type="image/png" />
      <category>feature</category>
      <category>notebooks</category>
      <category>sql</category>
      <category>ai</category>
      <category>charts</category>
      <category>analysis</category>
      <category>preview</category>
    </item>
    <item>
      <title>v0.9.13: Plugins Meet the Interface</title>
      <link>https://tabularis.dev/blog/v0913-ui-extensions-postgres-types-settings</link>
      <guid isPermaLink="true">https://tabularis.dev/blog/v0913-ui-extensions-postgres-types-settings</guid>
      <pubDate>Thu, 02 Apr 2026 11:33:00 GMT</pubDate>
      <description>UI Extensions land in production, PostgreSQL gains range, multirange, and enum support, and the Settings page gets a full redesign. Plus two new driver capabilities for plugin authors.</description>
      <content:encoded><![CDATA[<h1>v0.9.13: Plugins Meet the Interface</h1>
<p>This release closes the loop on something I&#39;ve been building toward for a while. Plugins can now render their own UI inside Tabularis — not through hacks or workarounds, but through a proper extension system with typed contracts and error isolation. Alongside that, the PostgreSQL driver picks up three new type families, and the Settings page has been completely rearchitected.</p>
<hr>
<h2>Plugin UI Extensions: From Spec to Production</h2>
<p>Back in v0.9.10 I published the <a href="https://tabularis.dev/docs/plugin-ui-extensions-spec.md">spec for UI extensions</a>. The idea was straightforward: define named insertion points (slots) in the host interface, let plugins register React components against those slots, and have the host render them with the right context. No iframes, no eval, no DOM hacking — just controlled React components inside controlled boundaries.</p>
<p>v0.9.13 ships the full implementation.</p>
<h3>What&#39;s in the box</h3>
<p><strong>Ten slots</strong> across the application — from individual field annotations in the row editor, to toolbar buttons, context menu items, sidebar actions, settings panels, and connection form extensions. Each slot passes a typed context object to the plugin component: connection metadata, table name, row data, column info, whatever makes sense for that location. The full list and context shapes are documented in the <a href="https://tabularis.dev/wiki/plugins#ui-extensions-phase-2">plugin wiki</a> and the <a href="https://github.com/TabularisDB/tabularis/blob/main/plugins/PLUGIN_GUIDE.md">PLUGIN_GUIDE</a>.</p>
<p><strong>A set of host-provided hooks</strong> that give slot components access to host capabilities: running read-only queries, showing toasts, opening modals, reading and writing plugin settings, detecting the current theme, accessing plugin-specific translations, and opening external URLs. Right now these hooks are exposed by the host as a runtime global (<code>__TABULARIS_API__</code>) — plugin bundles declare it as an external and reference it at build time. The plan is to publish a proper <code>@tabularis/plugin-api</code> package on npm so that plugin authors get type definitions, autocompletion, and a cleaner import experience out of the box.</p>
<p><strong>IIFE bundles</strong> as the delivery format. Plugin authors build their components with Vite (or any bundler), targeting IIFE output with React and the host API as externals. The host provides these as globals — no duplicate React instances, no version conflicts.</p>
<p><strong>Error isolation</strong> via per-contribution error boundaries. If a plugin component throws, a small badge appears in its place. The rest of the app keeps running. Other plugins in the same slot keep rendering.</p>
<p><strong>Conditional rendering</strong> through two mechanisms: a <code>driver</code> field in the manifest that restricts a contribution to connections using a specific driver, and component-level filtering where the component itself returns <code>null</code> based on context values.</p>
<p>For the full developer reference, see the <a href="https://github.com/TabularisDB/tabularis/blob/main/plugins/PLUGIN_GUIDE.md">Plugin Guide</a>. The <a href="https://tabularis.dev/wiki/plugins">wiki page on the plugin system</a> has also been updated with the complete slot list, available hooks, and build instructions.</p>
<h3>UI-only plugins and plugin modals</h3>
<p>Plugins no longer need to be database drivers. v0.9.13 introduces support for <strong>UI-only plugins</strong> — packages that declare <code>ui_extensions</code> in their manifest without providing an executable or driver capabilities. This opens the door to visualization tools, data inspectors, and utility panels that work across any database.</p>
<p>The new <code>usePluginModal()</code> hook lets slot components open host-managed modals with custom React content. Useful for OAuth setup flows, configuration wizards, or anything that needs more space than a toolbar button provides.</p>
<p>I&#39;m working on a set of demo plugins that showcase these capabilities in practice. Expect them in the coming days.</p>
<hr>
<h2>PostgreSQL: Range, Multirange, and Enum Types</h2>
<p>This is <a href="https://github.com/dev-void-7">@dev-void-7</a>&#39;s work in PR <a href="https://github.com/TabularisDB/tabularis/pull/111">#111</a> — 20 commits touching the Rust extraction layer, all focused on closing gaps in PostgreSQL type support. The same contributor who rewrote the PostgreSQL driver on top of <code>tokio-postgres</code> in v0.9.12 is back, this time expanding type coverage.</p>
<p><strong>Enum types</strong> are now extracted properly. Previously they were passed through as raw bytes; now they resolve to their string label.</p>
<p><strong>Range types</strong> (<code>int4range</code>, <code>int8range</code>, <code>numrange</code>, <code>tsrange</code>, <code>tstzrange</code>, <code>daterange</code>) are parsed from their binary wire format and displayed in standard PostgreSQL range notation — <code>[1,10)</code>, <code>(2024-01-01,2024-12-31]</code>, and so on. Empty ranges are handled correctly.</p>
<p><strong>Multirange types</strong> (PostgreSQL 14+) follow the same approach. A multirange value is displayed as a set of ranges: <code>{[1,5),[10,20)}</code>.</p>
<p>Along the way, <a href="https://github.com/dev-void-7">@dev-void-7</a> also hardened the existing extraction code. Null handling in composite types was tightened — <code>fill_nulls</code> now only fills remaining fields instead of overwriting the entire row. The <code>split_at_value_len</code> helper returns <code>Option</code> instead of panicking on unexpected input. Array extraction with zero dimensions returns an empty array instead of <code>null</code>. And the entire extract module gained a proper test suite: simple types, enums, arrays, composites, ranges, and multiranges all have dedicated test coverage now.</p>
<p>This is the kind of work that doesn&#39;t produce flashy screenshots but makes the difference between &quot;it sort of works&quot; and &quot;it handles real data.&quot;</p>
<hr>
<h2>Settings: Modular Redesign</h2>
<p>The Settings page was a single 800-line component. It did the job, but adding anything to it meant scrolling through a wall of JSX and hoping you didn&#39;t break something three sections away.</p>
<p>v0.9.13 splits Settings into dedicated tab components:</p>
<ul>
<li><strong>General</strong> — basic application preferences</li>
<li><strong>Appearance</strong> — themes, fonts, zoom, and the new editor preferences (font family, font size, minimap toggle)</li>
<li><strong>Localization</strong> — language selection</li>
<li><strong>AI</strong> — provider configuration and model selection</li>
<li><strong>Plugins</strong> — plugin management (install, enable, configure, remove)</li>
<li><strong>Shortcuts</strong> — keyboard shortcut reference</li>
<li><strong>Logs</strong> — log viewer</li>
<li><strong>Info</strong> — version, links, credits</li>
</ul>
<p>Each tab is its own file under <code>src/components/settings/</code>. The main <code>Settings.tsx</code> went from ~800 lines to ~40. Shared controls (<code>SettingRow</code>, <code>SettingSwitch</code>, <code>SettingSelect</code>, etc.) live in a dedicated <code>SettingControls.tsx</code> module, reused across all tabs.</p>
<p>The new <strong>editor preferences</strong> section in the Appearance tab lets you pick a font family (from a curated list of monospace fonts), adjust the font size, and toggle the minimap — all applied in real time to the SQL editor.</p>
<hr>
<h2>New Driver Capabilities: <code>readonly</code> and <code>manage_tables</code></h2>
<p>Two new capability flags for plugin authors:</p>
<p><strong><code>readonly</code></strong> — when set to <code>true</code>, the driver is treated as read-only. All data modification operations (INSERT, UPDATE, DELETE) are disabled in the UI. The add/delete row buttons, inline cell editing, and context menu edit actions are hidden. Table and column management is also hidden, regardless of the <code>manage_tables</code> flag. This is useful for analytics databases, data lakes, or any source where writes don&#39;t make sense.</p>
<p><strong><code>manage_tables</code></strong> — controls whether the table and column management UI (Create Table, Add/Modify/Drop Column, Drop Table) is shown. Defaults to <code>true</code>. Set it to <code>false</code> for drivers where DDL operations aren&#39;t supported or desirable.</p>
<p>Both flags are declared in the manifest&#39;s <code>capabilities</code> object and evaluated on the frontend to gate UI elements. Existing plugins are unaffected — the defaults preserve current behavior.</p>
<hr>
<h2>Error Modal</h2>
<p>Native browser <code>dialog()</code> calls have been replaced with a proper in-app error modal. When an async operation fails — a query that errors out, a save that can&#39;t complete — the error is now displayed in a themed, keyboard-accessible modal instead of an OS-native popup that ignores your dark theme and can&#39;t be styled.</p>
<p>This affects error handling in both the DataGrid and the SQL Editor.</p>
<hr>
<h2>SQL Editor Fixes</h2>
<p>A few annoyances that accumulated over the last couple of releases:</p>
<ul>
<li><strong>Cursor position preserved</strong> — switching between tabs no longer resets your cursor to the top of the file. The editor remembers where you were.</li>
<li><strong>Autocomplete behavior</strong> — the suggestion popup no longer steals focus from the editor or fires in contexts where it shouldn&#39;t.</li>
<li><strong>Paste per instance</strong> — the paste action is now registered per editor instance, fixing a bug where pasting in one tab could affect another.</li>
<li><strong>Scrollbar styling</strong> — the custom scrollbar theme now applies consistently across all editor instances.</li>
<li><strong>Tab closing</strong> — simplified the tab close logic and removed the auto-create behavior that would spawn a new empty tab when you closed the last one.</li>
</ul>
<hr>
<h2>Smaller Changes</h2>
<ul>
<li><strong><code>tailwind-merge</code> removed</strong> — the dependency was unused and added weight to the bundle.</li>
<li><strong>TypeScript strictness</strong> — explicit type guards and casts replace several <code>as unknown as X</code> patterns across the codebase.</li>
<li><strong>Test coverage</strong> — new tests for <code>SlotAnchor</code>, <code>PluginSlotProvider</code>, <code>SettingControls</code>, <code>pluginModuleLoader</code>, and settings utilities.</li>
</ul>
<hr>
<hr>
<p><em>v0.9.13 is available now. Update via the in-app updater, or download from the <a href="https://github.com/TabularisDB/tabularis/releases/tag/v0.9.13">releases page</a>.</em></p>
]]></content:encoded>
      <enclosure url="https://tabularis.dev/blog/v0913-ui-extensions-postgres-types-settings/opengraph-image.png" type="image/png" />
      <category>release</category>
      <category>plugins</category>
      <category>ui-extensions</category>
      <category>postgres</category>
      <category>settings</category>
      <category>community</category>
    </item>
    <item>
      <title>v0.9.12: Under the Hood</title>
      <link>https://tabularis.dev/blog/v0912-tokio-postgres-minimax-json-editor</link>
      <guid isPermaLink="true">https://tabularis.dev/blog/v0912-tokio-postgres-minimax-json-editor</guid>
      <pubDate>Sun, 29 Mar 2026 11:20:00 GMT</pubDate>
      <description>The PostgreSQL driver switches from sqlx to tokio-postgres, MiniMax joins the AI providers, JSON columns get a proper editor, and error messages stop being cryptic.</description>
      <content:encoded><![CDATA[<h1>v0.9.12: Under the Hood</h1>
<p>Most of this release happened in Rust.</p>
<hr>
<h2>PostgreSQL: from sqlx to tokio-postgres</h2>
<p>This one comes from <a href="https://github.com/dev-void-7">dev-void-7</a> in PR <a href="https://github.com/TabularisDB/tabularis/pull/105">#105</a> — the entire PostgreSQL driver has been rewritten on top of <code>tokio-postgres</code>. This was a long overdue move — <code>sqlx</code> served us well in the early days, but as Tabularis grew to support composite types, arrays, and more exotic column kinds, we kept running into limitations in how <code>sqlx</code> handles type extraction.</p>
<p>The migration touched roughly 1,200 lines across the Rust backend. Connection pooling, query execution, row extraction, dump and export — all rewired. The <code>extract</code> module was split into focused submodules (<code>simple</code>, <code>array</code>, <code>composite</code>, <code>common</code>) to keep things maintainable as more types get added.</p>
<p>From the user&#39;s perspective nothing should look different. Queries run the same, edits work the same, exports produce the same output. If something does behave differently, that&#39;s a bug — please open an issue.</p>
<p>One concrete fix that came out of this: columns reported as <code>text</code> or <code>blob</code> by the driver metadata but actually holding a different type (common with certain PostgreSQL extensions) are now handled through a <code>known_type</code> hint that overrides the wire metadata when we know better.</p>
<hr>
<h2>MiniMax AI Provider</h2>
<p><a href="https://github.com/octo-patch">octo-patch</a> contributed MiniMax as a new first-class AI provider in PR <a href="https://github.com/TabularisDB/tabularis/pull/108">#108</a>. Two models are available: <strong>MiniMax-M2.7</strong> and <strong>MiniMax-M2.7-highspeed</strong>.</p>
<p>It works exactly like the other providers — set your API key in Settings, pick a model, and the AI assistant uses it for SQL generation and query explanation. The backend talks to <code>api.minimax.io/v1</code> using the same OpenAI-compatible format we already use for several other providers, so it slotted in cleanly.</p>
<p>This brings the supported providers to seven: OpenAI, Anthropic, OpenRouter, Ollama, OpenAI-Compatible, and now MiniMax.</p>
<hr>
<h2>JSON Editor in the Sidebar</h2>
<p>Also from <a href="https://github.com/midasism">midasism</a> (PR <a href="https://github.com/TabularisDB/tabularis/pull/107">#107</a>) — editing JSON columns used to mean typing raw JSON into a single-line text input and hoping for the best. Now there&#39;s a proper multi-line editor with:</p>
<ul>
<li><strong>Live validation</strong> — errors show inline as you type</li>
<li><strong>Format button</strong> — pretty-prints your JSON with one click</li>
<li><strong>Proper sync</strong> — the editor state stays in sync with the underlying cell value without fighting React&#39;s effect cycle</li>
</ul>
<p>This applies to JSON and JSONB columns in the sidebar editor. The grid cell itself still shows the compact representation, but as soon as you click into the sidebar you get the full editor.</p>
<hr>
<h2>Better PostgreSQL Error Messages</h2>
<p>PostgreSQL errors can be verbose. The driver returns a one-line summary, then a wall of internal details (error codes, schema names, constraint definitions). Previously we dumped the whole thing into the error panel, which made it hard to spot the actual problem.</p>
<p>Now the error display splits the message: you see the brief summary first, and a <strong>Show details</strong> toggle reveals the rest. Small thing, but it makes a real difference when you&#39;re iterating on a query and getting syntax errors every few seconds.</p>
<hr>
<h2>MySQL JSON Columns</h2>
<p>JSON columns in MySQL were showing as <code>NULL</code> in the data grid. The root cause was a misread of the field type bytes in the MySQL binary protocol — the JSON type flag was being skipped during extraction. Fixed by <a href="https://github.com/midasism">midasism</a> in PR <a href="https://github.com/TabularisDB/tabularis/pull/107">#107</a>.</p>
<hr>
<h2>Smaller Changes</h2>
<ul>
<li><strong>Active database in window title</strong> — the Editor tab and the OS window title now show which database you&#39;re working in. Useful when you have multiple connections open.</li>
<li><strong>Provider icons in Settings</strong> — the AI provider dropdown now shows each provider&#39;s logo, making it easier to scan at a glance.</li>
<li><strong>Global alert modal</strong> — replaced the native browser <code>dialog()</code> calls with a proper in-app alert modal that respects theming and is keyboard-accessible.</li>
<li><strong>React stability fixes</strong> — several components had missing hook dependencies or unstable callback references causing unnecessary re-renders. Cleaned up.</li>
</ul>
<hr>
<hr>
<p><em>v0.9.12 is available now. Update via the in-app updater, or download from the <a href="https://github.com/TabularisDB/tabularis/releases/tag/v0.9.12">releases page</a>.</em></p>
]]></content:encoded>
      <enclosure url="https://tabularis.dev/blog/v0912-tokio-postgres-minimax-json-editor/opengraph-image.png" type="image/png" />
      <category>release</category>
      <category>postgres</category>
      <category>ai</category>
      <category>ux</category>
      <category>mysql</category>
      <category>community</category>
    </item>
    <item>
      <title>v0.9.11: 你好 Tabularis</title>
      <link>https://tabularis.dev/blog/v0911-chinese-postgres-arrays-inline-editing</link>
      <guid isPermaLink="true">https://tabularis.dev/blog/v0911-chinese-postgres-arrays-inline-editing</guid>
      <pubDate>Wed, 25 Mar 2026 07:21:00 GMT</pubDate>
      <description>Chinese (Simplified) joins the supported languages, PostgreSQL arrays finally work properly, query results are editable inline, and exports got more flexible.</description>
      <content:encoded><![CDATA[<h1>v0.9.11: 你好 Tabularis</h1>
<p>Another community-driven language lands in Tabularis.</p>
<hr>
<h2>Chinese (Simplified)</h2>
<p>Back when I built i18n in <a href="https://tabularis.dev/blog/security-and-i18n">v0.6.0</a>, I shipped English and Italian. Spanish came shortly after thanks to <a href="https://github.com/kconfesor">kconfesor</a>&#39;s contribution in PR <a href="https://github.com/TabularisDB/tabularis/pull/30">#30</a>. Now <a href="https://github.com/GTLOLI">GTLOLI</a> has done the same for Chinese (Simplified) — a full translation, 879 keys, merged in PR <a href="https://github.com/TabularisDB/tabularis/pull/103">#103</a>.</p>
<p>So Tabularis now supports <strong>English</strong>, <strong>Italian</strong>, <strong>Spanish</strong>, and <strong>Chinese (Simplified)</strong>. As before, the app picks your system locale automatically, or you can set <code>&quot;zh&quot;</code> in <code>config.json</code>.</p>
<p>Two out of four languages came from contributors. That&#39;s the kind of thing I was hoping for when I made the i18n files plain JSON with no build step. If you want to add your language, the bar is low — just copy one of the existing files and translate.</p>
<hr>
<h2>PostgreSQL Array Types</h2>
<p>If you had <code>integer[]</code> or <code>text[]</code> columns in PostgreSQL, the grid would show raw strings and edits were broken. Fixed now — array columns parse correctly, and writing back works through JSON-to-ARRAY literal conversion. You edit <code>[&quot;a&quot;, &quot;b&quot;, &quot;c&quot;]</code> in the cell, the driver generates <code>ARRAY[&#39;a&#39;,&#39;b&#39;,&#39;c&#39;]</code>.</p>
<p>All Rust-side, no UI changes.</p>
<hr>
<h2>Inline Editing for Query Results</h2>
<p>Running a <code>SELECT</code> in the editor used to give you a read-only grid. Now, if the query hits a single table, you can edit rows inline — same double-click behavior as table browsing.</p>
<p>Joins, aggregations, subqueries stay read-only. There&#39;s no safe way to map an edited cell back to a source row in those cases, so I&#39;m not pretending there is.</p>
<hr>
<h2>JSON Copy and CSV Delimiter</h2>
<ul>
<li>You can copy selected rows as <strong>JSON</strong> now. Set it as your default copy format in Settings if you want.</li>
<li>The <strong>CSV delimiter</strong> is configurable — semicolons, tabs, pipes, whatever you need.</li>
</ul>
<hr>
<h2>What&#39;s Next</h2>
<p>I&#39;ve been on the <a href="https://tabularis.dev/blog/plugin-ui-extensions">UI extensions branch</a> for a few weeks, testing with the JSON Viewer and Google Sheets plugins. The two open items from the <a href="https://tabularis.dev/blog/v0910-bugfixes-ui-extensions-wip">v0.9.10 post</a> — error surfaces and theme tokens — are almost done.</p>
<p>I expect to ship UI extensions next week. After that, plugins won&#39;t be limited to backend drivers anymore — they&#39;ll be able to put components directly in the Tabularis UI.</p>
<hr>
<hr>
<p><em>v0.9.11 is available now. Update via the in-app updater, or download from the <a href="https://github.com/TabularisDB/tabularis/releases/tag/v0.9.11">releases page</a>.</em></p>
]]></content:encoded>
      <enclosure url="https://tabularis.dev/blog/v0911-chinese-postgres-arrays-inline-editing/opengraph-image.png" type="image/png" />
      <category>release</category>
      <category>i18n</category>
      <category>postgres</category>
      <category>ux</category>
      <category>csv</category>
      <category>community</category>
    </item>
    <item>
      <title>v0.9.10, UI Extensions in Progress, and Two Real Plugins</title>
      <link>https://tabularis.dev/blog/v0910-bugfixes-ui-extensions-wip</link>
      <guid isPermaLink="true">https://tabularis.dev/blog/v0910-bugfixes-ui-extensions-wip</guid>
      <pubDate>Wed, 18 Mar 2026 13:00:00 GMT</pubDate>
      <description>v0.9.10 lands a handful of fixes, including multi-database window title and per-database record operations. Meanwhile, the UI extensions branch is being tested with two real plugins: a JSON Viewer and the Google Sheets driver.</description>
      <content:encoded><![CDATA[<h1>v0.9.10, UI Extensions in Progress, and Two Real Plugins</h1>
<p><strong>v0.9.10</strong> is a maintenance release — no headline features, just a set of fixes that were blocking clean usage after the multi-database work shipped in v0.9.8. Behind the scenes, <code>feat/plugin-system-ui</code> is heating up: the slot-based UI extension system is now far enough along to test with real plugins, and two of them are already running.</p>
<hr>
<h2>What Changed in v0.9.10</h2>
<h3>Multi-database window title</h3>
<p>When you open a connection with multiple databases selected, the window title now reflects all of them. Previously it would show only the first database in the selection, which made it confusing to tell windows apart when you had two multi-database sessions open side by side.</p>
<h3>Database selection in record operations</h3>
<p>INSERT, UPDATE, and DELETE operations in the Editor now correctly target the selected database in a multi-database session. Before this fix, record mutations would fall back to the first database in the list regardless of which one was active in the UI, which was both surprising and dangerous.</p>
<h3>Editor toolbar padding</h3>
<p>The editor toolbar was using inconsistent padding that caused the toolbar items to feel cramped and misaligned with the surrounding UI. The spacing has been adjusted to match the rest of the interface.</p>
<h3>Modal name input focus and placeholder</h3>
<p>When a validation error occurs in a modal that has a name field, the input now receives focus automatically so the user can correct it without an extra click. The placeholder text was also updated to be more descriptive.</p>
<hr>
<h2>What Is in Progress: Plugin UI Extensions</h2>
<p><a href="https://tabularis.dev/blog/plugin-ui-extensions">Phase 2 of the plugin system</a> was sketched out in a post from a few days ago. The design is slots: named insertion points in the host UI where a plugin can render a React component. Ten slots are currently defined:</p>
<table>
<thead>
<tr>
<th>Slot</th>
<th>Where it appears</th>
</tr>
</thead>
<tbody><tr>
<td><code>row-edit-modal.field.after</code></td>
<td>Below each field in the row edit modal</td>
</tr>
<tr>
<td><code>row-edit-modal.footer.before</code></td>
<td>Before the action buttons in the modal footer</td>
</tr>
<tr>
<td><code>row-editor-sidebar.field.after</code></td>
<td>Below each field in the row editor sidebar</td>
</tr>
<tr>
<td><code>row-editor-sidebar.header.actions</code></td>
<td>Extra action buttons in the sidebar header</td>
</tr>
<tr>
<td><code>data-grid.toolbar.actions</code></td>
<td>Alongside filter/sort/limit in the data grid toolbar</td>
</tr>
<tr>
<td><code>data-grid.context-menu.items</code></td>
<td>Extra items in the row right-click menu</td>
</tr>
<tr>
<td><code>sidebar.footer.actions</code></td>
<td>Persistent buttons in the main sidebar footer</td>
</tr>
<tr>
<td><code>settings.plugin.actions</code></td>
<td>Action area in the plugin&#39;s settings panel</td>
</tr>
<tr>
<td><code>settings.plugin.before_settings</code></td>
<td>Above the settings form for a plugin</td>
</tr>
<tr>
<td><code>connection-modal.connection_content</code></td>
<td>Custom content inside the new connection modal</td>
</tr>
</tbody></table>
<p>Each slot receives a typed <code>SlotContext</code> — connection ID, driver name, table name, current row data, column name, and more depending on the slot. Plugin components are standard React components; they receive <code>context</code> and <code>pluginId</code> as props and return JSX. The plugin API (<code>@tabularis/plugin-api</code>) exposes utilities for read-only queries, toasts, theme detection, and reading plugin settings. That is the entire approved surface — direct Tauri access and DOM mutations outside the plugin subtree are blocked.</p>
<p>Manifests declare UI extensions alongside the existing driver configuration:</p>
<pre><code class="language-json">{
  &quot;ui_extensions&quot;: [
    {
      &quot;slot&quot;: &quot;row-editor-sidebar.field.after&quot;,
      &quot;module&quot;: &quot;dist/index.js&quot;,
      &quot;order&quot;: 50
    }
  ]
}
</code></pre>
<p>Modules are lazy-loaded when the target slot first mounts. Plugins that declare no <code>ui_extensions</code> are unaffected — everything is additive.</p>
<hr>
<h2>Testing with Two Real Plugins</h2>
<p>The best way to find out what the slot API is missing is to write plugins against it. I am currently running two:</p>
<h3>JSON Viewer</h3>
<p>The <strong>JSON Viewer</strong> plugin (<code>tabularis-json-viewer</code>) targets two slots: <code>row-editor-sidebar.field.after</code> and <code>row-edit-modal.field.after</code>. When the active column holds a <code>JSON</code> or <code>JSONB</code> value, it renders a formatted, syntax-highlighted preview directly below the text input. No extra buttons, no modal — the preview appears inline, in both the sidebar and the modal editor.</p>
<pre><code class="language-json">{
  &quot;id&quot;: &quot;json-viewer&quot;,
  &quot;ui_extensions&quot;: [
    { &quot;slot&quot;: &quot;row-editor-sidebar.field.after&quot;, &quot;module&quot;: &quot;dist/index.js&quot;, &quot;order&quot;: 50 },
    { &quot;slot&quot;: &quot;row-edit-modal.field.after&quot;,     &quot;module&quot;: &quot;dist/index.js&quot;, &quot;order&quot;: 50 }
  ]
}
</code></pre>
<p>This is a UI-only plugin — no driver, no backend process. It demonstrates that Phase 2 plugins do not need to speak JSON-RPC at all: if the only goal is to enhance the interface, the manifest just declares <code>ui_extensions</code> and ships a compiled JS bundle.</p>
<h3>Google Sheets Plugin</h3>
<p>The <strong>Google Sheets plugin</strong> (<code>tabularis-google-sheets-plugin</code>) already existed as a Phase 1 driver. With Phase 2, it picks up two UI slots:</p>
<ul>
<li><code>settings.plugin.before_settings</code> — renders a &quot;Connect with Google&quot; OAuth button above the plugin&#39;s settings form. The button opens a browser, completes the OAuth flow, and writes the tokens back to the plugin settings automatically. Previously the user had to paste tokens by hand.</li>
</ul>
<img src="https://tabularis.dev/img/tabularis-google-sheets-oauth.png" alt="Plugin Settings for google-sheets showing Google Account Connected status with Re-authorize and Disconnect buttons" style="width:100%;border-radius:8px;margin:1rem 0" />

<ul>
<li><code>connection-modal.connection_content</code> — replaces the generic database field in the new connection modal with a Google Sheets spreadsheet selector. When the driver is <code>google-sheets</code>, the modal shows a search box and a list of the user&#39;s spreadsheets instead of a raw text input.</li>
</ul>
<pre><code class="language-json">{
  &quot;ui_extensions&quot;: [
    {
      &quot;slot&quot;: &quot;settings.plugin.before_settings&quot;,
      &quot;module&quot;: &quot;ui/google-auth.js&quot;,
      &quot;order&quot;: 10
    },
    {
      &quot;slot&quot;: &quot;connection-modal.connection_content&quot;,
      &quot;module&quot;: &quot;ui/google-sheets-db-field.js&quot;,
      &quot;order&quot;: 10,
      &quot;driver&quot;: &quot;google-sheets&quot;
    }
  ]
}
</code></pre>
<p>The <code>driver</code> filter on the second slot is worth noting: <code>connection-modal.connection_content</code> contributions are only active when the selected driver matches. Other drivers do not see this component.</p>
<hr>
<h2>What Is Still Missing</h2>
<p>Running two real plugins surfaced a few gaps that will need to land before the branch merges:</p>
<ul>
<li><strong>Error surfaces</strong> — the error boundary catches crashes correctly, but the warning UI is currently just a text node. It needs a proper component with a &quot;reload plugin&quot; affordance.</li>
<li><strong>Stable theme tokens</strong> — the JSON Viewer needs to match the host&#39;s syntax-highlighting palette across light and dark mode. Right now it hardcodes colors. A public theme contract is the prerequisite.</li>
</ul>
<p>None of these block testing, but all of them need to be resolved before the feature is ready for general plugin authors.</p>
<p>Testing with real plugins is also surfacing cases where the current ten slots are not enough. The connection modal and the settings panel are well covered, but there are interaction patterns — particularly around the data grid and the sidebar — where a plugin naturally wants to inject UI in a place that has no slot yet. I am keeping a list and will likely add a few more anchors before the branch merges.</p>
<hr>
<hr>
<p><em>v0.9.10 is available now. Update via the in-app updater, or download from the <a href="https://github.com/TabularisDB/tabularis/releases/tag/v0.9.10">releases page</a>.</em></p>
]]></content:encoded>
      <enclosure url="https://tabularis.dev/blog/v0910-bugfixes-ui-extensions-wip/opengraph-image.png" type="image/png" />
      <category>release</category>
      <category>bugfix</category>
      <category>plugins</category>
      <category>ui</category>
      <category>extensibility</category>
    </item>
    <item>
      <title>Phase 2 of the Plugin System: Plugins That Touch the UI</title>
      <link>https://tabularis.dev/blog/plugin-ui-extensions</link>
      <guid isPermaLink="true">https://tabularis.dev/blog/plugin-ui-extensions</guid>
      <pubDate>Sun, 15 Mar 2026 12:00:00 GMT</pubDate>
      <description>Phase 1 let anyone build a database driver. Phase 2 lets plugins put buttons, fields, and menu items directly into the Tabularis interface. Here is the design and why it works the way it does.</description>
      <content:encoded><![CDATA[<h1>Phase 2 of the Plugin System: Plugins That Touch the UI</h1>
<p>The plugin system shipped in v0.9.0. Since then, the community has built Redis drivers, a Python CSV driver, DuckDB support. All of them work the same way: a process speaks JSON-RPC over stdin/stdout, and Tabularis handles the UI. The plugin never sees the interface. It sends data, the host renders it.</p>
<p>That was always half the story. A PostGIS driver can return geometry columns, but the data grid shows <code>POINT(45.07 7.68)</code> as plain text. A ClickHouse driver knows about materialized views, but there is no place to surface that in the sidebar. The plugin does the hard work and then hands it off to a UI that knows nothing about it.</p>
<p>I have been thinking about this since before v0.9.0 shipped. Today I am publishing the spec for Phase 2: <strong>UI extensions for plugins</strong>.</p>
<h2>Stealing from WordPress (the good parts)</h2>
<p>If you have built a WordPress plugin, you know the pattern. WordPress defines &quot;hooks&quot; — named points in the page where plugins can inject content. You register a function, WordPress calls it at the right place. The host controls the layout. The plugin controls the content.</p>
<p>I wanted the same idea for Tabularis, but adapted for React. Instead of PHP hooks in a template, we define <strong>slots</strong> — named insertion points inside existing components. A plugin registers a React component against a slot name, and the host renders it with the right props at the right location.</p>
<p>The difference from WordPress: everything is typed. The plugin declares its slots in the manifest. The host validates them. Each component receives a typed context object with exactly the data it needs. No string parsing, no global mutations, no <code>innerHTML</code>.</p>
<h2>Concrete example: PostGIS</h2>
<p>Today, if you open a row with a geometry column in the Row Editor sidebar, you see a text field with <code>SRID=4326;POINT(45.07 7.68)</code>. Accurate, not useful.</p>
<p>With UI extensions, the PostGIS plugin could register a small map component in the <code>row-editor-sidebar.field.after</code> slot. The host renders the text field as before, and immediately below it, the plugin renders a Leaflet map with the point plotted. The plugin receives the column metadata and current value as props — it does not need to fetch anything.</p>
<p>Or think about export. Right now, exporting data to Parquet means copying queries to another tool. With a slot in <code>data-grid.toolbar.actions</code>, a plugin can drop an &quot;Export&quot; button right next to the filter controls. It gets the table name, current filters, column list, and connection ID as context. One click, done.</p>
<h2>The eight slots</h2>
<p>I went through the UI and picked eight places where plugin content makes sense without cluttering the interface:</p>
<p><strong>In the row editor (modal and sidebar):</strong> After each field input. This is where validation hints, computed previews, and lookup widgets belong. There is also a slot before the modal footer buttons — good for batch actions like &quot;fill from template.&quot;</p>
<p><strong>Sidebar header:</strong> Extra buttons next to the row title in the Row Editor sidebar. &quot;Copy as JSON&quot;, &quot;View audit log&quot;, that kind of thing.</p>
<p><strong>Data grid toolbar:</strong> Buttons alongside filter, sort, and limit. Export, analysis, visualization.</p>
<p><strong>Context menu:</strong> Extra items in the right-click menu on grid rows. &quot;Copy as INSERT&quot;, &quot;Bookmark row&quot;, &quot;Look up in external API.&quot;</p>
<p><strong>Main sidebar footer:</strong> Global actions or status badges that persist across views.</p>
<p><strong>Plugin settings:</strong> Per-plugin action buttons in the Settings page, next to the enable/disable toggle and settings gear. Diagnostics, custom configuration shortcuts, plugin-specific status indicators.</p>
<p>Eight slots. Not thirty. I would rather ship a small set that works well than scatter extension points everywhere and regret the maintenance cost.</p>
<h2>How it works for plugin authors</h2>
<p>You add a <code>ui_extensions</code> section to your manifest — the same JSON file that already declares your driver name, version, and capabilities. Each entry says which slot to target and which file contains the component:</p>
<pre><code class="language-json">{
  &quot;ui_extensions&quot;: [
    {
      &quot;slot&quot;: &quot;row-editor-sidebar.field.after&quot;,
      &quot;module&quot;: &quot;./ui/GeometryPreview.tsx&quot;,
      &quot;conditions&quot;: { &quot;drivers&quot;: [&quot;postgres&quot;] }
    }
  ]
}
</code></pre>
<p>That is it. The host reads the manifest, lazy-imports the module when the slot first appears on screen, and renders the component with the right context props.</p>
<p>The component itself is a normal React component. It receives <code>slotContext</code> (column info, row data, connection details, driver capabilities — varies by slot) and <code>pluginId</code>. It returns JSX. There is no special framework to learn, no lifecycle to manage beyond standard React.</p>
<p>Plugins also get a small utility library (<code>@tabularis/plugin-api</code>) for common tasks: running a read-only query on the active connection, showing a toast, reading their own settings, checking whether the theme is dark. That library is the entire approved API surface. Direct Tauri calls and DOM manipulation outside the plugin&#39;s subtree are off-limits.</p>
<h2>Nothing breaks</h2>
<p>This was a hard requirement from the start. The <code>ui_extensions</code> field is optional. Every existing plugin — every manifest, every driver, every configuration — works exactly as before. The types do not change. The Rust backend does not change. The settings structure does not change.</p>
<p>On the frontend, the new slot anchors render nothing when the registry is empty. If you never install a plugin with UI extensions, the app behaves identically. Zero overhead.</p>
<p>The community has active plugin authors. I am not going to ask them to rewrite their manifests. Phase 2 is additive.</p>
<h2>The hard problems</h2>
<p>I do not want to pretend this is simple. There are real challenges.</p>
<p><strong>Security.</strong> Plugin components run in the same React tree as the host. I chose this over iframe isolation because iframes make shared styling and context passing painful — and the whole point is seamless integration. The tradeoff is that a malicious plugin could theoretically poke at host state. The mitigation: plugins can only import from <code>@tabularis/plugin-api</code>. Direct Tauri access is blocked. Queries are read-only by default — write access requires an explicit permission flag in the manifest, and the user sees a prompt before granting it. This is a trust-but-verify model. It will need hardening over time.</p>
<p><strong>Crashes.</strong> A broken plugin component cannot take down the host. Every slot contribution gets its own React error boundary. If a plugin throws, you see a small warning icon — not a white screen. If it throws three times in a minute, it is disabled for the session. This is defensive, but necessary. Third-party code will crash. The question is how gracefully.</p>
<p><strong>Performance.</strong> Plugin modules are lazy-loaded. A plugin targeting the row editor sidebar adds zero bytes to the initial bundle if you never open the sidebar. The slot registry is empty at startup and only fills as plugins are activated and their target slots mount. The plugin ecosystem should grow without making the app heavier.</p>
<p><strong>Ordering.</strong> Multiple plugins can target the same slot. Each contribution has an ordering weight, and the host sorts by it. If two plugins pick the same weight, the order is stable but arbitrary. For seven slots, this is fine. If the ecosystem grows to a point where ten plugins compete for toolbar space, I will revisit.</p>
<h2>What is not in Phase 2</h2>
<p>The spec covers the slot architecture, manifest format, context API, and security model. It deliberately does not cover:</p>
<ul>
<li><strong>Full-page views</strong> — A plugin that wants to render an entire dashboard or monitoring panel needs a routing extension. That is a different problem.</li>
<li><strong>A stable theme API</strong> — Plugins can detect dark/light mode, but accessing the full design token set requires defining a public theme contract. Not ready yet.</li>
<li><strong>Scoped storage</strong> — Plugins have no way to persist state across sessions. A per-plugin key-value store is planned but not designed.</li>
<li><strong>Inter-plugin communication</strong> — A PostGIS preview in the sidebar might want to talk to a map view in the toolbar. There is no message bus for that. Will come if the use cases justify it.</li>
</ul>
<p>These are future extensions, not prerequisites. The Phase 2 spec is self-contained — you can build useful plugins against it without any of them.</p>
<h2>The point</h2>
<p>Phase 1 solved &quot;which databases can I connect to.&quot; Phase 2 starts solving &quot;what can I do once I am connected.&quot;</p>
<p>A team using PostGIS gets geometry previews without me knowing anything about PostGIS. A company with a custom audit system can show compliance info next to each row. Someone who exports data to Parquet daily can do it from the toolbar.</p>
<p>The host provides the stage. The plugin does the work.</p>
<p>The <a href="https://tabularis.dev/docs/plugin-ui-extensions-spec.md">full specification</a> is published alongside this post. If you build plugins — or want to start — read it, file issues, tell me what is missing. The design is not final. The direction is.</p>
]]></content:encoded>
      <enclosure url="https://tabularis.dev/blog/plugin-ui-extensions/opengraph-image.png" type="image/png" />
      <category>plugins</category>
      <category>extensibility</category>
      <category>architecture</category>
      <category>ui</category>
    </item>
  </channel>
</rss>
