Skip to content

Use VarString for string parameter encoding in binary protocol#142

Merged
trowski merged 10 commits into
amphp:3.xfrom
webpatser:fix/string-parameter-type-for-mariadb-uuid
May 10, 2026
Merged

Use VarString for string parameter encoding in binary protocol#142
trowski merged 10 commits into
amphp:3.xfrom
webpatser:fix/string-parameter-type-for-mariadb-uuid

Conversation

@webpatser
Copy link
Copy Markdown
Contributor

Summary

  • String parameters in prepared statements are currently encoded as MYSQL_TYPE_LONG_BLOB (0xfb), which tells the server to treat values as raw binary data
  • This breaks MariaDB's native UUID column type (introduced in MariaDB 10.7), which requires string-typed parameters to trigger string→UUID parsing
  • Changes the type to MYSQL_TYPE_VAR_STRING (0xfd), which is more semantically correct for PHP string values and matches MySQL's own C API behavior for string binds

Why this matters

MariaDB 10.7+ introduced a native UUID column type that stores UUIDs as 16-byte binary internally. When a prepared statement parameter is typed as LONG_BLOB, MariaDB interprets the 36-byte ASCII UUID string as raw bytes instead of parsing the human-readable format. With VAR_STRING, MariaDB correctly converts the string representation to its internal format.

Safety

The wire encoding (length-prefixed bytes) is identical for both LongBlob and VarString — only the 2-byte type header in COM_STMT_EXECUTE changes. All existing tests pass (136/136).

This is effectively a no-op for all standard MySQL/MariaDB column types (VARCHAR, CHAR, TEXT, BLOB, etc.) since both types trigger the same implicit conversion paths. The difference only surfaces for MariaDB-specific types like UUID that distinguish between string and binary parameter sources.

Suggestion

