Skip to content

Phase 1 complete: wire-type codecs and the response packets - #4

Merged
m13m merged 8 commits into
masterfrom
phase-1-err-packet
Oct 29, 2026
Merged

m13m merged 8 commits into
masterfrom
phase-1-err-packet

Conversation

@m13m

@m13m m13m commented Oct 29, 2026

Copy link
Copy Markdown
Member

Finishes phase 1. Eight commits, one per green step. 31 tests, 4 of them properties.

What is here

Wire types — Mysqlex.Protocol.Codec

encode_int_lenenc/1     decode_int_lenenc/1      four size forms
encode_string_lenenc/1  decode_string_lenenc/1   length prefix + bytes
encode_string_nul/1     decode_string_nul/1      terminator

string<EOF> has no function — it is whatever remains of the payload, which callers already hold as rest. Fixed-width fields stay inline bitstring patterns. So all seven wire types are covered, two of them without code.

Response packets — Protocol.Err, Protocol.Ok, Protocol.Eof, plus a Mysqlex.Error struct.

With these, real server traffic that can now be parsed: a failed login, a successful login, the response to any INSERT/UPDATE/DELETE/DDL, and any query error with its code, SQL state and message.

What to look at

The lenenc one-byte form stops at 250, not 251. 0xFB means NULL in a text resultset row and 0xFC–0xFE are the prefixes for the longer forms, so five byte values cannot stand for themselves. Tested either side of all three boundaries plus a property across 0..2^64-1.

0xFB deliberately matches no decode clause — it raises. Recognising NULL belongs to the row parser, which has the column context; returning nil from a function whose job is integers would be worse.

EOF carries warnings before status flags — the reverse of OK, which holds the same two values the other way round. There is a test whose only purpose is catching that transposition, with values chosen so a swap cannot pass.

Capability-gated fields are guard clauses, not ifs. The OK packet's status section is three clauses matching the spec's if / else if / else; the ERR packet's SQL state is two. The handshake is full of these, so this is the pattern to agree on now.

Spec examples are used as oracles. Every packet here is checked against the byte sequence in MySQL's own documentation, not against fixtures I built from my reading of the field table. That distinction is what would have caught the 2018 server_version bug: a hand-built fixture encodes the misreading too.

Deliberately absent

  • The dispatcher. Nothing inspects a payload's first byte to choose a parser. Deciding whether 0xFE is EOF, an OK standing in for EOF, or a length-encoded integer needs the payload length and the command context.
  • Sequence id tracking. It stays a parameter; tracking across a command belongs to whatever owns the connection, which does not exist yet.
  • CLIENT_SESSION_TRACK in the OK packet — with it, info becomes a length-encoded string followed by a session-state block. The capability will not be advertised until phase 5.

Also

--warnings-as-errors caught a real one: binary-size(length) with the variable bound outside the match needs the pin operator in current Elixir. That is a semantics change, not a style warning.

The roadmap is brought up to date in the last commit — phase 1 ticked, the omissions above named, and the sections that had drifted (transport behaviour, test tiers, the branch-and-PR rule) corrected.

m13m added 8 commits October 28, 2026 23:15
The first packet body, and the one worth having first: without it every
failure in the connection phase is an opaque binary.

Checked against the example in the protocol documentation — bytes and
their meaning both from the spec, so the test is not just confirming my
own reading of the field table.

Capability flags are an argument because the SQL state is only sent when
CLIENT_PROTOCOL_41 was negotiated, and an ERR packet can arrive during
the connection phase before that is settled. The flag is a literal here;
a flags module can come when there is a second caller for it.
The branch that matters during a failed login: an ERR packet can arrive
before capabilities are settled, and then there is no SQL state to read.
Error 1045 is what that actually looks like in practice.
A packet whose capabilities claim 4.1 but which carries no '#' marker
raised a FunctionClauseError, naming a private function and telling the
caller nothing. This input comes off the network, so it should not be a
crash.

The marker carries no information — it exists only so that pre-4.1
clients could detect the field's presence — so its absence is read as
the server having sent no SQL state.
Every packet body above the frame is built from the protocol's own wire
types, and this is the one with branching: four size forms, with
boundaries at 251, 2^16 and 2^24.

Tested at both sides of each boundary, plus a property over the whole
0..2^64-1 range. The one-byte form stops at 250 rather than 251 because
0xFB means NULL in a text resultset row and 0xFC to 0xFE are the
prefixes for the longer forms, so those byte values cannot stand for
themselves.

0xFB deliberately matches no decode clause. Where it appears it means
NULL, and recognising that belongs to the row parser rather than here.
string<lenenc> is a length-encoded integer followed by that many bytes,
so it is built directly on the integer codec — including the multi-byte
prefix once a string passes 250 bytes, which the tests exercise.

string<NUL> reads to the first terminator. Its property generator
excludes zero bytes, because a string that can contain its own
terminator has no decodable encoding — a limitation of the wire type
rather than of the codec.

string<EOF> has no function here. It is whatever remains of the payload,
so callers already hold it as `rest`, and wrapping that in a call would
add a layer that computes nothing.
The success half of every command response, and the first packet built
out of the lenenc codecs: affected_rows and last_insert_id are both
length-encoded integers.

Checked against the example in the protocol documentation, plus the
cases that carry real values — an insert reporting its AUTO_INCREMENT
id, and a packet with a trailing info string.

Status flags come from one of three shapes depending on capabilities:
flags and warnings under CLIENT_PROTOCOL_41, flags alone under
CLIENT_TRANSACTIONS, neither otherwise. Expressed as three clauses with
guards, since that is what the spec's nested conditionals describe.

CLIENT_SESSION_TRACK is not handled. With that capability info becomes a
length-encoded string followed by an optional session-state block; the
driver will not advertise it, so the server will not send it, and the
parsing belongs with the rest of session tracking later.
Last of the three generic response packets. Deprecated since 5.7.5 in
favour of an OK packet standing in for it, but still sent to any
connection that does not advertise CLIENT_DEPRECATE_EOF.

It carries warnings before status flags — the reverse of the OK packet,
which holds the same two values the other way round. There is a test
whose only purpose is to catch that swap, with values chosen so a
transposition cannot pass.

Deciding whether a payload beginning 0xFE is this packet, an OK packet
standing in for it, or a length-encoded integer needs the payload
length, so it is left to whatever dispatches on the payload.
Phase 1 is finished, so its checklist is ticked and the items that were
deliberately left out are named: sequence id tracking, the 0xFE
dispatcher, and session tracking in the OK packet.

Also records what the earlier sections had drifted past — the transport
behaviour, the four test tiers and where fixtures live, the
branch-and-pull-request rule, gating checks on exit codes rather than
filtered output, and that replacing db_connection is an option rather
than a prerequisite.
@m13m
m13m merged commit 31c4239 into master Oct 29, 2026
3 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant