Driver id mysql, crate crates/drivers/mysql. One driver serves both servers through sqlx 0.9 (mysql, mysql-rsa, tls-rustls-aws-lc-rs) with a deadpool 0.13 pool. MySqlFlavour tells them apart from VERSION() (MariaDB says so in it), because they name their timeouts, error numbers and catalogue columns differently. The integration suite runs against MySQL 8.4 and MariaDB 11.4, and CI runs it again on MySQL 8.0 with MariaDB 10.11 and on MySQL 9.7 with MariaDB 12.3 (supported servers). The contract it implements is in adding-drivers.md.
A connection needs a database name (database_required).
| Connection | Used for | Session |
|---|---|---|
Workspace pool, 4 connections (WORKSPACE_POOL_MAX_SIZE) |
The sidebar, the grid, counts, saves | Workspace |
| One login per call | stream_read (exports), and probe when every pool slot is busy |
Workspace without a statement timeout, or Control |
| One connection per editor tab | EditorSession |
Editor |
| One control connection per workspace and per editor session | KILL QUERY |
Control: nothing changed |
A pool member is handed out again only after it answers a ping within the probe timeout.
sqlx dials its own sockets and takes no stream, so the driver passes it the transport's route:
- A direct route is the host and port, dialled by sqlx.
- An SSH forward is the Unix socket OpenSSH listens on. sqlx connects to the socket, and its
hostcarries the name TLS checks: the service's own, or the override.
Nothing relays bytes in between. The whole login, dial included, is bounded by the connect and login timeouts together (25 s). sqlx 0.9 sets no TCP_NODELAY and no keepalive on its own dial, so on a direct route a dead peer is found by the probe and by each call's deadline, not by the kernel.
Every workspace and editor session starts the same way, so a statement means the same thing on each:
SET NAMES utf8mb4
SET SESSION TRANSACTION READ ONLY -- read-only connections only
SET SESSION max_execution_time = 60000 -- MySQL, milliseconds
SET SESSION max_statement_time = 60 -- MariaDB, seconds
SET SESSION lock_wait_timeout = 10
SET SESSION innodb_lock_wait_timeout = 10SET NAMES utf8mb4names no collation, so the server picks its default for the charset: MySQL'sdefault_collation_for_utf8mb4, MariaDB'scharacter_set_collations. That is what the mysql client gets too.- The statement timeout is the workspace's 60 s. MySQL's
max_execution_timebounds only read-onlySELECTs; MariaDB'smax_statement_timebounds every statement, which is why a schema change lifts it (below). An editor session sets it only when the session has a statement timeout of its own. - Lock waits get the workspace's 10 s, in whole seconds rounded up.
- Numbers are sent as integers, never floats a server could read in another locale.
A workspace session also pins what the app's generated SQL depends on:
time_zone = '+00:00'for reads, so aTIMESTAMPreads as the instant it names. UTC repeats no hour, so the read is exact.sql_modewithoutNO_BACKSLASH_ESCAPES, because generated DDL writes literals such asCOMMENT '...'that have no hex form.sql_modewithSTRICT_ALL_TABLES. Without strict mode the server stores what it can and only warns: a too-long string is cut, an unknownENUMlabel becomes'', a number out of range is clamped, and a column madeNOT NULLturns its NULLs into0or''. Strict mode makes each of these an error, so a save, an import or a schema change stores what it was given or fails.
An editor session keeps the server's own zone and sql_mode, as the mysql client would.
sqlx's own session statements (PIPES_AS_CONCAT, NO_ENGINE_SUBSTITUTION, its SET NAMES) are turned off. Its time zone is off too, except on a workspace session, where sqlx's time_zone option sends the +00:00 above. Its statement cache is 0 so no prepared statement outlives its call, and its statement logging is off.
A save or a schema change runs in the server's own zone, @@GLOBAL.time_zone, and the batch puts +00:00 back when it ends; a connection where that fails is closed. In UTC, DATETIME DEFAULT CURRENT_TIMESTAMP would store UTC wall time, 7 hours off on a +07:00 server, and an ALTER from DATETIME to TIMESTAMP would shift every value.
- A
TIMESTAMPvalue binds as its UTC wall time insideCONVERT_TZ(?, '+00:00', @@session.time_zone), which does nothing on a read and gives the server's wall time on a write. - In the hour a daylight-saving zone repeats, one wall time names two instants and the server picks one, so a key compared there can match the other row. Before a write sends anything, each instant it binds goes to the server's zone and back; one that changes refuses the batch. A zone written as an offset (
+07:00) skips the check. - A
TIMESTAMPcolumn's constant default is read fromSHOW CREATE TABLEand converted to the server's zone, so aMODIFYrestates the same instant. MySQL'sCOLUMN_DEFAULTkeeps the text in the zone of whoever last altered the table, so it is not used for this.
crates/drivers/mysql/src/server_zone.rs has the reasoning.
Disableconnects without TLS. A server withrequire_secure_transportanswers with error 3159, reported asServerRequiresTls. MariaDB before 11.2 answers 1045, Access denied, as it does for a wrong password (MDEV-30178), so on 10.11 the refusal reads as an authentication failure.Requireencrypts without checking the certificate. A server that does not offer TLS is reported asServerRefusedTls.VerifyFullis sqlx'sVerifyIdentity. It trusts the system's CA store and the connection's CA file, which sqlx takes only as PEM, so the driver hands it both as one bundle; sqlx adds the Mozilla roots compiled into it (Trust).Requirechecks no certificate, so it loads neither.- The name checked is the host sqlx dials, so a server name override works only through an SSH forward, where the dial goes to a socket path (
ServerNameOverride::SshForwardOnly). - A client certificate and key are sent when set, for
REQUIRE X509logins. - With TLS off, the password is safe only from a passive observer.
caching_sha2_passwordencrypts it with an RSA key the server sends in band, which nothing authenticates. sqlx 0.9 also takes the login plugin the first handshake names without checking it againstenable_cleartext_plugin, which it checks only on an auth switch, so a man in the middle can ask formysql_clear_passwordand receive the password in clear. The mysql client refuses both by default (--enable-cleartext-plugin,--get-server-public-key). sqlx has no option to refuse either, so the driver cannot: on a network you do not trust, use TLS.
A stop is KILL QUERY <connection id> on the control connection, which reaches the server over the same route as the connection it stops. The id comes from CONNECTION_ID() at login. It names the connection, not the statement: StopDelivery::OutOfBand(StopScope::Connection). A pool connection goes back to the pool only when the server confirmed the stop (the statement ended with 1317, or with the server's own timeout after the KILL landed) and the KILL itself finished; a call stopped between statements also gives up its connection, since a KILL may be on its way to it. Error 1094 (no such thread) counts as delivered: there was nothing left to stop.
| Server answer | Reported as |
|---|---|
| 1317 | Cancelled, or Timeout { server_cancelled: true } when the deadline asked for it |
| 3024 (MySQL), 1969 (MariaDB) | Timeout { phase: Statement, server_cancelled: true } |
| 1205 | Timeout { phase: LockWait } |
| 1792 | ReadOnly(ServerRejected) |
| 1045 | Auth |
| 1040 | Busy(NoFreeConnection) |
| 3159 | Tls { failure: ServerRequiresTls } |
| 1053, 1927 (MariaDB), 4031 (MySQL) | ConnectionLost |
A call that timed out or lost its connection is checked before the link is declared lost, as adding-drivers.md describes, with the KILL as the stop and a login of its own as the check.
KILL QUERY stops the server thread CONNECTION_ID() named at login. Behind a proxy that multiplexes client connections over server connections, as ProxySQL does by default, the client may not be on that thread any more, and the KILL can miss or stop another client's statement. The driver has no proxy detection. What it does guarantee: a stop counts as confirmed only when the statement itself reported it, and a connection whose stop was not confirmed within the cancel grace is closed, so the app never claims a cancel that did not happen. Point TablePro at a ProxySQL user or query rule with multiplexing off, or at the server directly.
ReadOnlyEnforcement::SessionDefault: SET SESSION TRANSACTION READ ONLY on every session. Saves are refused before anything is sent, and a write through generated SQL fails with 1792. In the editor, the user's own SET TRANSACTION READ WRITE or SET SESSION TRANSACTION READ WRITE lifts it. For a guarantee, connect with a login that has no write privileges.
An export, on any connection, runs in START TRANSACTION READ ONLY and refuses user SQL with a statement that does not start with SELECT, WITH, SHOW, DESCRIBE, DESC, EXPLAIN, TABLE or a parenthesis.
Whether a write can be undone depends on the table's storage engine, not on the server. Before a row change the driver reads the table's engine and whether that engine supports transactions (information_schema.TABLES joined to ENGINES):
| Change | Engine | How it runs |
|---|---|---|
| Row changes | Transactional (InnoDB) | One transaction. A failure rolls back. |
| Row changes | Not transactional (MyISAM, MEMORY, ARCHIVE, CSV, MariaDB's Aria), or a view | Every probe first, then statement by statement. Each statement lands on its own, and the WriteLedger reports how far it got. |
| Schema changes | Any | Statement by statement: each DDL statement commits on its own. |
- An exact row guard relies on
CLIENT_FOUND_ROWS, which sqlx turns on: anUPDATEreports the rows it matched, also the ones it left unchanged. - On a non-transactional table, a statement whose connection drops before its answer may have run, so it is reported as unknown, not as refused.
- A trigger on an InnoDB table can write into a non-transactional one. A
ROLLBACKthen leaves those rows, and the server says so with warning 1196. The driver readsSHOW WARNINGSafter every rollback, and a 1196 makes the outcomePartiallyAppliedinstead ofRolledBack. A MySQL server that enforces GTID consistency, as 9.7 does by default, refuses such a save with 1785 before anything lands, and it reads asRolledBack. - MariaDB's
max_statement_timewould stop a longALTERthe way it stops aSELECT, so a schema change on MariaDB lifts it for the batch and puts it back after. A connection where it cannot be put back is closed. MySQL's bound covers onlySELECT. - Every step is prepared, also one with no parameters, so a stray
;in generated SQL fails instead of running a second statement. The prepared statement is closed after it runs. A step with no parameters that the server cannot prepare (error 1295, such asCREATE TRIGGER) is sent again over the text protocol. - MySQL cannot suspend foreign keys for one batch, so a batch that asks for it is refused before anything is sent.
- There is nothing to ask the server about a lost
COMMIT(CommitResolutionSupport::Unavailable).CommitOutcomeUnknowncarries no token, and the user reloads to see what landed.
- The editor sends one statement per run. Text with several statements runs as the server's own multi-statement batch over the text protocol, except that mysql-client
DELIMITERlines split it where the client would, because the server does not know the directive. - The row limit is
SET SESSION sql_select_limit = <limit + 1>, sent when the limit changes. The server stops a plainSELECTthere, and the set ends withTruncation::ServerStoppedand no total. ASELECTwith aLIMITof its own, or one inside a routine, is read to its end: the rows past the limit are counted, never decoded, and the set ends withTruncation::ReadPastLimitand its total. A set that ends exactly one row past the limit reads asServerStoppedeither way. - A stop sends
KILL QUERYon the session's own control connection and gives the statement the cancel grace to end. A statement that has not ended by then ends the session, because its answer is still on the way. - When the grid stops reading, the run sends the same
KILL QUERYand ends as cancelled. The batch already sent is read to its end, unshown, and no later batch is sent. - sqlx describes a result column by its type alone, with no fractional digits and no BIT length. A run shows a DATETIME(6) holding
12:00:00.5as12:00:00.5and a BIT(1) in the whole byte it arrived in,00000001. A table tab and its export take both from the catalogue and show12:00:00.500000and1. - The transaction state after each run comes from
@@in_transactionon MariaDB and fromperformance_schema.events_transactions_currenton MySQL. On MySQL before 8.0.16, withperformance_schemaoff, or for a login that cannot read it, the state isUnknown, and the tab's close guard warns instead of guessing.