When Your Database Client Lies to You: Patching Workbench 26 for MariaDB

· 11 min read · devops mysqlmariadbworkbenchdebuggingelectronmysql-shcyberpanelself-hosting

I run four database servers from one Windows machine: two MySQL 8.4 instances, one local install, and a MariaDB 10.11 box behind CyberPanel on my LAN. Three of them opened fine in the shiny new MySQL Workbench 26.7.0. One didn’t.

The error it gave me was this:

TypeError: DbModuleSession.on_session_message() missing 1 required positional argument: 'result'

That message contains no information about databases, networking, credentials, or MariaDB. It’s a Python argument-count error inside the client. And it was the single most misleading error message I’ve chased this year — because it wasn’t the bug. It was the error reporter crashing while trying to report the bug.

This post is the trail from that message to three patches in Oracle’s own shipping code, and the underlying design assumption that causes all three.

Why this matters

If you keep more than one database engine around — MySQL here, MariaDB there — you’ll hit this class of failure eventually, and the error text will actively mislead you. Three things in this post are worth knowing even if you never open Workbench:

  1. A garbage error message is itself a finding. When a tool reports something structurally impossible, suspect its error path before its logic.
  2. major >= 8 is not a reliable “is this MySQL?” test. MariaDB reports version 10.x and 11.x. A lot of software gets this wrong.
  3. The version-number collision is systemic, not a one-off bug in one app — it’s why the same fix had to be applied three times in three files.

The setup

The four connections, and which one broke:

ServerEngineVersionWorkbench 26.7.0
DSM NASMySQL8.4works
unRaidMySQL8.4works
localhost (since removed)MySQL8.4.6works
CyberPanel VMMariaDB10.11.19fails

The pattern is obvious in hindsight: everything MySQL 8.4 works, the one MariaDB doesn’t. But “it’s the odd one out” is not a diagnosis, and I didn’t trust it yet.

What I tried first, and why it was wrong

Theory 1: TLS. The connection profile had useSSL = 1 with empty sslCA, sslCert, sslKey. For MySQL 8.x that’s fine — it auto-generates certificates. For MariaDB, maybe not.

This one turned out to be half right, and I’ll come back to it, because it’s a genuine second fault that produces a different error.

Theory 2: credentials or grants. Also wrong, and I could disprove it cheaply. Workbench 26 ships its own MySQL Shell binary. Running that binary by hand against the same host is a direct test of everything except Workbench’s own code:

MSH="/c/Program Files/MySQL/MySQL Workbench/resources/app/shell/bin/mysqlsh.exe"
"$MSH" --sql --uri "admin:[email protected]:3306" \
  -e "select current_user(), version();"
current_user()   version()
admin@%          10.11.19-MariaDB-ubu2404

It connects. Same binary, same host, same credentials — works from the command line, fails from the GUI. That narrows it decisively: the network, server, grants, and password are all fine. Whatever is broken lives in Workbench’s own session-handling code.

If you take one technique from this post, take that one. Before debugging a client, drive the client’s own underlying tool directly to split “the thing is broken” from “this UI is broken.”

Getting the real error out of it

Workbench 26 stores its logs somewhere new. The useful ones:

%APPDATA%\MySQL Workbench\MySQL Workbench-electron.log   # app-level, session stderr
%APPDATA%\MySQL\mysqlsh\mysqlsh.log                     # per-connect log

The connect log showed something odd. The session connected, then stopped:

Info: Connecting to MySQL at: [email protected]:3306
Info: main: tid=0: CONNECTED: 192.168.1.124:3306
Warning: Could not enable session tracking for sql_mode: Session tracker not supported by server

CONNECTED, and then nothing. No error, no disconnect. The connection succeeded and the session died silently afterwards.

Meanwhile the Electron log had the TypeError from the top of this post, repeated fifty-one times. That repetition is the clue: an error handler being called once per failure, crashing every time.

The bug was in the error handler

From gui_plugin\core\dbms\DbSession.py:

def terminate_thread(self):
    self._close_database(True)
    if self.thread_error is not None:
        logger.error(f"Thread {self._id} exiting with code {self.thread_error}")
        self._message_callback("ERROR", self.thread_error)      # two arguments
    self._term_complete.set()