The current test suite only runs against MySQL. Adding MariaDB to the CI matrix (related: #80) would help catch driver-specific issues like this. Happy to help with that in a follow-up PR if there's interest.

String parameters in prepared statements were encoded with
MYSQL_TYPE_LONG_BLOB (0xfb), which tells the server to treat
the value as raw binary data. This breaks MariaDB's native UUID
column type (introduced in 10.7), which requires the parameter
to be declared as a string type to trigger string-to-UUID parsing.

Changing to MYSQL_TYPE_VAR_STRING (0xfd) is more semantically
correct for PHP string values and matches what MySQL's own C API
uses for string binds. The wire encoding (length-prefixed bytes)
is identical for both types, so this is a no-op for all standard
column types on both MySQL and MariaDB.
Psalm 6.15.1 on PHP 8.4.19 incorrectly reports InvalidAttribute
for #[\Override] attributes. This was green on PHP 8.4.18 but
broke with the runner update. Suppress until Psalm is updated.
@webpatser
Copy link
Copy Markdown
Contributor Author

Note: The CI failures on PHP 8.2–8.5 are unrelated to this change — Psalm 6.15.1 started reporting InvalidAttribute for #[\Override] after the GitHub runner updated to PHP 8.4.19 (8.4.18 was green). The actual test suite passes on all PHP versions.

webpatser added a commit to webpatser/fledge-fiber that referenced this pull request Apr 10, 2026
Security:
- Fix HTTP/2 ping flood on active streams (amphp/http-server#386)

Bug fixes:
- Use VarString for string params in binary protocol (amphp/mysql#142)
- Decode BIT columns as int instead of string (amphp/mysql#138)
- Close connections on pool destruct (amphp/http-client#396)
- Fix duplicate keys in byte-stream split() (amphp/byte-stream#113)
- Fix Closure type annotation for static analysis (amphp/amp#451)
- Safely handle DisposedException on unsubscribe (amphp/redis#100)

Features:
- Add TLS support for Redis connections (amphp/redis#98)
- Add disperse() for concurrent closure execution (amphp/amp#460)
@trowski
Copy link
Copy Markdown
Member

trowski commented May 1, 2026

I wonder if this could negatively affect a prepared statement providing data for a blob-type field via a prepared statement.

I think this makes sense for string types by default, but I think this might need to be coupled with changes to the connection processor to set the value type based on the column type.

Switching every string parameter to VarString broke binary payloads sent
to BLOB columns: VarString carries connection-charset semantics, while
LongBlob is treated as binary (charset 63). Non-UTF8 bytes flowing into
a BLOB column on a utf8mb4 connection could trip "Incorrect string
value" or transcode silently.

The prepare response already exposes the target type per parameter, so
ConnectionProcessor::execute now forwards it to MysqlEncodedValue. For
PHP string values, the encoder picks LongBlob when the target is in the
Blob family (TinyBlob, Blob, MediumBlob, LongBlob) and VarString
otherwise. The same branch is applied to the prebound types entry,
which previously hardcoded VarString and would have hit the same issue
for COM_STMT_SEND_LONG_DATA writes to blob columns.

Keeps the MariaDB UUID fix intact (string targets like Varchar, String,
VarString fall through to VarString) while leaving binary blobs binary.
@webpatser
Copy link
Copy Markdown
Contributor Author

Hey @trowski, fair point and you're right. I'd been telling myself "wire format is identical, so no behavior change", but VarString carries connection-charset semantics where LongBlob is binary (charset 63), so a non-UTF8 payload (raw bytes, gzip, encrypted) headed into a BLOB column could trip Incorrect string value on a utf8mb4 connection or transcode silently. That's not a no-op.

Pushed b603de7 with the column-type-aware version:

  • MysqlEncodedValue::fromValue() now takes an optional target MysqlDataType
  • for string PHP values it returns LongBlob when the target is TinyBlob / Blob / MediumBlob / LongBlob, otherwise VarString
  • the prebound types entry in ConnectionProcessor::execute uses the same branch (it was hardcoded to VarString before, so this also fixes long-data sends to blob columns)
  • Stringable / BackedEnum recurse with the target type preserved

Added unit coverage in test/MysqlEncodedValueTest.php (17 assertions) covering the type-selection table, binary random_bytes into Blob, Stringable into LongBlob, and the non-string PHP types staying unchanged.

The MariaDB UUID fix still works since String / Varchar / VarString all fall through to VarString.

Adding MariaDB to the CI matrix in a follow-up still makes sense since the UUID side of this only exercises on MariaDB. Happy to take that on after this lands.

Comment thread src/Internal/MysqlEncodedValue.php Outdated
@trowski
Copy link
Copy Markdown
Member

trowski commented May 1, 2026

If you'd like to add test for MariaDB I'd certainly appreciate it. Feel free to add that to this PR or in another PR.

@webpatser
Copy link
Copy Markdown
Contributor Author

Thanks @trowski. I'll do MariaDB in a follow-up so this PR stays scoped to the encoding fix. Should be a CI matrix addition (service container + MARIADB_VERSION env) plus a small guarded test that exercises the native UUID column. Will open it once this one lands.

@trowski trowski merged commit dd4ba25 into amphp:3.x May 10, 2026
5 checks passed
@trowski
Copy link
Copy Markdown
Member

trowski commented May 10, 2026

@webpatser Thanks so much! If you'd like to make changes to our test set up for MariaDB and UUID columns I'd greatly appreciate it!

@webpatser
Copy link
Copy Markdown
Contributor Author

@trowski Thanks for getting this in. I'll kick off the MariaDB follow-up next.

Plan is a service container in the CI matrix with a MARIADB_VERSION env, plus a small guarded test exercising the native UUID column so we don't regress on the encoding path. I'll ping you when the PR is up.

trowski added a commit that referenced this pull request May 16, 2026
* Add MariaDB CI job and integration test for native UUID column

The encoding fix from #142 made prepared-statement string parameters
round-trip correctly against MariaDB's native UUID column type, but
that integration path was never exercised in CI. This adds:

- A tests-mariadb job (PHP 8.4 + 8.5) that installs MariaDB via apt
  and runs the new test against a live server
- An integration test that creates a UUID column, inserts via
  prepared statement, reads back, and asserts the value survives
  the binary protocol path

The test skips on MySQL (no native UUID type) and on MariaDB < 10.7,
so it's a no-op on the existing 5 MySQL jobs and only runs on the
new MariaDB jobs.

Verified locally against MariaDB 12.2.2 (passes). A controlled revert
of MysqlEncodedValue::fromValue() to LongBlob encoding reproduces the
original "Incorrect uuid value (1292/22007)" failure, confirming the
test catches the regression it's meant to guard.

* Run full suite on MariaDB jobs with documented vendor skips

Drops --filter and exercises the whole test suite against MariaDB, with
explicit skips for tests that hit MariaDB/MySQL semantic differences:

- testDateType YEAR rows: MariaDB rejects CAST(... AS YEAR); MySQL-only
  syntax. The actual YEAR storage type is unaffected.
- testJson: MariaDB has no native JSON wire type (aliased to LONGTEXT)
  and rejects CAST(... AS JSON). The test was designed around MySQL's
  JSON column type.
- testPrepared parameter-definition assertion: MariaDB returns
  MYSQL_TYPE_NULL for parameter placeholders by design, rather than
  the resolved types MySQL returns. Wraps just that block; the rest of
  the test (column metadata + execution + result iteration) still runs.
- testTransactionsCallbacksOnDestruct: rollback callbacks don't fire
  within delay(0) on MariaDB; needs a separate timing-tolerant test.

Adds MysqlTestCase::isMariaDb() helper that does a SELECT VERSION()
once and caches the result. Reused by all four skips so future
divergences have one obvious place to hook.

Locally: 239 tests, 752 assertions, 11 skipped, 0 failures against
MariaDB 12.2.2.

* Move MariaDB tests to existing matrix

* Remove unnecessary cast

Remove reference to non-existent method.

* Fewer connects/disconnects

* Revert change to testTransactionsCallbacksOnDestruct

---------

Co-authored-by: Aaron Piotrowski <aaron@trowski.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

2 participants