From 95f5e6fc3527b7364e9a55225098cc9e53a72836 Mon Sep 17 00:00:00 2001 From: Christoph Kempen Date: Wed, 8 Apr 2026 12:48:39 +0200 Subject: [PATCH 01/10] Use VarString instead of LongBlob for string parameter encoding 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. --- src/Internal/MysqlEncodedValue.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Internal/MysqlEncodedValue.php b/src/Internal/MysqlEncodedValue.php index 6b9e169..f4b125a 100644 --- a/src/Internal/MysqlEncodedValue.php +++ b/src/Internal/MysqlEncodedValue.php @@ -11,7 +11,7 @@ public static function fromValue(mixed $param): self { switch (\get_debug_type($param)) { case "string": - return new self(MysqlDataType::LongBlob, MysqlDataType::encodeInt(\strlen($param)) . $param); + return new self(MysqlDataType::VarString, MysqlDataType::encodeInt(\strlen($param)) . $param); case "int": if ($param >= -(1 << 7) && $param < (1 << 7)) { From 693d0b70cd0fdc20602c721fea5d9ee4f45aa5c8 Mon Sep 17 00:00:00 2001 From: Christoph Kempen Date: Wed, 8 Apr 2026 13:22:49 +0200 Subject: [PATCH 02/10] Suppress Psalm InvalidAttribute for #[\Override] 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. --- psalm.xml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/psalm.xml b/psalm.xml index c2d5b4d..6c67578 100644 --- a/psalm.xml +++ b/psalm.xml @@ -56,5 +56,11 @@ + + + + + + From bd4e36b2e2c81374b213fe6ecbc6ffaa55a1b756 Mon Sep 17 00:00:00 2001 From: Christoph Kempen Date: Wed, 8 Apr 2026 13:24:22 +0200 Subject: [PATCH 03/10] =?UTF-8?q?Revert=20psalm.xml=20changes=20=E2=80=94?= =?UTF-8?q?=20CI=20issue=20is=20pre-existing?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- psalm.xml | 6 ------ 1 file changed, 6 deletions(-) diff --git a/psalm.xml b/psalm.xml index 6c67578..c2d5b4d 100644 --- a/psalm.xml +++ b/psalm.xml @@ -56,11 +56,5 @@ - - - - - - From b603de7b7957c5609a5998c6c688fc4e67d047e3 Mon Sep 17 00:00:00 2001 From: Christoph Kempen Date: Fri, 1 May 2026 17:25:44 +0200 Subject: [PATCH 04/10] Pick string parameter type from target column 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. --- src/Internal/ConnectionProcessor.php | 11 +++- src/Internal/MysqlEncodedValue.php | 26 ++++++-- test/MysqlEncodedValueTest.php | 88 ++++++++++++++++++++++++++++ 3 files changed, 119 insertions(+), 6 deletions(-) create mode 100644 test/MysqlEncodedValueTest.php diff --git a/src/Internal/ConnectionProcessor.php b/src/Internal/ConnectionProcessor.php index df443bf..c4df034 100644 --- a/src/Internal/ConnectionProcessor.php +++ b/src/Internal/ConnectionProcessor.php @@ -483,13 +483,20 @@ public function execute(int $stmtId, string $query, array $params, array $prebou $paramType = $params[$paramId]->getType(); if (isset($prebound[$paramId])) { - $types[] = MysqlDataType::encodeInt16(MysqlDataType::VarString->value); + $preboundType = match ($paramType) { + MysqlDataType::TinyBlob, + MysqlDataType::Blob, + MysqlDataType::MediumBlob, + MysqlDataType::LongBlob => MysqlDataType::LongBlob, + default => MysqlDataType::VarString, + }; + $types[] = MysqlDataType::encodeInt16($preboundType->value); continue; } $encodedValue = match ($paramType) { MysqlDataType::Json => MysqlEncodedValue::fromJson($param), - default => MysqlEncodedValue::fromValue($param), + default => MysqlEncodedValue::fromValue($param, $paramType), }; $types[] = MysqlDataType::encodeInt16($encodedValue->getType()->value); diff --git a/src/Internal/MysqlEncodedValue.php b/src/Internal/MysqlEncodedValue.php index f4b125a..7cd8afb 100644 --- a/src/Internal/MysqlEncodedValue.php +++ b/src/Internal/MysqlEncodedValue.php @@ -7,11 +7,11 @@ /** @internal */ final class MysqlEncodedValue { - public static function fromValue(mixed $param): self + public static function fromValue(mixed $param, ?MysqlDataType $targetType = null): self { switch (\get_debug_type($param)) { case "string": - return new self(MysqlDataType::VarString, MysqlDataType::encodeInt(\strlen($param)) . $param); + return new self(self::stringTypeFor($targetType), MysqlDataType::encodeInt(\strlen($param)) . $param); case "int": if ($param >= -(1 << 7) && $param < (1 << 7)) { @@ -40,17 +40,35 @@ public static function fromValue(mixed $param): self default: if ($param instanceof \BackedEnum) { - return self::fromValue($param->value); + return self::fromValue($param->value, $targetType); } if ($param instanceof \Stringable) { - return self::fromValue((string) $param); + return self::fromValue((string) $param, $targetType); } throw new \TypeError("Unexpected type for query parameter: " . \get_debug_type($param)); } } + /** + * Picks the wire type for a PHP string parameter. Blob-family targets keep + * the binary (charset 63) interpretation of LongBlob so raw bytes are not + * transcoded against the connection charset; everything else uses VarString + * so MariaDB's native UUID column type and similar string-typed columns + * parse the value correctly. + */ + private static function stringTypeFor(?MysqlDataType $targetType): MysqlDataType + { + return match ($targetType) { + MysqlDataType::TinyBlob, + MysqlDataType::Blob, + MysqlDataType::MediumBlob, + MysqlDataType::LongBlob => MysqlDataType::LongBlob, + default => MysqlDataType::VarString, + }; + } + public static function fromJson(?string $json): self { if ($json === null) { diff --git a/test/MysqlEncodedValueTest.php b/test/MysqlEncodedValueTest.php new file mode 100644 index 0000000..f62cd00 --- /dev/null +++ b/test/MysqlEncodedValueTest.php @@ -0,0 +1,88 @@ + + */ + public function provideStringTargets(): iterable + { + yield 'no target (legacy default)' => [null, MysqlDataType::VarString]; + yield 'varchar' => [MysqlDataType::Varchar, MysqlDataType::VarString]; + yield 'string' => [MysqlDataType::String, MysqlDataType::VarString]; + yield 'var_string' => [MysqlDataType::VarString, MysqlDataType::VarString]; + yield 'tiny_blob' => [MysqlDataType::TinyBlob, MysqlDataType::LongBlob]; + yield 'blob' => [MysqlDataType::Blob, MysqlDataType::LongBlob]; + yield 'medium_blob' => [MysqlDataType::MediumBlob, MysqlDataType::LongBlob]; + yield 'long_blob' => [MysqlDataType::LongBlob, MysqlDataType::LongBlob]; + yield 'bit' => [MysqlDataType::Bit, MysqlDataType::VarString]; + yield 'enum' => [MysqlDataType::Enum, MysqlDataType::VarString]; + yield 'set' => [MysqlDataType::Set, MysqlDataType::VarString]; + yield 'geometry' => [MysqlDataType::Geometry, MysqlDataType::VarString]; + } + + /** + * @dataProvider provideStringTargets + */ + public function testStringPicksTypeBasedOnTarget(?MysqlDataType $target, MysqlDataType $expected): void + { + $encoded = MysqlEncodedValue::fromValue('hello', $target); + + self::assertSame($expected, $encoded->getType()); + } + + public function testBinaryPayloadIntoBlobStaysLongBlob(): void + { + $encoded = MysqlEncodedValue::fromValue(\random_bytes(64), MysqlDataType::Blob); + + self::assertSame(MysqlDataType::LongBlob, $encoded->getType()); + } + + public function testStringableIntoBlobStaysLongBlob(): void + { + $stringable = new class implements \Stringable { + public function __toString(): string + { + return 'cast me'; + } + }; + + $encoded = MysqlEncodedValue::fromValue($stringable, MysqlDataType::LongBlob); + + self::assertSame(MysqlDataType::LongBlob, $encoded->getType()); + } + + public function testBackedEnumIntoStringTargetUsesVarString(): void + { + $enum = StringTargetEnum::Foo; + + $encoded = MysqlEncodedValue::fromValue($enum, MysqlDataType::Varchar); + + self::assertSame(MysqlDataType::VarString, $encoded->getType()); + } + + public function testIntegerEncodingIgnoresTarget(): void + { + $encoded = MysqlEncodedValue::fromValue(1, MysqlDataType::LongBlob); + + self::assertSame(MysqlDataType::Tiny, $encoded->getType()); + } + + public function testNullEncodingIgnoresTarget(): void + { + $encoded = MysqlEncodedValue::fromValue(null, MysqlDataType::LongBlob); + + self::assertSame(MysqlDataType::Null, $encoded->getType()); + } +} + +enum StringTargetEnum: string +{ + case Foo = 'foo'; +} From 4339143662e20ff32a606644df85381337201b30 Mon Sep 17 00:00:00 2001 From: Aaron Piotrowski Date: Sun, 3 May 2026 10:52:05 -0500 Subject: [PATCH 05/10] Refactor a bit --- src/Internal/ConnectionProcessor.php | 10 +++++-- src/Internal/MysqlEncodedValue.php | 44 ++++++++++++---------------- 2 files changed, 27 insertions(+), 27 deletions(-) diff --git a/src/Internal/ConnectionProcessor.php b/src/Internal/ConnectionProcessor.php index c4df034..977b62a 100644 --- a/src/Internal/ConnectionProcessor.php +++ b/src/Internal/ConnectionProcessor.php @@ -490,13 +490,19 @@ public function execute(int $stmtId, string $query, array $params, array $prebou MysqlDataType::LongBlob => MysqlDataType::LongBlob, default => MysqlDataType::VarString, }; + $types[] = MysqlDataType::encodeInt16($preboundType->value); + continue; } $encodedValue = match ($paramType) { - MysqlDataType::Json => MysqlEncodedValue::fromJson($param), - default => MysqlEncodedValue::fromValue($param, $paramType), + MysqlDataType::TinyBlob, + MysqlDataType::Blob, + MysqlDataType::MediumBlob, + MysqlDataType::LongBlob => MysqlEncodedValue::forTargetType(MysqlDataType::LongBlob, $param), + MysqlDataType::Json => MysqlEncodedValue::forTargetType(MysqlDataType::Json, $param), + default => MysqlEncodedValue::fromValue($param), }; $types[] = MysqlDataType::encodeInt16($encodedValue->getType()->value); diff --git a/src/Internal/MysqlEncodedValue.php b/src/Internal/MysqlEncodedValue.php index 7cd8afb..2132c2d 100644 --- a/src/Internal/MysqlEncodedValue.php +++ b/src/Internal/MysqlEncodedValue.php @@ -7,11 +7,11 @@ /** @internal */ final class MysqlEncodedValue { - public static function fromValue(mixed $param, ?MysqlDataType $targetType = null): self + public static function fromValue(mixed $param): self { switch (\get_debug_type($param)) { case "string": - return new self(self::stringTypeFor($targetType), MysqlDataType::encodeInt(\strlen($param)) . $param); + return new self(MysqlDataType::VarString, MysqlDataType::encodeInt(\strlen($param)) . $param); case "int": if ($param >= -(1 << 7) && $param < (1 << 7)) { @@ -40,42 +40,36 @@ public static function fromValue(mixed $param, ?MysqlDataType $targetType = null default: if ($param instanceof \BackedEnum) { - return self::fromValue($param->value, $targetType); + return self::fromValue($param->value); } if ($param instanceof \Stringable) { - return self::fromValue((string) $param, $targetType); + return self::fromValue((string) $param); } throw new \TypeError("Unexpected type for query parameter: " . \get_debug_type($param)); } } - /** - * Picks the wire type for a PHP string parameter. Blob-family targets keep - * the binary (charset 63) interpretation of LongBlob so raw bytes are not - * transcoded against the connection charset; everything else uses VarString - * so MariaDB's native UUID column type and similar string-typed columns - * parse the value correctly. - */ - private static function stringTypeFor(?MysqlDataType $targetType): MysqlDataType + public static function forTargetType(MysqlDataType $targetType, mixed $data): self { - return match ($targetType) { - MysqlDataType::TinyBlob, - MysqlDataType::Blob, - MysqlDataType::MediumBlob, - MysqlDataType::LongBlob => MysqlDataType::LongBlob, - default => MysqlDataType::VarString, - }; - } - - public static function fromJson(?string $json): self - { - if ($json === null) { + if ($data === null) { return new self(MysqlDataType::Null, ""); } - return new self(MysqlDataType::Json, MysqlDataType::encodeInt(\strlen($json)) . $json); + if ($data instanceof \Stringable) { + $data = (string) $data; + } + + if (!\is_string($data)) { + throw new \TypeError(\sprintf( + "Expected string or null for %s column data, got %s", + $targetType->name, + \get_debug_type($data), + )); + } + + return new self($targetType, MysqlDataType::encodeInt(\strlen($data)) . $data); } private function __construct( From 619b2de876fc8db2aa692f7dc3530a2f03792a42 Mon Sep 17 00:00:00 2001 From: Aaron Piotrowski Date: Sun, 3 May 2026 10:55:25 -0500 Subject: [PATCH 06/10] Add test for blob data --- test/MysqlLinkTest.php | 32 +++++++++++++++++++++++++++++--- test/initialize.php | 25 +++++++++++++++++++++++-- 2 files changed, 52 insertions(+), 5 deletions(-) diff --git a/test/MysqlLinkTest.php b/test/MysqlLinkTest.php index a13e09e..7032c2e 100644 --- a/test/MysqlLinkTest.php +++ b/test/MysqlLinkTest.php @@ -233,7 +233,7 @@ public function testPrepared(): void $stmt = $db->prepare("SELECT * FROM main WHERE a = ? OR b = ?"); $result = $stmt->execute([1, 8]); $this->assertInstanceOf(MysqlResult::class, $result); - $this->assertSame(5, $result->getColumnCount()); + $this->assertSame(EXPECTED_COLUMN_COUNT, $result->getColumnCount()); $got = []; foreach ($result as $row) { $got[] = \array_values($row); @@ -243,7 +243,7 @@ public function testPrepared(): void $stmt = $db->prepare("SELECT * FROM main WHERE a = :a OR b = ?"); $result = $stmt->execute(["a" => 2, 5]); $this->assertInstanceOf(MysqlResult::class, $result); - $this->assertSame(5, $result->getColumnCount()); + $this->assertSame(EXPECTED_COLUMN_COUNT, $result->getColumnCount()); $got = []; foreach ($result as $row) { $got[] = \array_values($row); @@ -324,7 +324,7 @@ public function testExecute(): void $got[] = \array_values($row); } $this->assertCount(2, $got); - $this->assertSame([[2, 2, 3, self::EPOCH, 'b'], [4, 4, 5, self::EPOCH, 'd']], $got); + $this->assertSame([[2, 2, 3, self::EPOCH, 'b', null], [4, 4, 5, self::EPOCH, 'd', null]], $got); $result = $db->execute("INSERT INTO main (a, b) VALUES (:a, :b)", ["a" => 10, "b" => 11, "c" => '1970-01-01 00:00:00']); $this->assertInstanceOf(MysqlResult::class, $result); @@ -457,4 +457,30 @@ public function testBindJson(): void self::assertSame($json, $row['json_data']); } } + + public function provideBlobData(): iterable + { + foreach (\range(0, 9) as $i) { + yield 'blob-data-' . $i => [\random_bytes(10)]; + } + } + + /** + * @dataProvider provideBlobData + */ + public function testBlobData(string $data): void + { + $db = $this->getLink(); + + $result = $db->execute("INSERT INTO main SET e = :data", ['data' => $data]); + + $this->assertSame($result->getRowCount(), 1); + $id = $result->getLastInsertId(); + $this->assertNotEmpty($id); + + $result = $db->execute("SELECT e FROM main WHERE id = :id", ['id' => $id]); + $this->assertSame($data, $result->fetchRow()['e']); + + $db->close(); + } } diff --git a/test/initialize.php b/test/initialize.php index 9e28a7c..ade36c0 100644 --- a/test/initialize.php +++ b/test/initialize.php @@ -2,14 +2,35 @@ namespace Amp\Mysql\Test; +const EXPECTED_COLUMN_COUNT = 6; + function initialize(\mysqli $db): void { $db->query("CREATE DATABASE test"); - $db->query("CREATE TABLE test.main (id INT NOT NULL AUTO_INCREMENT, a INT, b INT, c DATETIME, d VARCHAR(255), PRIMARY KEY (id))"); + $db->query(<<query("INSERT INTO test.main (a, b, c, d) VALUES (1, 2, '$epoch', 'a'), (2, 3, '$epoch', 'b'), (3, 4, '$epoch', 'c'), (4, 5, '$epoch', 'd'), (5, 6, '$epoch', 'e')"); + $db->query(<<close(); } From 1e8ecd222261fafa7c4eca22e19be6a7458ca5d9 Mon Sep 17 00:00:00 2001 From: Aaron Piotrowski Date: Sun, 3 May 2026 10:58:07 -0500 Subject: [PATCH 07/10] Wrap in transaction --- test/MysqlLinkTest.php | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/test/MysqlLinkTest.php b/test/MysqlLinkTest.php index 7032c2e..e0b6cb6 100644 --- a/test/MysqlLinkTest.php +++ b/test/MysqlLinkTest.php @@ -472,14 +472,20 @@ public function testBlobData(string $data): void { $db = $this->getLink(); - $result = $db->execute("INSERT INTO main SET e = :data", ['data' => $data]); + $transaction = $db->beginTransaction(); + + try { + $result = $transaction->execute("INSERT INTO main SET e = :data", ['data' => $data]); - $this->assertSame($result->getRowCount(), 1); - $id = $result->getLastInsertId(); - $this->assertNotEmpty($id); + $this->assertSame($result->getRowCount(), 1); + $id = $result->getLastInsertId(); + $this->assertNotEmpty($id); - $result = $db->execute("SELECT e FROM main WHERE id = :id", ['id' => $id]); - $this->assertSame($data, $result->fetchRow()['e']); + $result = $transaction->execute("SELECT e FROM main WHERE id = :id", ['id' => $id]); + $this->assertSame($data, $result->fetchRow()['e']); + } finally { + $transaction->rollback(); + } $db->close(); } From d48e12343d62b8b2cf5f1d5346009e36a4788703 Mon Sep 17 00:00:00 2001 From: Aaron Piotrowski Date: Sun, 3 May 2026 11:08:14 -0500 Subject: [PATCH 08/10] Update test --- test/MysqlEncodedValueTest.php | 66 +++++----------------------------- 1 file changed, 8 insertions(+), 58 deletions(-) diff --git a/test/MysqlEncodedValueTest.php b/test/MysqlEncodedValueTest.php index f62cd00..9a32bd7 100644 --- a/test/MysqlEncodedValueTest.php +++ b/test/MysqlEncodedValueTest.php @@ -8,43 +8,7 @@ class MysqlEncodedValueTest extends TestCase { - /** - * @return iterable - */ - public function provideStringTargets(): iterable - { - yield 'no target (legacy default)' => [null, MysqlDataType::VarString]; - yield 'varchar' => [MysqlDataType::Varchar, MysqlDataType::VarString]; - yield 'string' => [MysqlDataType::String, MysqlDataType::VarString]; - yield 'var_string' => [MysqlDataType::VarString, MysqlDataType::VarString]; - yield 'tiny_blob' => [MysqlDataType::TinyBlob, MysqlDataType::LongBlob]; - yield 'blob' => [MysqlDataType::Blob, MysqlDataType::LongBlob]; - yield 'medium_blob' => [MysqlDataType::MediumBlob, MysqlDataType::LongBlob]; - yield 'long_blob' => [MysqlDataType::LongBlob, MysqlDataType::LongBlob]; - yield 'bit' => [MysqlDataType::Bit, MysqlDataType::VarString]; - yield 'enum' => [MysqlDataType::Enum, MysqlDataType::VarString]; - yield 'set' => [MysqlDataType::Set, MysqlDataType::VarString]; - yield 'geometry' => [MysqlDataType::Geometry, MysqlDataType::VarString]; - } - - /** - * @dataProvider provideStringTargets - */ - public function testStringPicksTypeBasedOnTarget(?MysqlDataType $target, MysqlDataType $expected): void - { - $encoded = MysqlEncodedValue::fromValue('hello', $target); - - self::assertSame($expected, $encoded->getType()); - } - - public function testBinaryPayloadIntoBlobStaysLongBlob(): void - { - $encoded = MysqlEncodedValue::fromValue(\random_bytes(64), MysqlDataType::Blob); - - self::assertSame(MysqlDataType::LongBlob, $encoded->getType()); - } - - public function testStringableIntoBlobStaysLongBlob(): void + public function testStringable(): void { $stringable = new class implements \Stringable { public function __toString(): string @@ -53,36 +17,22 @@ public function __toString(): string } }; - $encoded = MysqlEncodedValue::fromValue($stringable, MysqlDataType::LongBlob); + $encoded = MysqlEncodedValue::forTargetType(MysqlDataType::LongBlob, $stringable); self::assertSame(MysqlDataType::LongBlob, $encoded->getType()); } - public function testBackedEnumIntoStringTargetUsesVarString(): void - { - $enum = StringTargetEnum::Foo; - - $encoded = MysqlEncodedValue::fromValue($enum, MysqlDataType::Varchar); - - self::assertSame(MysqlDataType::VarString, $encoded->getType()); - } - - public function testIntegerEncodingIgnoresTarget(): void + public function testNullEncodingIgnoresTarget(): void { - $encoded = MysqlEncodedValue::fromValue(1, MysqlDataType::LongBlob); + $encoded = MysqlEncodedValue::forTargetType(MysqlDataType::LongBlob, null); - self::assertSame(MysqlDataType::Tiny, $encoded->getType()); + self::assertSame(MysqlDataType::Null, $encoded->getType()); } - public function testNullEncodingIgnoresTarget(): void + public function testNonStringValueThrows(): void { - $encoded = MysqlEncodedValue::fromValue(null, MysqlDataType::LongBlob); + self::expectException(\TypeError::class); - self::assertSame(MysqlDataType::Null, $encoded->getType()); + MysqlEncodedValue::forTargetType(MysqlDataType::LongBlob, 1); } } - -enum StringTargetEnum: string -{ - case Foo = 'foo'; -} From efea7d34955450ceb0d4388b8ee0dd3639b0160f Mon Sep 17 00:00:00 2001 From: Aaron Piotrowski Date: Sun, 3 May 2026 11:43:58 -0500 Subject: [PATCH 09/10] Refactor JSON column tests --- test/MysqlLinkTest.php | 107 ++++++++++++++++++++++++++++++++++++----- test/initialize.php | 3 +- 2 files changed, 97 insertions(+), 13 deletions(-) diff --git a/test/MysqlLinkTest.php b/test/MysqlLinkTest.php index e0b6cb6..3e9691b 100644 --- a/test/MysqlLinkTest.php +++ b/test/MysqlLinkTest.php @@ -317,14 +317,14 @@ public function testExecute(): void { $db = $this->getLink(); - $result = $db->execute("SELECT * FROM test.main WHERE a = ? OR b = ?", [2, 5]); + $result = $db->execute("SELECT id, a, b, c, d FROM test.main WHERE a = ? OR b = ?", [2, 5]); $this->assertInstanceOf(MysqlResult::class, $result); $got = []; foreach ($result as $row) { $got[] = \array_values($row); } $this->assertCount(2, $got); - $this->assertSame([[2, 2, 3, self::EPOCH, 'b', null], [4, 4, 5, self::EPOCH, 'd', null]], $got); + $this->assertSame([[2, 2, 3, self::EPOCH, 'b'], [4, 4, 5, self::EPOCH, 'd']], $got); $result = $db->execute("INSERT INTO main (a, b) VALUES (:a, :b)", ["a" => 10, "b" => 11, "c" => '1970-01-01 00:00:00']); $this->assertInstanceOf(MysqlResult::class, $result); @@ -444,18 +444,73 @@ public function testInsertSelect(): void $db->close(); } - public function testBindJson(): void + public function provideJsonData(): array { - $json = '{"key": "value"}'; + return \array_map( + fn (mixed $data) => [$data, \json_encode($data, \JSON_THROW_ON_ERROR)], + [ + 'object' => (object)['key' => 'value'], + 'array' => [1, 2, 3], + 'string' => 'string', + 'integer' => 123, + 'float' => 3.14159, + 'boolean' => true, + 'null' => null, + ], + ); + } - $statement = $this->getLink()->prepare("SELECT CAST(? AS JSON) AS json_data"); - $statement->bind(0, $json); + /** + * @dataProvider provideJsonData + */ + public function testJsonData(mixed $data, string $json): void + { + $db = $this->getLink(); - $result = $statement->execute(); + $transaction = $db->beginTransaction(); - foreach ($result as $row) { - self::assertSame($json, $row['json_data']); + try { + $result = $transaction->execute("INSERT INTO main SET f = :json", ['json' => $json]); + + self::assertSame($result->getRowCount(), 1); + $id = $result->getLastInsertId(); + self::assertNotEmpty($id); + + $result = $transaction->execute("SELECT f FROM main WHERE id = :id", ['id' => $id]); + self::assertEquals($data, \json_decode($result->fetchRow()['f'], flags: \JSON_THROW_ON_ERROR)); + } finally { + $transaction->rollback(); } + + $db->close(); + } + + /** + * @dataProvider provideJsonData + */ + public function testBindJsonData(mixed $data, string $json): void + { + $db = $this->getLink(); + + $transaction = $db->beginTransaction(); + + try { + $statement = $transaction->prepare("INSERT INTO main SET f = ?"); + $statement->bind(0, $json); + + $result = $statement->execute(); + + self::assertSame($result->getRowCount(), 1); + $id = $result->getLastInsertId(); + self::assertNotEmpty($id); + + $result = $transaction->execute("SELECT f FROM main WHERE id = :id", ['id' => $id]); + self::assertEquals($data, \json_decode($result->fetchRow()['f'], flags: \JSON_THROW_ON_ERROR)); + } finally { + $transaction->rollback(); + } + + $db->close(); } public function provideBlobData(): iterable @@ -477,12 +532,40 @@ public function testBlobData(string $data): void try { $result = $transaction->execute("INSERT INTO main SET e = :data", ['data' => $data]); - $this->assertSame($result->getRowCount(), 1); + self::assertSame($result->getRowCount(), 1); + $id = $result->getLastInsertId(); + self::assertNotEmpty($id); + + $result = $transaction->execute("SELECT e FROM main WHERE id = :id", ['id' => $id]); + self::assertSame($data, $result->fetchRow()['e']); + } finally { + $transaction->rollback(); + } + + $db->close(); + } + + /** + * @dataProvider provideBlobData + */ + public function testBindBlobData(string $data): void + { + $db = $this->getLink(); + + $transaction = $db->beginTransaction(); + + try { + $statement = $transaction->prepare("INSERT INTO main SET e = ?"); + $statement->bind(0, $data); + + $result = $statement->execute(); + + self::assertSame($result->getRowCount(), 1); $id = $result->getLastInsertId(); - $this->assertNotEmpty($id); + self::assertNotEmpty($id); $result = $transaction->execute("SELECT e FROM main WHERE id = :id", ['id' => $id]); - $this->assertSame($data, $result->fetchRow()['e']); + self::assertSame($data, $result->fetchRow()['e']); } finally { $transaction->rollback(); } diff --git a/test/initialize.php b/test/initialize.php index ade36c0..c6c5453 100644 --- a/test/initialize.php +++ b/test/initialize.php @@ -2,7 +2,7 @@ namespace Amp\Mysql\Test; -const EXPECTED_COLUMN_COUNT = 6; +const EXPECTED_COLUMN_COUNT = 7; function initialize(\mysqli $db): void { @@ -16,6 +16,7 @@ function initialize(\mysqli $db): void c DATETIME NULL, d VARCHAR(255) NULL, e BLOB NULL, + f JSON NULL, PRIMARY KEY (id) ); SQL); From cc595970910e72b1a18cfb1c8a252eb22a1c5845 Mon Sep 17 00:00:00 2001 From: Aaron Piotrowski Date: Sun, 3 May 2026 11:46:33 -0500 Subject: [PATCH 10/10] Style fix --- test/MysqlLinkTest.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/MysqlLinkTest.php b/test/MysqlLinkTest.php index 3e9691b..c0c00c4 100644 --- a/test/MysqlLinkTest.php +++ b/test/MysqlLinkTest.php @@ -449,7 +449,7 @@ public function provideJsonData(): array return \array_map( fn (mixed $data) => [$data, \json_encode($data, \JSON_THROW_ON_ERROR)], [ - 'object' => (object)['key' => 'value'], + 'object' => (object) ['key' => 'value'], 'array' => [1, 2, 3], 'string' => 'string', 'integer' => 123,