moonmysql

    Pure-MoonBit MySQL wire-protocol driver implementing @moondb.Driver — no C, like PyMySQL.

    mysql
    mariadb
    driver
    database
    sql
    wire-protocol
    protocol
    client
    tls
    caching-sha2-password
    prepared-statement
    async
    pool
    pymysql
    moondb
    wire
    moonbit
    Download zip
    Version
    0.7.11
    License
    Apache-2.0
    Last updated
    10 hours ago
    Downloads
    494

    #moonmysql

    Moved on mooncakes from Lfan-ke/moonmysql to moonbitstack/moonmysql.

    A pure-MoonBit MySQL / MariaDB wire-protocol driver — no C, no bindings, like PyMySQL. It speaks the MySQL client/server protocol (version 10 handshake, mysql_native_password / caching_sha2_password / MariaDB client_ed25519 auth, the text COM_QUERY protocol and the binary prepared-statement protocol) directly over a raw socket, and implements the @moondb.Driver contract so a moondb-based query layer (moonorm) can talk to MySQL through the same seam as the SQLite and Postgres drivers.

    MariaDB speaks the same wire protocol and is a first-class target: the handshake parser recognises MariaDB's 5.5.5- version sentinel (recovering the real version and reporting server_kind = MariaDB), reads past its extended capability bits without disturbing the MySQL-8 offsets, and authenticates over the shared mysql_native_password path. CI runs the same integration suite against MySQL 8, MariaDB 11, and MariaDB 10.11.

    $ moon add moonbitstack/moonmysql

    Previously published as Lfan-ke/moon-mysql.

    • API reference: https://moonbitstack.github.io/moonorm/mysql/
    • License: Apache-2.0

    #Quickstart

    The synchronous @moondb.Driver adapter is the one-call-per-statement surface:

    let db = @client.MysqlDriver::new(
    user="root", password="root", database="test",
    host="127.0.0.1", port=3306,
    )
    db.execute("CREATE TABLE t (id INT PRIMARY KEY, name TEXT)", [])
    db.execute("INSERT INTO t VALUES (?, ?)", [@moondb.Int(1), @moondb.Text("a")])
    let rows = db.query("SELECT id, name FROM t", [])
    rows[0].int_by("id") // => 1
    rows[0].text_by("name") // => "a"

    For long-lived connections and real transactions, use the asynchronous MysqlConn directly (see The async wall below):

    async fn run() -> Unit raise {
    let conn = @client.MysqlConn::connect("127.0.0.1", 3306, "root", "root", "test")
    conn.begin()
    conn.execute("INSERT INTO t VALUES (?, ?)", [@moondb.Int(2), @moondb.Text("b")])
    conn.commit()
    let rows = conn.query("SELECT * FROM t", [])
    conn.close()
    }

    #Architecture

    The library is split so the entire wire format is testable off any backend. The socket transport supports native and wasm; wasm runtime use requires a host with TCP socket bindings, such as moonrun, and does not include browser wasm.

    flowchart TD subgraph root["moonbitstack/moonmysql · pure codec · target: all backends"] sha1["sha1 — FIPS 180-4"] packet["packet — lenenc / fixed-int / string cursor"] handshake["handshake — parse + scramble + response"] response["response — OK/ERR/EOF + text-row decode"] binding["binding — ? placeholders → escaped literals"] end subgraph client["moonbitstack/moonmysql/client · native + wasm"] conn["MysqlConn — async transport over @socket.Tcp"] driver["MysqlDriver — sync @moondb.Driver adapter"] end handshake --> conn response --> conn binding --> conn packet --> handshake packet --> response sha1 --> handshake conn --> driver driver -. implements .-> moondb["@moondb.Driver"]

    %% root (pure, all backends) client (native + wasm host) %% sha1 ─┐ MysqlConn ── async socket transport %% packet┼─ handshake ─┐ │ %% └─ response ──┼──────────────► │ %% binding ───┘ MysqlDriver ── impl @moondb.Driver

    Every packet field type — the 3-byte length + sequence-id framing, length-encoded and fixed-width little-endian integers, NUL- and length-encoded strings — is parsed through an in-memory cursor in the pure half, so the codec is unit-tested on wasm, wasm-gc, js, and native. The client package adds the async socket transport and the driver. It compiles on native and wasm; wasm execution requires host-provided TCP socket bindings (verified with moonrun), so browser wasm is not a supported runtime.

    #The async wall

    moonbitlang/async sockets are asynchronous, but @moondb.Driver's methods are synchronous, and MoonBit forbids calling an async function from a non-async one. The synchronous adapter bridges each call through @async.run_async_main, which runs an async body to completion on a fresh event loop.

    A socket does not survive across two such event loops (each tears its file descriptors down — verified empirically), so the synchronous MysqlDriver opens a fresh connection, authenticates, runs one statement, and closes, on every execute/query. This is correct and efficient enough for autocommit work (which is exactly what a moondb roundtrip is), but it means a transaction cannot be held open across separate Driver calls. begin/commit/rollback on the synchronous adapter therefore raise a clear DbError directing you to the asynchronous MysqlConn, which owns its socket for its whole lifetime and brackets real server-side transactions. This is a language constraint, not a shortcut.

    #Injection safety

    Values never reach the server as SQL text unescaped. A statement with parameters goes over COM_STMT_PREPARE/COM_STMT_EXECUTE, where the values travel in the binary protocol and the SQL text is never built — the server has already parsed it. The text protocol is used only for a statement with no parameters.

    #Prepared statements

    MysqlConn::prepare sends COM_STMT_PREPARE and returns an @moondb.AsyncStmt: bind sends new values, execute/query run the statement, close sends COM_STMT_CLOSE. A :name statement is rewritten to ? at prepare time and the slot names are kept, so bind_named takes a map.

    The binary protocol's row decoder reads the column flags word: an INT UNSIGNED above 2^31-1 comes back as Int64 rather than a negative Int, and a BIGINT UNSIGNED above 2^63-1 keeps its exact digits as Text.

    #Long parameters

    A parameter of one mebibyte or more is not carried inside COM_STMT_EXECUTE. It is streamed first, in COM_STMT_SEND_LONG_DATA packets of one mebibyte each; the execute packet that follows still declares its type — the server needs to know how to read the buffered bytes — but does not repeat the value. The server appends the pieces in order and answers none of them, so a streamed statement still costs one round trip.

    async fn store(conn : @client.MysqlConn, id : Int, raw : Bytes) -> Unit raise {
    // A 40 MB blob: four long-data packets, then a one-packet execute.
    conn.execute("INSERT INTO asset VALUES (?, ?)", [
    @moondb.Int(id), @moondb.Blob(raw),
    ])
    |> ignore
    }

    No call site decides this: the driver does, by size, for every parameter of every prepared statement — so execute, query, a prepared handle and the internal paths all behave the same way. @moonmysql.long_data_chunk is the number, one mebibyte, and it is both the threshold and the packet size.

    Why it exists: max_allowed_packet caps a single packet — 64 MiB on MySQL 8 and 16 MiB on MariaDB by default — and an execute packet holds every parameter at once. A value near that cap is un-sendable inline, however it is encoded; chunked, each packet is one mebibyte and the limit stops applying to the value itself.

    #Type mapping (text protocol)

    MySQL column type@moondb.Value
    TINYINT / SMALLINT / INT / MEDIUMINT / YEARInt
    BIGINTInt64
    FLOAT / DOUBLEDouble
    binary-collation columns (BLOB, VARBINARY, …)Blob
    VARCHAR / TEXT / DECIMAL / JSON / temporal (ISO text)Text
    SQL NULLNull

    #Tested against a real MySQL and MariaDB

    CI (.github/workflows/ci.yml) starts a real MySQL 8 (pinned to mysql_native_password) and, in separate jobs, MariaDB 11 and MariaDB 10.11, then runs moon check/build/test on native against each. The integration tests — gated by MYSQL_TEST so a local moon test without a database skips them — connect, authenticate, create tables, insert parameter-bound rows (integers, UTF-8 text, an embedded quote), and assert the decoded result sets cell by cell; the same suite is green on all three servers. Among them:

    • Protocol parity — one table of twenty column types, read once with COM_QUERY and once through a prepared statement, compared value by value, so the protocol a caller happens to take cannot change what it reads.
    • Long parameters — a blob and a text value above one mebibyte inserted through a prepared statement, then read back over both protocols and compared byte for byte, next to a parameter just under the threshold that stayed inline.

    The pure codec additionally runs on every backend, and parse_server_version has a unit test covering both MariaDB's 5.5.5- sentinel and a plain MySQL version string.

    #Consistency

    Every column type reads the same through the text and the binary protocol, with one exception the server causes and no client can paper over: a FLOAT column. MySQL renders float with six significant digits in the text protocol (1.23457) and sends all four bytes in the binary one, so the two paths carry different doubles for the same stored 32-bit value. It is pinned by a test rather than smoothed over: read FLOAT through a prepared statement, or use DOUBLE, when the exact 32-bit value matters.

    A JSON column is a dialect difference rather than a divergence: MySQL parses it into a JSON type of its own and reads back as Json with its keys reordered, while MariaDB's JSON is LONGTEXT with a validity check and reads back as Text, word for word as written. Both protocols report whichever the server does.

    #Authentication

    • mysql_native_password — SHA1 scramble, and the down-negotiation a MariaDB server may ask for through AuthSwitchRequest.
    • caching_sha2_password — MySQL 8's default. The fast path uses the cached SHA256 scramble; a cold cache triggers full authentication, where the password is XOR-ed with the nonce and sealed with RSA-OAEP under the server's public key. Over TLS the credential goes in the clear, which is what the protocol specifies.
    • client_ed25519 — MariaDB's plugin: the 32-byte nonce is signed with the password-derived Ed25519 secret, and a wrong signature fails the login. CI creates an ed25519 account and authenticates through it.
    • TLS — MysqlDriver::new(tls=…) / MysqlConn::connect(tls=…) swap the transport after the greeting; the SSLRequest and the handshake response carry the same capability word.

    Any other plugin raises a clear UnsupportedError rather than silently mis-authenticating.

    #Result sets, charsets, unsigned columns

    One statement may answer with more than one result set — a stored procedure's CALL does. MysqlConn::query_multi returns every set in the order the server sent them; the single-result path drains the rest so the connection stays in step. CLIENT_MULTI_RESULTS and CLIENT_MULTI_STATEMENTS are advertised for this.

    The connection charset is the charset parameter of connect (a collation id, 45 = utf8mb4_general_ci by default). A charset whose trailing byte can be 0x5C (gbk, big5, sjis, cp932, gb18030) is refused when escaping would be load-bearing, because a backslash there can continue a multibyte character.

    #Not covered

    • sha256_password — MySQL's older RSA plugin, superseded by caching_sha2_password and not a default anywhere current.
    • Compressed protocol and the X protocol. Neither changes what a query layer can ask for.
    • LOAD DATA LOCAL INFILE. It is disabled on most servers; COPY-style bulk loading goes through prepared statements here.

    #Parameter binding

    The text protocol has no out-of-band binding, so bind_params renders each value as an escaped literal. That makes the escaping load-bearing rather than cosmetic: a connection asks the server for @@sql_mode and @@character_set_connection at connect time and escapes for what it finds — a doubled quote under NO_BACKSLASH_ESCAPES, a backslash otherwise. A charset whose characters can end in 0x5C (gbk, big5, sjis, cp932, gb18030) cannot be escaped safely with backslashes at all, so such a connection is refused rather than served.

    A statement with parameters never takes that path: it goes over COM_STMT_PREPARE/COM_STMT_EXECUTE, where the values travel in the binary protocol and the SQL text is never built.

    MysqlError

    pub(all) suberror MysqlError {
    ProtocolError(String)
    ServerError(Int, String, String)
    UnsupportedError(String)
    } derive(Eq)

    A failure raised while speaking the MySQL wire protocol. It is deliberately distinct from @moondb.DbError: the pure codec and the socket transport raise MysqlError (which carries wire-level specifics — a server error code, an unsupported auth plugin, a malformed packet), and the @moondb.Driver adapter maps it onto the coarse DbError cases at the public boundary.

    • ProtocolError — a packet did not parse: short read, bad prefix byte, an out-of-place packet in the result-set state machine.
    • ServerError — the server sent an ERR packet. Carries the numeric error code, the 5-char SQLSTATE, and the human message verbatim.
    • UnsupportedError — a code path this round does not implement (e.g. the caching_sha2_password full-auth exchange, a LOCAL INFILE request).
    impl Show for MysqlError

    MysqlError::equal

    fn MysqlError::equal(MysqlError, MysqlError) -> Bool

    MysqlError::not_equal

    fn MysqlError::not_equal(x : MysqlError, y : MysqlError) -> Bool

    MysqlError::output

    fn MysqlError::output(self : MysqlError, logger : &Logger) -> Unit

    MysqlError::to_string

    fn MysqlError::to_string(self : MysqlError) -> String

    A one-line rendering, e.g. ServerError(1146, 42S02): Table 'test.t' doesn't exist.

    ColumnDef

    pub(all) struct ColumnDef {
    name : String
    column_type : Int
    charset : Int
    flags : Int
    } derive(Eq)

    One result-set column's definition, reduced to what text decoding needs: the projected name, the MySQL type code, the collation (to tell text from binary), and the column flags.

    ColumnDef::equal

    fn ColumnDef::equal(ColumnDef, ColumnDef) -> Bool

    ColumnDef::not_equal

    fn ColumnDef::not_equal(x : ColumnDef, y : ColumnDef) -> Bool

    Handshake

    pub struct Handshake {
    server_version : String
    server_kind : ServerKind
    connection_id : Int
    salt : Bytes
    capability : Int
    mariadb_capability : Int
    charset : Int
    status : Int
    auth_plugin : String
    }

    The server's initial Handshake packet (protocol version 10), decoded down to the fields the client needs: the 20-byte auth salt, the negotiated capabilities, and the auth-plugin name that selects the scramble.

    OkPacket

    pub(all) struct OkPacket {
    affected_rows : Int64
    last_insert_id : Int64
    status : Int
    warnings : Int
    } derive(Eq)

    An OK packet: a statement that returned no result set (INSERT/UPDATE/DELETE/DDL) or the terminator of a successful command.

    OkPacket::equal

    fn OkPacket::equal(OkPacket, OkPacket) -> Bool

    OkPacket::not_equal

    fn OkPacket::not_equal(x : OkPacket, y : OkPacket) -> Bool

    PacketReader

    pub struct PacketReader {
    data : Bytes
    pos : Int
    }

    A cursor over one decoded MySQL packet payload. Every field type the protocol uses — fixed-width little-endian integers, length-encoded integers and strings, NUL-terminated strings, and raw byte runs — is read through this, advancing a position and raising [MysqlError::ProtocolError] on any short read. It holds no socket: the transport reads a full payload into Bytes first, then parses it here, which is what lets the whole codec be tested off any backend.

    PacketReader::at_end

    fn PacketReader::at_end(self : PacketReader) -> Bool

    Whether the cursor has consumed the whole payload.

    PacketReader::bytes

    fn PacketReader::bytes(self : PacketReader, n : Int) -> Bytes raise MysqlError

    Read n raw bytes.

    PacketReader::lenenc_bytes

    fn PacketReader::lenenc_bytes(self : PacketReader) -> Bytes? raise MysqlError

    Read a length-encoded string (the string<lenenc> type), returning None for the 0xFB NULL sentinel that appears in text-protocol rows.

    PacketReader::lenenc_uint

    fn PacketReader::lenenc_uint(self : PacketReader) -> Int64 raise MysqlError

    Read a length-encoded unsigned integer (the int<lenenc> type). The 0xFB NULL sentinel and 0xFF are rejected here — NULL only has meaning inside a row, which [lenenc_bytes] handles.

    PacketReader::new

    fn PacketReader::new(data : Bytes) -> PacketReader

    Wrap a decoded packet payload for reading from the start.

    PacketReader::peek

    fn PacketReader::peek(self : PacketReader) -> Int

    Peek the next byte without advancing; -1 at end of payload.

    PacketReader::remaining

    fn PacketReader::remaining(self : PacketReader) -> Int

    Bytes not yet consumed.

    PacketReader::rest

    fn PacketReader::rest(self : PacketReader) -> Bytes

    Read everything left in the payload (an EOF-terminated string field).

    PacketReader::skip

    fn PacketReader::skip(self : PacketReader, n : Int) -> Unit raise MysqlError

    Skip n bytes.

    PacketReader::string_nul

    fn PacketReader::string_nul(self : PacketReader) -> Bytes raise MysqlError

    Read a NUL-terminated string, consuming the terminator.

    PacketReader::u8

    fn PacketReader::u8(self : PacketReader) -> Int raise MysqlError

    Read one byte as an Int in 0..=255.

    PacketReader::uint_le

    fn PacketReader::uint_le(self : PacketReader, n : Int) -> Int64 raise MysqlError

    Read an n-byte little-endian unsigned integer.

    QuoteMode

    pub(all) enum QuoteMode {
    Backslash
    DoubleQuote
    } derive(Eq)

    How the server reads a string literal, which decides how a value has to be escaped. NO_BACKSLASH_ESCAPES is part of ANSI and of several stock sql_mode combinations: there, a backslash is an ordinary character and the only escape is a doubled quote — so a backslash-escaping writer leaves the value's own quote live and the statement is injectable.

    QuoteMode::equal

    fn QuoteMode::equal(QuoteMode, QuoteMode) -> Bool

    QuoteMode::not_equal

    fn QuoteMode::not_equal(x : QuoteMode, y : QuoteMode) -> Bool

    ServerKind

    pub(all) enum ServerKind {
    MySQL
    MariaDB
    } derive(Eq,
    Debug
    )

    Which server dialect answered the handshake. Both speak the MySQL wire protocol; they diverge in the version string and the extended capabilities.
    impl Show for ServerKind

    ServerKind::equal

    fn ServerKind::equal(ServerKind, ServerKind) -> Bool

    ServerKind::not_equal

    fn ServerKind::not_equal(x : ServerKind, y : ServerKind) -> Bool

    ServerKind::output

    fn ServerKind::output(self : ServerKind, logger : &Logger) -> Unit

    ServerKind::to_string

    fn ServerKind::to_string(self : ServerKind) -> String

    StmtOk

    pub(all) struct StmtOk {
    id : Int
    num_columns : Int
    num_params : Int
    }

    The server's reply to COM_STMT_PREPARE: the statement handle and how many parameters and result columns the statement has.

    bind_params

    fn bind_params(sql : String, params : Array[
    Value
    ], mode? : QuoteMode) -> Bytes raise MysqlError

    Substitute the ordered params for the ? placeholders in sql, producing the UTF-8 query bytes a COM_QUERY carries.

    The text protocol has no out-of-band parameter binding — that arrives with prepared statements (COM_STMT_PREPARE, binary protocol) in a later round — so values are rendered as escaped literals here. The scanner tracks single-, double-, and backtick-quoted spans (honouring backslash escapes) so a literal ? inside a string or identifier is never mistaken for a placeholder, and it raises when the placeholder and parameter counts disagree.

    build_handshake_response

    fn build_handshake_response(handshake : Handshake, user : String, password : Bytes, database : String, ssl? : Bool, charset? : Int) -> Bytes raise MysqlError

    Build the client's HandshakeResponse41 payload for user/password/database.

    mysql_native_password and caching_sha2_password (MySQL 8's default, fast-path scramble here and full auth driven in [MysqlConn::connect]) are both handled directly. For any other plugin the client advertises mysql_native_password and sends a native token, so a native-capable account authenticates and otherwise the server drives an AuthSwitchRequest the connection layer answers (native or caching_sha2) or rejects. Only a server that requires a non-native plugin and does not offer pluggable auth is rejected here. client_ed25519 is answered by the connection layer when MariaDB switches to it.

    build_ssl_request

    fn build_ssl_request(database : String, charset? : Int) -> Bytes

    The 32-byte SSLRequest payload the client sends before starting TLS: the same capability word as the handshake response (with CLIENT_SSL), the max packet size, the charset, and 23 reserved zero bytes.

    build_stmt_close

    fn build_stmt_close(id : Int) -> Bytes

    COM_STMT_CLOSE (0x19): the statement id; the server answers nothing.

    build_stmt_execute

    fn build_stmt_execute(id : Int, params : Array[
    Value
    ], streamed? : Array[Int]) -> Bytes

    COM_STMT_EXECUTE (0x17) for statement id, binding params out-of-band. Every non-NULL parameter is declared as VAR_STRING (or BLOB for bytes) and sent length-encoded; the server coerces it to the column type. The NULL bitmap uses bit i for parameter i, LSB first.

    streamed names the parameters whose value was already sent with COM_STMT_SEND_LONG_DATA: their type is still declared here — the server needs to know how to read the buffered bytes — but their value is not repeated.

    build_stmt_prepare

    fn build_stmt_prepare(sql : String) -> Bytes

    COM_STMT_PREPARE (0x16): the SQL text follows the command byte.

    build_stmt_send_long_data

    fn build_stmt_send_long_data(id : Int, param : Int, chunk : Bytes) -> Bytes

    COM_STMT_SEND_LONG_DATA (0x18): statement id, parameter index, then one slice of that parameter's value. The server appends slices in the order they arrive, answers nothing at all, and forgets the accumulated value once the statement is executed or reset — so the pieces must be sent immediately before the execute that binds them.

    build_text_rows

    fn build_text_rows(columns : Array[ColumnDef], row_payloads : Array[Bytes]) -> Array[
    Row
    ] raise MysqlError

    Build the materialised rows of a text-protocol result set from its column definitions and the raw row packets. This is the pure heart of query: the socket transport collects columns and row_payloads, and every cell decode happens here, off any backend.

    bytes_to_string

    fn bytes_to_string(b : Bytes) -> String

    Bytes → String, lossily (server version, error text, text-protocol cells).

    caching_sha2_full_auth_token

    fn caching_sha2_full_auth_token(password : Bytes, nonce : Bytes, pem : String, seed : Bytes) -> Bytes raise MysqlError

    The caching_sha2_password full-auth token: the NUL-terminated password XORed with the nonce (cycled), then RSA-OAEP encrypted under the server's public key.

    seed is the 20 random octets OAEP masks with. MySQL's own client draws them from OpenSSL; here they are a parameter, because this package has no entropy source and a caller that reuses a seed should have to write that down.

    caching_sha2_scramble

    fn caching_sha2_scramble(password : Bytes, nonce : Bytes) -> Bytes

    The caching_sha2_password fast-auth scramble: SHA256(pw) XOR SHA256( SHA256(SHA256(pw)) ‖ nonce ), 32 bytes. An empty password sends an empty token.

    client_connect_with_db

    let client_connect_with_db : Int

    The CLIENT_CONNECT_WITH_DB capability flag of the connection handshake.

    client_long_flag

    let client_long_flag : Int

    The CLIENT_LONG_FLAG capability flag of the connection handshake.

    client_long_password

    let client_long_password : Int

    The CLIENT_LONG_PASSWORD capability flag of the connection handshake.

    client_multi_results

    let client_multi_results : Int

    CLIENT_MULTI_RESULTS: one statement may answer with more than one result set — what a stored procedure's CALL needs. Without it the server rejects a procedure that returns rows (ER_SP_BADSELECT).

    client_multi_statements

    let client_multi_statements : Int

    CLIENT_MULTI_STATEMENTS: one COM_QUERY may carry several ;-separated statements.

    client_plugin_auth

    let client_plugin_auth : Int

    The CLIENT_PLUGIN_AUTH capability flag of the connection handshake.

    client_protocol_41

    let client_protocol_41 : Int

    The CLIENT_PROTOCOL_41 capability flag of the connection handshake.

    client_secure_connection

    let client_secure_connection : Int

    The CLIENT_SECURE_CONNECTION capability flag of the connection handshake.

    client_ssl

    let client_ssl : Int

    The CLIENT_SSL capability flag of the connection handshake: the client will begin a TLS handshake after an SSLRequest packet.

    client_transactions

    let client_transactions : Int

    The CLIENT_TRANSACTIONS capability flag of the connection handshake.

    concat_bytes

    fn concat_bytes(a : Bytes, b : Bytes) -> Bytes

    Concatenate two byte strings.

    decode_binary_row

    fn decode_binary_row(payload : Bytes, columns : Array[ColumnDef]) ->
    Row
    raise MysqlError

    Decode one binary-protocol result row against columns into a [@moondb.Row]. The row header is 0x00, then a NULL bitmap offset by two bits, then each non-NULL value in the column's binary type.

    decode_text_value

    fn decode_text_value(raw : Bytes?, col : ColumnDef) ->
    Value
    raise MysqlError

    Map one text-protocol cell (None = the 0xFB NULL sentinel) onto a [@moondb.Value] using the column's declared type. Integers narrow to Int except BIGINT, which keeps 64 bits; FLOAT/DOUBLE become Double; binary-collation strings become Blob; everything else (VARCHAR, TEXT, DECIMAL, temporal types as ISO text, JSON) becomes Text.

    eof_status

    fn eof_status(payload : Bytes) -> Int

    The status-flags word of an EOF packet (0xFE, warnings LE16, status LE16), or 0 for a packet that is not a protocol-41 EOF.

    has_more_results

    fn has_more_results(payload : Bytes) -> Bool

    Whether an EOF packet's status word says another result set follows.

    is_auth_switch_request

    fn is_auth_switch_request(payload : Bytes) -> Bool

    Whether payload is an AuthSwitchRequest (0xFE lead byte with a body — the length is what tells it apart from a 5-byte EOF, exactly as [is_eof_packet] keys off < 9).

    is_eof_packet

    fn is_eof_packet(payload : Bytes) -> Bool

    Whether payload is an EOF packet (first byte 0xFE, fewer than 9 bytes — which is what tells it apart from a row whose first cell is an 8-byte length-encoded value).

    is_err_packet

    fn is_err_packet(payload : Bytes) -> Bool

    Whether payload is an ERR packet (first byte 0xFF).

    is_ok_packet

    fn is_ok_packet(payload : Bytes) -> Bool

    Whether payload is an OK packet (first byte 0x00, at least 7 bytes).

    long_data_chunk

    let long_data_chunk : Int

    How much of one parameter a COM_STMT_SEND_LONG_DATA packet carries, and the length at which a parameter stops travelling inside the execute packet and is streamed this way instead.

    A packet is capped by the server's max_allowed_packet (64 MiB on MySQL 8 by default, 16 MiB on MariaDB), and the execute packet has to hold every parameter at once, so a value near that cap used to be un-sendable however it was encoded. One mebibyte is a sixteenth of the smaller default, which leaves room for the rest of the packet whatever the server is set to.

    long_data_params

    fn long_data_params(params : Array[
    Value
    ]) -> Array[Int]

    The parameter indexes, ascending, whose value is streamed rather than inlined. build_stmt_execute takes this list so it can leave their value bytes out.

    long_data_slices

    fn long_data_slices(params : Array[
    Value
    ], index : Int) -> Array[Bytes]

    The slices of parameter index to send with COM_STMT_SEND_LONG_DATA, in order. Empty when the value fits in the execute packet, which is the case for everything shorter than [long_data_chunk] and for NULL.

    mariadb_client_cache_metadata

    let mariadb_client_cache_metadata : Int

    MariaDB's MARIADB_CLIENT_CACHE_METADATA capability flag of the connection handshake.

    mariadb_client_com_multi

    let mariadb_client_com_multi : Int

    MariaDB's MARIADB_CLIENT_COM_MULTI capability flag of the connection handshake.

    mariadb_client_extended_metadata

    let mariadb_client_extended_metadata : Int

    MariaDB's MARIADB_CLIENT_EXTENDED_METADATA capability flag of the connection handshake.

    mariadb_client_progress

    let mariadb_client_progress : Int

    MariaDB's MARIADB_CLIENT_PROGRESS capability flag of the connection handshake.

    mariadb_client_stmt_bulk_operations

    let mariadb_client_stmt_bulk_operations : Int

    MariaDB's MARIADB_CLIENT_STMT_BULK_OPERATIONS capability flag of the connection handshake.

    mariadb_ed25519_response

    fn mariadb_ed25519_response(password : Bytes, scramble : Bytes) -> Bytes

    The MariaDB client_ed25519 response: the 64-byte Ed25519 signature of the server's 32-byte challenge, keyed by the password.

    MariaDB keys the signature with the password itself, where RFC 8032 would expand a 32-byte seed — the derivation PrivateKey::of_secret exists for. The plugin signs unconditionally, so an empty password expands to SHA-512("") and still yields a valid signature.

    native_password_scramble

    fn native_password_scramble(password : Bytes, salt : Bytes) -> Bytes

    The mysql_native_password challenge response: SHA1(password) XOR SHA1(salt ++ SHA1(SHA1(password))), 20 bytes. An empty password produces an empty response (the server accepts a zero-length token).

    parse_auth_switch_request

    fn parse_auth_switch_request(payload : Bytes) -> (String, Bytes) raise MysqlError

    Decode an AuthSwitchRequest into the plugin name the server wants and the fresh 20-byte auth salt for it. The trailing NUL that follows the scramble is dropped so the salt feeds [native_password_scramble] directly.

    parse_column_def

    fn parse_column_def(payload : Bytes) -> ColumnDef raise MysqlError

    Decode a column-definition packet (protocol 41). Only the fields that steer text decoding are kept; catalog/schema/table names and the length/decimals fields are read past.

    parse_ed25519_challenge

    fn parse_ed25519_challenge(payload : Bytes) -> Bytes raise MysqlError

    The 32-byte nonce from a client_ed25519 AuthSwitchRequest. MariaDB sends exactly NONCE_BYTES (= 32) with no NUL terminator and the client signs all of them, so — unlike the native scramble — the full 32 bytes are read raw rather than through the NUL-stripping [parse_auth_switch_request].

    parse_err

    fn parse_err(payload : Bytes) -> MysqlError raise MysqlError

    Decode a full ERR packet (including its 0xFF marker) into the [MysqlError::ServerError] it represents.

    parse_handshake

    fn parse_handshake(payload : Bytes) -> Handshake raise MysqlError

    Parse a protocol-10 initial Handshake payload. An ERR packet in its place (0xFF, e.g. "Host is blocked" / "Too many connections") is surfaced as a [MysqlError::ServerError].

    parse_ok

    fn parse_ok(payload : Bytes) -> OkPacket raise MysqlError

    Decode an OK packet (including its 0x00 marker).

    parse_rsa_public_key

    fn parse_rsa_public_key(pem : String) ->
    PublicKey
    raise MysqlError

    The RSA public key inside the PEM SubjectPublicKeyInfo the server sends.

    parse_server_version

    fn parse_server_version(raw : String) -> (ServerKind, String)

    Classify a raw handshake version string and recover the real version.

    MariaDB 10+ prefixes its version with the fake 5.5.5- sentinel (MySQL 5.5.5 was never released), so pre-10 clients that gate on the leading 5. still connect. Strip it to get the true version — e.g. 5.5.5-11.4.2-MariaDB-ubu2404 → 11.4.2-MariaDB-ubu2404 as [ServerKind::MariaDB]; a plain 8.0.35 is returned unchanged as [ServerKind::MySQL].

    parse_stmt_prepare_ok

    fn parse_stmt_prepare_ok(payload : Bytes) -> StmtOk raise MysqlError

    Decode the COM_STMT_PREPARE_OK packet: 0x00, statement id (LE32), column count (LE16), parameter count (LE16), one reserved byte, warning count (LE16).

    put_lenenc_bytes

    fn put_lenenc_bytes(buf :
    Buffer
    , s : Bytes) -> Unit

    Append a length-encoded string (its lenenc length prefix, then the bytes).

    put_lenenc_uint

    fn put_lenenc_uint(buf :
    Buffer
    , v : Int64) -> Unit

    Append a length-encoded unsigned integer.

    put_string_nul

    fn put_string_nul(buf :
    Buffer
    , s : Bytes) -> Unit

    Append raw bytes followed by a NUL terminator.

    put_uint_le

    fn put_uint_le(buf :
    Buffer
    , v : Int64, n : Int) -> Unit

    Append an n-byte little-endian unsigned integer.

    server_more_results_exists

    let server_more_results_exists : Int

    SERVER_MORE_RESULTS_EXISTS: the status flag that says another result set follows the one just terminated.