And the callback it’s calling, in DbModuleSession.py:

def on_session_message(self, type, message, result, request_id=None):

Three required parameters. The call passes two. So whenever a session thread dies, the code that reports it dies too — and the TypeError replaces the original error on its way to the user.

That’s the whole reason this bug was hard to see. Every real error in that session was being swallowed by a crash in the reporting path. Fixing the handler doesn’t fix the underlying problem; it makes the problem visible. Which is what you need.

The correct call shape is right there in a sibling file, DbMySQLSession.py:

self._message_callback(
    "PENDING",
    "Connection lost, reconnecting session...",
    None,
    self._current_task_id,
)

So the fix is to match it:

self._message_callback(
    "ERROR", str(self.thread_error), None, self._current_task_id
)

Restart, retry, and the error message finally tells the truth:

MySQL Error 1193 (HY000): Unknown system variable 'gtid_mode'

Now we’re somewhere. That’s a real error, and it names a real cause.

The actual root cause

gtid_mode is a MySQL-only system variable. MariaDB doesn’t have it — it has gtid_strict_mode, gtid_binlog_pos, gtid_current_pos, and others, but nothing named gtid_mode.

You can confirm both halves in one minute:

# On MariaDB — no gtid_mode anywhere in the list
"$MSH" --sql --uri "..." -e "show variables like 'gtid%';"

# On MySQL 8.4 — there it is
"$MSH" --sql --uri "..." -e "select @@gtid_mode;"

The offending code, in gui_plugin\migration\lib\backend\replication.py:

if log_bin:
    if session.nversion >= 56000:
        gtid_mode = session.run_sql("select @@gtid_mode").fetch_one()[0]

session.nversion for MariaDB 10.11.19 is 101119. That’s greater than 56000, so the guard passes, so the query runs, so MariaDB rejects it and the session dies.

And here is the design flaw underneath it. The guard assumes a version number can tell you which engine you’re talking to. It can’t. MariaDB and MySQL share a single version register:

EngineReports as
MySQL 5.75.7.x
MySQL 8.0 / 8.48.0.x / 8.4.x
MariaDB 10.610.6.x
MariaDB 10.1110.11.x
MariaDB 11.x11.x

Every MariaDB version is numerically above every MySQL version in production use. So nversion >= 56000 is true for MariaDB. So is >= 80000. Any feature-gate written as a MySQL version comparison will pass against MariaDB and then send it syntax it doesn’t understand.

The reliable discriminator is @@version_comment, which returns mariadb.org binary distribution on MariaDB and something else on MySQL. Workbench even fetches that value — it stores it as VERSION_COMMENT right next to VERSION_INFO — and then never uses it to make this decision.

This is not sloppiness in one place. It’s a wrong assumption applied consistently, which is why the same bug appeared three separate times.

Three patches, same mistake

Once I knew what to look for, the rest were mechanical. All three are the same fix: make the MySQL-only probe tolerant.

Patch 1 — replication.py

if session.nversion >= 56000:
    try:
        gtid_mode = session.run_sql("select @@gtid_mode").fetch_one()[0]
    except Exception:
        # MariaDB has no gtid_mode system variable; ignore and continue.
        gtid_mode = None

Worth noting: the same query is called elsewhere in the codebase, in checks.py, and there it’s already wrapped in try/except. Oracle knew this call could fail on some servers. They just didn’t apply that knowledge here. Only triggers when binlog is enabled, which it is on this box.

Patch 2 — DbSession.py

The error handler from earlier. Strictly this fixes reporting, not MariaDB compatibility — but it’s the patch that made patches 1 and 3 findable. Do it early.

Patch 3 — DbMySQLSessionSetupTasks.py

Next failure, surfaced by patch 2:

MySQL Error 1193: Unknown system variable 'explain_json_format_version'

Which lives here, in a session-setup task that runs on every connect:

version = self.get_data(common.MySQLData.VERSION_INFO)
major, minor, _ = version.split(".", 2)
if int(major) < 8 or int(major) == 8 and int(minor) < 3:
    return
