diff --git a/src/Internal/ConnectionProcessor.php b/src/Internal/ConnectionProcessor.php index df443bf..977b62a 100644 --- a/src/Internal/ConnectionProcessor.php +++ b/src/Internal/ConnectionProcessor.php @@ -483,12 +483,25 @@ 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), + MysqlDataType::TinyBlob, + MysqlDataType::Blob, + MysqlDataType::MediumBlob, + MysqlDataType::LongBlob => MysqlEncodedValue::forTargetType(MysqlDataType::LongBlob, $param), + MysqlDataType::Json => MysqlEncodedValue::forTargetType(MysqlDataType::Json, $param), default => MysqlEncodedValue::fromValue($param), }; diff --git a/src/Internal/MysqlEncodedValue.php b/src/Internal/MysqlEncodedValue.php index 6b9e169..2132c2d 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)) { @@ -51,13 +51,25 @@ public static function fromValue(mixed $param): self } } - public static function fromJson(?string $json): self + public static function forTargetType(MysqlDataType $targetType, mixed $data): 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( diff --git a/test/MysqlEncodedValueTest.php b/test/MysqlEncodedValueTest.php new file mode 100644 index 0000000..9a32bd7 --- /dev/null +++ b/test/MysqlEncodedValueTest.php @@ -0,0 +1,38 @@ +getType()); + } + + public function testNullEncodingIgnoresTarget(): void + { + $encoded = MysqlEncodedValue::forTargetType(MysqlDataType::LongBlob, null); + + self::assertSame(MysqlDataType::Null, $encoded->getType()); + } + + public function testNonStringValueThrows(): void + { + self::expectException(\TypeError::class); + + MysqlEncodedValue::forTargetType(MysqlDataType::LongBlob, 1); + } +} diff --git a/test/MysqlLinkTest.php b/test/MysqlLinkTest.php index a13e09e..c0c00c4 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); @@ -317,7 +317,7 @@ 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) { @@ -444,17 +444,132 @@ 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 + { + foreach (\range(0, 9) as $i) { + yield 'blob-data-' . $i => [\random_bytes(10)]; } } + + /** + * @dataProvider provideBlobData + */ + public function testBlobData(string $data): void + { + $db = $this->getLink(); + + $transaction = $db->beginTransaction(); + + try { + $result = $transaction->execute("INSERT INTO main SET e = :data", ['data' => $data]); + + 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(); + 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(); + } } diff --git a/test/initialize.php b/test/initialize.php index 9e28a7c..c6c5453 100644 --- a/test/initialize.php +++ b/test/initialize.php @@ -2,14 +2,36 @@ namespace Amp\Mysql\Test; +const EXPECTED_COLUMN_COUNT = 7; + 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(); }