Use VarString for string parameter encoding in binary protocol#142
Conversation
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.
|
Note: The CI failures on PHP 8.2–8.5 are unrelated to this change — Psalm 6.15.1 started reporting |
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)
|
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.
|
Hey @trowski, fair point and you're right. I'd been telling myself "wire format is identical, so no behavior change", but Pushed b603de7 with the column-type-aware version:
Added unit coverage in The MariaDB UUID fix still works since 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. |
|
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. |
|
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 + |
|
@webpatser Thanks so much! If you'd like to make changes to our test set up for MariaDB and |
|
@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 |
* 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>
Summary
MYSQL_TYPE_LONG_BLOB(0xfb), which tells the server to treat values as raw binary dataUUIDcolumn type (introduced in MariaDB 10.7), which requires string-typed parameters to trigger string→UUID parsingMYSQL_TYPE_VAR_STRING(0xfd), which is more semantically correct for PHP string values and matches MySQL's own C API behavior for string bindsWhy this matters
MariaDB 10.7+ introduced a native
UUIDcolumn type that stores UUIDs as 16-byte binary internally. When a prepared statement parameter is typed asLONG_BLOB, MariaDB interprets the 36-byte ASCII UUID string as raw bytes instead of parsing the human-readable format. WithVAR_STRING, MariaDB correctly converts the string representation to its internal format.Safety
The wire encoding (length-prefixed bytes) is identical for both
LongBlobandVarString— only the 2-byte type header inCOM_STMT_EXECUTEchanges. 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 likeUUIDthat 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.