self.execute("SET SESSION explain_json_format_version=2")

@@version is 10.11.19-MariaDB-ubu2404. Split it: major = 10, minor = 11. Is int(major) < 8? No. Guard passes. SET SESSION explain_json_format_version=2 is MySQL-only. Error 1193 again.

Same fix:

try:
    self.execute("SET SESSION explain_json_format_version=2")
except Exception:
    pass

The separate TLS fault

Remember Theory 1? It was a real problem, just a different one — and it fails before authentication, which makes it look like a credential issue.

Your connection profile can hold useSSL = 1 with no certificates. Against MySQL 8.x that’s harmless. Against MariaDB with TLS switched off, the handshake is refused outright:

"$MSH" --sql --uri "..." -e "select @@have_ssl;"
# have_ssl: DISABLED

"$MSH" --sql --uri "...?ssl-mode=REQUIRED" -e "select 1;"
# ERROR 2026 (HY000): SSL connection error:
#   SSL is required but the server doesn't support it

The fix is one value in %APPDATA%\MySQL\Workbench\connections.xml — set useSSL to 0 for that connection only. On a trusted LAN, a plain connection is fine. Back the file up first.

Two independent faults, two different error signatures. Worth separating in your head: TLS failures happen before login; the 1193 failures happen after. If the server rejects your handshake, credentials never get evaluated.

How to patch files under Program Files

The Workbench files are shipped read-only, and this bit me twice in a way worth documenting: writes fail silently. PowerShell’s WriteAllText reported success while the file on disk was unchanged — same size, same timestamp. A .Replace() on the content reported success and did nothing, because of line-ending differences.

What actually works:

  1. Back up in placeCopy-Item $f "$f.bak", so the pristine original sits next to your version.
  2. Clear the read-only flag and confirm it: attrib -R, then check (Get-Item $f).Attributes no longer says ReadOnly.
  3. Patch by line index, not string replacement — read all lines, splice, write all lines. Immune to CRLF and quoting differences.
  4. Restore read-only afterwards with attrib +R.
  5. Delete the stale __pycache__ .pyc — otherwise Python keeps running the old compiled module and you’ll conclude your patch didn’t work. This one is easy to forget and produces a genuinely confusing result.
  6. Verify syntax before relaunching: ast.parse() on the file, or node --check for JS.

Then restart the app so the patched source is loaded.

Test the patch before you hand it back

A patched build that hasn’t been exercised is a guess. Since the failure depends on server behaviour, simulate the server rather than clicking hopefully at the GUI:

class FakeSession:
    nversion = 101119            # MariaDB 10.11.19
    def run_sql(self, q):
        if "gtid_mode" in q:
            raise Exception("1193 (HY000): Unknown system variable 'gtid_mode'")
        if "@@log_bin, @@binlog_format" in q:
            return FakeResult((1, "ROW"))     # binlog ON triggers the path

Load the patched function with exec(), call it with that fake, and assert it returns instead of raising. It’s a five-minute test that turns “I think this works” into “this path is covered.” It also confirmed the patch was genuinely being loaded, before I blamed the app.

The result

All four connections work from Workbench 26.7.0 now, including the MariaDB box. Three patched files in Oracle’s shipping code, each with a .bak beside it:

FileChange
replication.pyguard select @@gtid_mode
DbSession.pyfix error-handler argument count
DbMySQLSessionSetupTasks.pyguard SET SESSION explain_json_format_version

One thing I’d flag rather than dress up: these are patches to a closed application’s bundled code, not a contribution upstream. They’ll be overwritten by the next Workbench update, and the fault list may be longer than three — I stopped when the connections worked. If it breaks again after upgrading, expect to walk the same trail and possibly find a fourth instance of the same mistake.

What I’d do differently

Version numbers are the easiest thing to compare and the easiest thing to compare wrongly. Two projects sharing a numbering scheme is not the same as two projects sharing a feature set.

Lee Teong Hoe

Full-stack developer & DevOps engineer. I build web apps, self-host infrastructure, and automate things — this blog is my living portfolio.